v1.9.4: S3 optimizations — skip redundant health checks, folder drag-and-drop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-03 07:59:26 -05:00
parent bc2a3bc6b5
commit 61461767fd
4 changed files with 44 additions and 16 deletions

View File

@@ -74,6 +74,7 @@ class S3Client:
self._use_ssl = server.get("use_ssl", True) self._use_ssl = server.get("use_ssl", True)
self._client = None self._client = None
self._transfer_config = None self._transfer_config = None
self._last_ok: float = 0 # timestamp of last successful operation
# -- lifecycle -------------------------------------------------------- # -- lifecycle --------------------------------------------------------
@@ -101,6 +102,7 @@ class S3Client:
self._transfer_config = _get_transfer_config() self._transfer_config = _get_transfer_config()
# Test connection # Test connection
self._client.list_buckets() self._client.list_buckets()
self._last_ok = time.time()
log.info("S3 connected %s", self._endpoint) log.info("S3 connected %s", self._endpoint)
return True return True
except Exception as exc: except Exception as exc:
@@ -115,11 +117,16 @@ class S3Client:
return self.connect() return self.connect()
def _ensure_connected(self) -> bool: def _ensure_connected(self) -> bool:
"""Check connection, reconnect if needed.""" """Check connection, reconnect if needed.
Skips health-check if last success was <30s ago (avoids redundant RTTs).
"""
if self._client is None: if self._client is None:
return self._reconnect() return self._reconnect()
if time.time() - self._last_ok < 30:
return True
try: try:
self._client.list_buckets() self._client.list_buckets()
self._last_ok = time.time()
return True return True
except Exception: except Exception:
return self._reconnect() return self._reconnect()
@@ -145,6 +152,7 @@ class S3Client:
return [] return []
try: try:
resp = self._client.list_buckets() resp = self._client.list_buckets()
self._last_ok = time.time()
return resp.get("Buckets", []) return resp.get("Buckets", [])
except Exception as exc: except Exception as exc:
log.error("S3 list_buckets failed: %s", exc) log.error("S3 list_buckets failed: %s", exc)
@@ -179,6 +187,7 @@ class S3Client:
objects.append(obj) objects.append(obj)
for cp in page.get("CommonPrefixes", []): for cp in page.get("CommonPrefixes", []):
prefixes.append(cp["Prefix"]) prefixes.append(cp["Prefix"])
self._last_ok = time.time()
return objects, prefixes return objects, prefixes
except Exception as exc: except Exception as exc:
log.error("S3 list_objects failed: %s", exc) log.error("S3 list_objects failed: %s", exc)

View File

@@ -262,20 +262,34 @@ class S3Tab(ctk.CTkFrame):
self._dnd_active = False self._dnd_active = False
def _on_files_dropped(self, files): def _on_files_dropped(self, files):
"""Handle files dropped from OS file manager.""" """Handle files/folders dropped from OS file manager."""
if not self._client or not self._current_bucket: if not self._client or not self._current_bucket:
return return
# windnd gives list of bytes on Windows # windnd gives list of bytes on Windows
paths = [] raw_paths = []
for f in files: for f in files:
if isinstance(f, bytes): if isinstance(f, bytes):
paths.append(f.decode("utf-8", errors="replace")) raw_paths.append(f.decode("utf-8", errors="replace"))
else: else:
paths.append(str(f)) raw_paths.append(str(f))
paths = [p for p in paths if os.path.isfile(p)]
if not paths: # Collect (local_path, s3_key_suffix) pairs
upload_pairs: list[tuple[str, str]] = []
for p in raw_paths:
if os.path.isfile(p):
upload_pairs.append((p, os.path.basename(p)))
elif os.path.isdir(p):
base = os.path.basename(p.rstrip("/\\"))
for root, _dirs, fnames in os.walk(p):
for fn in fnames:
full = os.path.join(root, fn)
rel = os.path.relpath(full, os.path.dirname(p))
rel = rel.replace("\\", "/")
upload_pairs.append((full, rel))
if not upload_pairs:
return return
self._upload_files(paths) self._upload_pairs(upload_pairs)
def _show_progress(self, label: str, total_bytes: int): def _show_progress(self, label: str, total_bytes: int):
"""Show and reset the progress bar.""" """Show and reset the progress bar."""
@@ -315,11 +329,17 @@ class S3Tab(ctk.CTkFrame):
self.after(0, lambda: self._status_label.configure(text=message)) self.after(0, lambda: self._status_label.configure(text=message))
def _upload_files(self, paths: list[str]): def _upload_files(self, paths: list[str]):
"""Upload multiple files to current prefix.""" """Upload multiple files to current prefix (flat — no subdirs)."""
pairs = [(p, os.path.basename(p)) for p in paths if os.path.isfile(p)]
if pairs:
self._upload_pairs(pairs)
def _upload_pairs(self, pairs: list[tuple[str, str]]):
"""Upload (local_path, relative_key) pairs to current prefix."""
if not self._client or not self._current_bucket: if not self._client or not self._current_bucket:
return return
total_files = len(paths) total_files = len(pairs)
total_bytes = sum(os.path.getsize(p) for p in paths if os.path.isfile(p)) total_bytes = sum(os.path.getsize(p) for p, _ in pairs if os.path.isfile(p))
label = (t("s3_uploading_n").format(count=total_files) if total_files > 1 label = (t("s3_uploading_n").format(count=total_files) if total_files > 1
else t("s3_uploading")) else t("s3_uploading"))
self._status_label.configure(text=label) self._status_label.configure(text=label)
@@ -327,11 +347,10 @@ class S3Tab(ctk.CTkFrame):
def _do(): def _do():
ok_count = 0 ok_count = 0
for path in paths: for local_path, rel_key in pairs:
filename = os.path.basename(path) key = self._current_prefix + rel_key
key = self._current_prefix + filename
if self._client.upload_file( if self._client.upload_file(
path, self._current_bucket, key, local_path, self._current_bucket, key,
progress_cb=self._on_progress, progress_cb=self._on_progress,
status_cb=self._on_transfer_status): status_cb=self._on_transfer_status):
ok_count += 1 ok_count += 1

Binary file not shown.

View File

@@ -1,6 +1,6 @@
"""Version info for ServerManager.""" """Version info for ServerManager."""
__version__ = "1.9.3" __version__ = "1.9.4"
__app_name__ = "ServerManager" __app_name__ = "ServerManager"
__author__ = "aibot777" __author__ = "aibot777"
__description__ = "Desktop GUI for managing remote servers" __description__ = "Desktop GUI for managing remote servers"