s3ui 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- s3ui/__init__.py +1 -0
- s3ui/app.py +56 -0
- s3ui/constants.py +39 -0
- s3ui/core/__init__.py +0 -0
- s3ui/core/cost.py +218 -0
- s3ui/core/credentials.py +165 -0
- s3ui/core/download_worker.py +260 -0
- s3ui/core/errors.py +104 -0
- s3ui/core/listing_cache.py +178 -0
- s3ui/core/s3_client.py +358 -0
- s3ui/core/stats.py +128 -0
- s3ui/core/transfers.py +281 -0
- s3ui/core/upload_worker.py +311 -0
- s3ui/db/__init__.py +0 -0
- s3ui/db/database.py +143 -0
- s3ui/db/migrations/001_initial.sql +114 -0
- s3ui/logging_setup.py +18 -0
- s3ui/main_window.py +969 -0
- s3ui/models/__init__.py +0 -0
- s3ui/models/s3_objects.py +295 -0
- s3ui/models/transfer_model.py +282 -0
- s3ui/resources/__init__.py +0 -0
- s3ui/resources/s3ui.png +0 -0
- s3ui/ui/__init__.py +0 -0
- s3ui/ui/breadcrumb_bar.py +150 -0
- s3ui/ui/confirm_delete.py +60 -0
- s3ui/ui/cost_dialog.py +163 -0
- s3ui/ui/get_info.py +50 -0
- s3ui/ui/local_pane.py +226 -0
- s3ui/ui/name_conflict.py +68 -0
- s3ui/ui/s3_pane.py +547 -0
- s3ui/ui/settings_dialog.py +328 -0
- s3ui/ui/setup_wizard.py +462 -0
- s3ui/ui/stats_dialog.py +162 -0
- s3ui/ui/transfer_panel.py +153 -0
- s3ui-1.0.0.dist-info/METADATA +118 -0
- s3ui-1.0.0.dist-info/RECORD +40 -0
- s3ui-1.0.0.dist-info/WHEEL +4 -0
- s3ui-1.0.0.dist-info/entry_points.txt +2 -0
- s3ui-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Settings dialog with tabs for credentials, transfers, costs, and general."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtCore import pyqtSignal
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QCheckBox,
|
|
11
|
+
QComboBox,
|
|
12
|
+
QDialog,
|
|
13
|
+
QDialogButtonBox,
|
|
14
|
+
QFileDialog,
|
|
15
|
+
QFormLayout,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLabel,
|
|
18
|
+
QLineEdit,
|
|
19
|
+
QListWidget,
|
|
20
|
+
QMessageBox,
|
|
21
|
+
QPushButton,
|
|
22
|
+
QSpinBox,
|
|
23
|
+
QTabWidget,
|
|
24
|
+
QVBoxLayout,
|
|
25
|
+
QWidget,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from s3ui.core.credentials import CredentialStore, Profile, discover_aws_profiles
|
|
29
|
+
from s3ui.db.database import get_bool_pref, get_int_pref, get_pref, set_pref
|
|
30
|
+
from s3ui.ui.setup_wizard import AWS_REGIONS
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from s3ui.db.database import Database
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("s3ui.settings_dialog")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CredentialsTab(QWidget):
|
|
39
|
+
"""Tab for managing AWS credential profiles."""
|
|
40
|
+
|
|
41
|
+
profile_changed = pyqtSignal()
|
|
42
|
+
|
|
43
|
+
def __init__(self, store: CredentialStore, parent=None) -> None:
|
|
44
|
+
super().__init__(parent)
|
|
45
|
+
self._store = store
|
|
46
|
+
|
|
47
|
+
layout = QVBoxLayout(self)
|
|
48
|
+
|
|
49
|
+
# Profile list
|
|
50
|
+
layout.addWidget(QLabel("Saved Profiles:"))
|
|
51
|
+
self._profile_list = QListWidget()
|
|
52
|
+
layout.addWidget(self._profile_list)
|
|
53
|
+
|
|
54
|
+
# Buttons
|
|
55
|
+
btn_row = QHBoxLayout()
|
|
56
|
+
self._add_btn = QPushButton("Add Profile...")
|
|
57
|
+
self._add_btn.clicked.connect(self._on_add)
|
|
58
|
+
btn_row.addWidget(self._add_btn)
|
|
59
|
+
|
|
60
|
+
self._edit_btn = QPushButton("Edit...")
|
|
61
|
+
self._edit_btn.clicked.connect(self._on_edit)
|
|
62
|
+
self._edit_btn.setEnabled(False)
|
|
63
|
+
btn_row.addWidget(self._edit_btn)
|
|
64
|
+
|
|
65
|
+
self._delete_btn = QPushButton("Delete")
|
|
66
|
+
self._delete_btn.clicked.connect(self._on_delete)
|
|
67
|
+
self._delete_btn.setEnabled(False)
|
|
68
|
+
btn_row.addWidget(self._delete_btn)
|
|
69
|
+
|
|
70
|
+
btn_row.addStretch()
|
|
71
|
+
layout.addLayout(btn_row)
|
|
72
|
+
|
|
73
|
+
self._profile_list.currentItemChanged.connect(self._on_selection_changed)
|
|
74
|
+
self._refresh_list()
|
|
75
|
+
|
|
76
|
+
def _refresh_list(self) -> None:
|
|
77
|
+
self._profile_list.clear()
|
|
78
|
+
# Show AWS CLI profiles first
|
|
79
|
+
aws_profiles = discover_aws_profiles()
|
|
80
|
+
for name in aws_profiles:
|
|
81
|
+
self._profile_list.addItem(f"{name} (AWS CLI)")
|
|
82
|
+
# Then custom profiles from keyring
|
|
83
|
+
for name in self._store.list_profiles():
|
|
84
|
+
self._profile_list.addItem(name)
|
|
85
|
+
|
|
86
|
+
def _on_selection_changed(self) -> None:
|
|
87
|
+
has_selection = self._profile_list.currentItem() is not None
|
|
88
|
+
self._edit_btn.setEnabled(has_selection)
|
|
89
|
+
self._delete_btn.setEnabled(has_selection)
|
|
90
|
+
|
|
91
|
+
def _on_add(self) -> None:
|
|
92
|
+
dialog = _ProfileEditDialog(self._store, parent=self)
|
|
93
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
94
|
+
self._refresh_list()
|
|
95
|
+
self.profile_changed.emit()
|
|
96
|
+
|
|
97
|
+
def _on_edit(self) -> None:
|
|
98
|
+
item = self._profile_list.currentItem()
|
|
99
|
+
if not item:
|
|
100
|
+
return
|
|
101
|
+
profile = self._store.get_profile(item.text())
|
|
102
|
+
if profile:
|
|
103
|
+
dialog = _ProfileEditDialog(self._store, profile=profile, parent=self)
|
|
104
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
105
|
+
self._refresh_list()
|
|
106
|
+
self.profile_changed.emit()
|
|
107
|
+
|
|
108
|
+
def _on_delete(self) -> None:
|
|
109
|
+
item = self._profile_list.currentItem()
|
|
110
|
+
if not item:
|
|
111
|
+
return
|
|
112
|
+
name = item.text()
|
|
113
|
+
reply = QMessageBox.question(
|
|
114
|
+
self,
|
|
115
|
+
"Delete Profile",
|
|
116
|
+
f"Remove profile '{name}'?\n\nCredentials will be removed from the system keychain.",
|
|
117
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
118
|
+
)
|
|
119
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
120
|
+
self._store.delete_profile(name)
|
|
121
|
+
self._refresh_list()
|
|
122
|
+
self.profile_changed.emit()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _ProfileEditDialog(QDialog):
|
|
126
|
+
"""Dialog for adding or editing a credential profile."""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
store: CredentialStore,
|
|
131
|
+
profile: Profile | None = None,
|
|
132
|
+
parent=None,
|
|
133
|
+
) -> None:
|
|
134
|
+
super().__init__(parent)
|
|
135
|
+
self._store = store
|
|
136
|
+
self._editing = profile is not None
|
|
137
|
+
|
|
138
|
+
self.setWindowTitle("Edit Profile" if self._editing else "Add Profile")
|
|
139
|
+
self.setMinimumWidth(400)
|
|
140
|
+
|
|
141
|
+
layout = QFormLayout(self)
|
|
142
|
+
|
|
143
|
+
self._name_edit = QLineEdit()
|
|
144
|
+
if profile:
|
|
145
|
+
self._name_edit.setText(profile.name)
|
|
146
|
+
self._name_edit.setReadOnly(True)
|
|
147
|
+
layout.addRow("Profile Name:", self._name_edit)
|
|
148
|
+
|
|
149
|
+
self._access_key_edit = QLineEdit()
|
|
150
|
+
if profile:
|
|
151
|
+
self._access_key_edit.setText(profile.access_key_id)
|
|
152
|
+
layout.addRow("Access Key ID:", self._access_key_edit)
|
|
153
|
+
|
|
154
|
+
self._secret_key_edit = QLineEdit()
|
|
155
|
+
self._secret_key_edit.setEchoMode(QLineEdit.EchoMode.Password)
|
|
156
|
+
if profile:
|
|
157
|
+
self._secret_key_edit.setText(profile.secret_access_key)
|
|
158
|
+
layout.addRow("Secret Access Key:", self._secret_key_edit)
|
|
159
|
+
|
|
160
|
+
self._region_combo = QComboBox()
|
|
161
|
+
for display_name, region_code in AWS_REGIONS:
|
|
162
|
+
self._region_combo.addItem(f"{display_name} ({region_code})", region_code)
|
|
163
|
+
if profile:
|
|
164
|
+
for i in range(self._region_combo.count()):
|
|
165
|
+
if self._region_combo.itemData(i) == profile.region:
|
|
166
|
+
self._region_combo.setCurrentIndex(i)
|
|
167
|
+
break
|
|
168
|
+
layout.addRow("Region:", self._region_combo)
|
|
169
|
+
|
|
170
|
+
self._endpoint_edit = QLineEdit()
|
|
171
|
+
self._endpoint_edit.setPlaceholderText("e.g., http://localhost:9000")
|
|
172
|
+
if profile:
|
|
173
|
+
self._endpoint_edit.setText(profile.endpoint_url)
|
|
174
|
+
layout.addRow("Endpoint URL (optional):", self._endpoint_edit)
|
|
175
|
+
|
|
176
|
+
buttons = QDialogButtonBox(
|
|
177
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
178
|
+
)
|
|
179
|
+
buttons.accepted.connect(self._on_accept)
|
|
180
|
+
buttons.rejected.connect(self.reject)
|
|
181
|
+
layout.addRow(buttons)
|
|
182
|
+
|
|
183
|
+
def _on_accept(self) -> None:
|
|
184
|
+
name = self._name_edit.text().strip()
|
|
185
|
+
access_key = self._access_key_edit.text().strip()
|
|
186
|
+
secret_key = self._secret_key_edit.text().strip()
|
|
187
|
+
region = self._region_combo.currentData()
|
|
188
|
+
endpoint_url = self._endpoint_edit.text().strip()
|
|
189
|
+
|
|
190
|
+
if not name or not access_key or not secret_key:
|
|
191
|
+
QMessageBox.warning(self, "Missing Fields", "Please fill in all fields.")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
profile = Profile(
|
|
195
|
+
name=name,
|
|
196
|
+
access_key_id=access_key,
|
|
197
|
+
secret_access_key=secret_key,
|
|
198
|
+
region=region,
|
|
199
|
+
endpoint_url=endpoint_url,
|
|
200
|
+
)
|
|
201
|
+
self._store.save_profile(profile)
|
|
202
|
+
self.accept()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TransfersTab(QWidget):
|
|
206
|
+
"""Tab for transfer settings."""
|
|
207
|
+
|
|
208
|
+
def __init__(self, db: Database | None = None, parent=None) -> None:
|
|
209
|
+
super().__init__(parent)
|
|
210
|
+
self._db = db
|
|
211
|
+
|
|
212
|
+
layout = QFormLayout(self)
|
|
213
|
+
|
|
214
|
+
self._max_concurrent = QSpinBox()
|
|
215
|
+
self._max_concurrent.setRange(1, 16)
|
|
216
|
+
self._max_concurrent.setValue(4)
|
|
217
|
+
layout.addRow("Max concurrent transfers:", self._max_concurrent)
|
|
218
|
+
|
|
219
|
+
self._retention_combo = QComboBox()
|
|
220
|
+
self._retention_combo.addItems(
|
|
221
|
+
[
|
|
222
|
+
"Clear after session",
|
|
223
|
+
"Keep for 24 hours",
|
|
224
|
+
"Keep forever",
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
layout.addRow("Completed transfer retention:", self._retention_combo)
|
|
228
|
+
|
|
229
|
+
if db:
|
|
230
|
+
val = get_int_pref(db, "max_concurrent_transfers", 4)
|
|
231
|
+
self._max_concurrent.setValue(val)
|
|
232
|
+
ret = get_pref(db, "transfer_retention", "Clear after session")
|
|
233
|
+
idx = self._retention_combo.findText(ret)
|
|
234
|
+
if idx >= 0:
|
|
235
|
+
self._retention_combo.setCurrentIndex(idx)
|
|
236
|
+
|
|
237
|
+
def apply_settings(self) -> None:
|
|
238
|
+
if self._db:
|
|
239
|
+
set_pref(self._db, "max_concurrent_transfers", str(self._max_concurrent.value()))
|
|
240
|
+
set_pref(self._db, "transfer_retention", self._retention_combo.currentText())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class GeneralTab(QWidget):
|
|
244
|
+
"""Tab for general settings."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, db: Database | None = None, parent=None) -> None:
|
|
247
|
+
super().__init__(parent)
|
|
248
|
+
self._db = db
|
|
249
|
+
|
|
250
|
+
layout = QFormLayout(self)
|
|
251
|
+
|
|
252
|
+
# Default local directory
|
|
253
|
+
dir_row = QHBoxLayout()
|
|
254
|
+
self._dir_edit = QLineEdit()
|
|
255
|
+
dir_row.addWidget(self._dir_edit)
|
|
256
|
+
browse_btn = QPushButton("Browse...")
|
|
257
|
+
browse_btn.clicked.connect(self._browse_directory)
|
|
258
|
+
dir_row.addWidget(browse_btn)
|
|
259
|
+
layout.addRow("Default local directory:", dir_row)
|
|
260
|
+
|
|
261
|
+
# Show hidden files
|
|
262
|
+
self._show_hidden = QCheckBox("Show hidden files by default")
|
|
263
|
+
layout.addRow(self._show_hidden)
|
|
264
|
+
|
|
265
|
+
if db:
|
|
266
|
+
from pathlib import Path
|
|
267
|
+
|
|
268
|
+
self._dir_edit.setText(get_pref(db, "default_local_dir", str(Path.home())))
|
|
269
|
+
self._show_hidden.setChecked(get_bool_pref(db, "show_hidden_files", False))
|
|
270
|
+
|
|
271
|
+
def _browse_directory(self) -> None:
|
|
272
|
+
path = QFileDialog.getExistingDirectory(
|
|
273
|
+
self, "Select Default Directory", self._dir_edit.text()
|
|
274
|
+
)
|
|
275
|
+
if path:
|
|
276
|
+
self._dir_edit.setText(path)
|
|
277
|
+
|
|
278
|
+
def apply_settings(self) -> None:
|
|
279
|
+
if self._db:
|
|
280
|
+
set_pref(self._db, "default_local_dir", self._dir_edit.text())
|
|
281
|
+
set_pref(self._db, "show_hidden_files", str(self._show_hidden.isChecked()))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class SettingsDialog(QDialog):
|
|
285
|
+
"""Application settings dialog."""
|
|
286
|
+
|
|
287
|
+
settings_changed = pyqtSignal()
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
store: CredentialStore | None = None,
|
|
292
|
+
db: Database | None = None,
|
|
293
|
+
parent=None,
|
|
294
|
+
) -> None:
|
|
295
|
+
super().__init__(parent)
|
|
296
|
+
self._store = store or CredentialStore()
|
|
297
|
+
self._db = db
|
|
298
|
+
self.setWindowTitle("Settings")
|
|
299
|
+
self.setMinimumSize(500, 400)
|
|
300
|
+
|
|
301
|
+
layout = QVBoxLayout(self)
|
|
302
|
+
|
|
303
|
+
self._tabs = QTabWidget()
|
|
304
|
+
|
|
305
|
+
self._cred_tab = CredentialsTab(self._store)
|
|
306
|
+
self._tabs.addTab(self._cred_tab, "Credentials")
|
|
307
|
+
|
|
308
|
+
self._transfers_tab = TransfersTab(db)
|
|
309
|
+
self._tabs.addTab(self._transfers_tab, "Transfers")
|
|
310
|
+
|
|
311
|
+
self._general_tab = GeneralTab(db)
|
|
312
|
+
self._tabs.addTab(self._general_tab, "General")
|
|
313
|
+
|
|
314
|
+
layout.addWidget(self._tabs)
|
|
315
|
+
|
|
316
|
+
# Buttons
|
|
317
|
+
buttons = QDialogButtonBox(
|
|
318
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
319
|
+
)
|
|
320
|
+
buttons.accepted.connect(self._on_accept)
|
|
321
|
+
buttons.rejected.connect(self.reject)
|
|
322
|
+
layout.addWidget(buttons)
|
|
323
|
+
|
|
324
|
+
def _on_accept(self) -> None:
|
|
325
|
+
self._transfers_tab.apply_settings()
|
|
326
|
+
self._general_tab.apply_settings()
|
|
327
|
+
self.settings_changed.emit()
|
|
328
|
+
self.accept()
|