v1.9.0: S3 server type — bucket/object browser, drag-and-drop upload, resilient transfers

New server type: S3 (MinIO, AWS, any S3-compatible storage)
- core/s3_client.py: boto3 client with auto-reconnect, 10 retries, exponential backoff, multipart upload/download, tcp_keepalive
- gui/tabs/s3_tab.py: object browser (Treeview), bucket selector, folder navigation, drag-and-drop upload from Explorer (windnd), progress bar with %, multi-file upload
- CLI: --s3-buckets, --s3-ls, --s3-upload, --s3-download, --s3-delete with retry
- ServerDialog: access_key, secret_key, bucket fields
- Registration: server_store, connection_factory, status_checker, icons, app, i18n (EN/RU/ZH)
- Fix: build.py cleanup_old_releases now sorts by semver (was lexicographic, broke v1.8.100+)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-03 06:32:03 -05:00
parent f2dc978c57
commit 9b0e4c76a3
19 changed files with 1277 additions and 8 deletions

View File

@@ -24,6 +24,7 @@ FIELD_MAP = {
"prometheus": ["use_ssl"],
"rdp": ["user", "password", "rdp_resolution", "rdp_quality", "rdp_clipboard", "rdp_drives", "rdp_printers"],
"vnc": ["password"],
"s3": ["access_key", "secret_key", "bucket", "use_ssl"],
}
@@ -242,6 +243,27 @@ class ServerDialog(ctk.CTkToplevel):
ctk.CTkCheckBox(f, text=t("rdp_printers"), variable=self._rdp_printers_var).pack(fill="x", padx=20, pady=(4, 2))
self._field_frames["rdp_printers"] = f
# --- access_key ---
f = ctk.CTkFrame(self, fg_color="transparent")
ctk.CTkLabel(f, text=t("access_key"), anchor="w").pack(fill="x", **pad)
self.access_key_entry = ctk.CTkEntry(f, placeholder_text="AKIAIOSFODNN7EXAMPLE")
self.access_key_entry.pack(fill="x", **entry_pad)
self._field_frames["access_key"] = f
# --- secret_key ---
f = ctk.CTkFrame(self, fg_color="transparent")
ctk.CTkLabel(f, text=t("secret_key"), anchor="w").pack(fill="x", **pad)
self.secret_key_entry = ctk.CTkEntry(f, show="*", placeholder_text=t("placeholder_secret_key"))
self.secret_key_entry.pack(fill="x", **entry_pad)
self._field_frames["secret_key"] = f
# --- bucket ---
f = ctk.CTkFrame(self, fg_color="transparent")
ctk.CTkLabel(f, text=t("bucket"), anchor="w").pack(fill="x", **pad)
self.bucket_entry = ctk.CTkEntry(f, placeholder_text="my-bucket")
self.bucket_entry.pack(fill="x", **entry_pad)
self._field_frames["bucket"] = f
# --- use_ssl ---
f = ctk.CTkFrame(self, fg_color="transparent")
self.use_ssl_var = ctk.BooleanVar(value=False)
@@ -282,6 +304,9 @@ class ServerDialog(ctk.CTkToplevel):
self.db_index_entry.insert(0, str(server.get("db_index", "")))
self.api_token_entry.insert(0, server.get("api_token", ""))
self.use_ssl_var.set(server.get("use_ssl", False))
self.access_key_entry.insert(0, server.get("access_key", ""))
self.secret_key_entry.insert(0, server.get("secret_key", ""))
self.bucket_entry.insert(0, server.get("bucket", ""))
# RDP settings
res_raw = server.get("rdp_resolution", "auto")
@@ -411,6 +436,21 @@ class ServerDialog(ctk.CTkToplevel):
if token:
server_data["api_token"] = token
if "access_key" in visible:
ak = self.access_key_entry.get().strip()
if ak:
server_data["access_key"] = ak
if "secret_key" in visible:
sk = self.secret_key_entry.get()
if sk:
server_data["secret_key"] = sk
if "bucket" in visible:
bkt = self.bucket_entry.get().strip()
if bkt:
server_data["bucket"] = bkt
if "use_ssl" in visible:
if self.use_ssl_var.get():
server_data["use_ssl"] = True