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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
BIN
releases/ServerManager-v1.9.4-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.9.4-win-x64.exe
Normal file
Binary file not shown.
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user