v1.9.7: S3 folder download — recursive with preserved directory structure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -547,7 +547,7 @@ class S3Tab(ctk.CTkFrame):
|
||||
file_state = "normal" if not is_folder else "disabled"
|
||||
self._ctx_menu.entryconfigure(0, state=file_state) # Link 48h
|
||||
self._ctx_menu.entryconfigure(1, state=file_state) # Link permanent
|
||||
self._ctx_menu.entryconfigure(3, state=file_state) # Download
|
||||
self._ctx_menu.entryconfigure(3, state="normal") # Download (files + folders)
|
||||
# 5 = New Folder — always enabled
|
||||
# 7 = Delete — enabled for both files and folders
|
||||
self._ctx_menu.tk_popup(event.x_root, event.y_root)
|
||||
@@ -644,21 +644,25 @@ class S3Tab(ctk.CTkFrame):
|
||||
|
||||
def _download(self):
|
||||
sel = self._tree.selection()
|
||||
if not sel:
|
||||
if not sel or not self._client or not self._current_bucket:
|
||||
return
|
||||
item = self._tree.item(sel[0])
|
||||
name = item["values"][0] if item["values"] else ""
|
||||
if not isinstance(name, str):
|
||||
name = str(name)
|
||||
if name.endswith("/"):
|
||||
return # Can't download a folder
|
||||
|
||||
if name.endswith("/"):
|
||||
self._download_folder(name)
|
||||
else:
|
||||
self._download_file(name)
|
||||
|
||||
def _download_file(self, name: str):
|
||||
"""Download a single file."""
|
||||
key = self._current_prefix + name
|
||||
save_path = filedialog.asksaveasfilename(initialfile=name)
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
# Get file size for progress
|
||||
total_bytes = self._client.get_object_size(self._current_bucket, key)
|
||||
label = t("s3_downloading")
|
||||
self._status_label.configure(text=label)
|
||||
@@ -669,13 +673,61 @@ class S3Tab(ctk.CTkFrame):
|
||||
self._current_bucket, key, save_path,
|
||||
progress_cb=self._on_progress,
|
||||
status_cb=self._on_transfer_status)
|
||||
if ok:
|
||||
self.after(0, lambda: self._on_download_done(True))
|
||||
else:
|
||||
self.after(0, lambda: self._on_download_done(False))
|
||||
self.after(0, lambda: self._on_download_done(ok))
|
||||
|
||||
threading.Thread(target=_do, daemon=True).start()
|
||||
|
||||
def _download_folder(self, display_name: str):
|
||||
"""Download all objects under a prefix, preserving directory structure."""
|
||||
clean = display_name.replace("\U0001f4c1 ", "").strip()
|
||||
prefix = self._current_prefix + clean
|
||||
|
||||
# Ask user to pick a local directory
|
||||
dest_dir = filedialog.askdirectory(title=t("s3_download_folder_title"))
|
||||
if not dest_dir:
|
||||
return
|
||||
|
||||
self._status_label.configure(text=t("s3_downloading"))
|
||||
|
||||
def _do():
|
||||
# List all objects recursively under this prefix
|
||||
objects = self._client.list_all_objects(self._current_bucket, prefix)
|
||||
if not objects:
|
||||
self.after(0, lambda: self._status_label.configure(
|
||||
text=t("s3_download_failed")))
|
||||
return
|
||||
|
||||
total_bytes = sum(o.get("Size", 0) for o in objects)
|
||||
self.after(0, lambda: self._show_progress(
|
||||
t("s3_downloading_n").format(count=len(objects)), total_bytes))
|
||||
|
||||
ok_count = 0
|
||||
for obj in objects:
|
||||
obj_key = obj["Key"]
|
||||
# Relative path from the selected folder
|
||||
rel = obj_key[len(self._current_prefix):]
|
||||
local_path = os.path.join(dest_dir, rel.replace("/", os.sep))
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
if self._client.download_file(
|
||||
self._current_bucket, obj_key, local_path,
|
||||
progress_cb=self._on_progress,
|
||||
status_cb=self._on_transfer_status):
|
||||
ok_count += 1
|
||||
|
||||
total = len(objects)
|
||||
self.after(0, lambda: self._on_folder_download_done(ok_count, total))
|
||||
|
||||
threading.Thread(target=_do, daemon=True).start()
|
||||
|
||||
def _on_folder_download_done(self, ok_count: int, total: int):
|
||||
self._hide_progress()
|
||||
if ok_count == total:
|
||||
self._status_label.configure(
|
||||
text=t("s3_downloaded_n").format(count=ok_count))
|
||||
else:
|
||||
self._status_label.configure(
|
||||
text=t("s3_download_partial").format(ok=ok_count, total=total))
|
||||
|
||||
def _on_download_done(self, success: bool):
|
||||
self._hide_progress()
|
||||
if success:
|
||||
|
||||
Reference in New Issue
Block a user