v1.9.3: S3 context menu — two link types: temp 48h + permanent direct
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -390,7 +390,8 @@ _EN = {
|
||||
"s3_uploading_n": "Uploading {count} files...",
|
||||
"s3_uploaded_n": "Uploaded {count} files",
|
||||
"s3_upload_partial": "Uploaded {ok}/{total} files",
|
||||
"s3_copy_link": "Copy Link",
|
||||
"s3_copy_link_48h": "Copy Link (48h)",
|
||||
"s3_copy_link_permanent": "Copy Direct Link",
|
||||
"s3_generating_link": "Generating link...",
|
||||
"s3_link_copied": "Link copied to clipboard",
|
||||
"s3_link_failed": "Failed to generate link",
|
||||
@@ -896,7 +897,8 @@ _RU = {
|
||||
"s3_uploading_n": "Загрузка {count} файлов...",
|
||||
"s3_uploaded_n": "Загружено {count} файлов",
|
||||
"s3_upload_partial": "Загружено {ok}/{total} файлов",
|
||||
"s3_copy_link": "Копировать ссылку",
|
||||
"s3_copy_link_48h": "Ссылка (48ч)",
|
||||
"s3_copy_link_permanent": "Прямая ссылка",
|
||||
"s3_generating_link": "Генерация ссылки...",
|
||||
"s3_link_copied": "Ссылка скопирована",
|
||||
"s3_link_failed": "Ошибка генерации ссылки",
|
||||
@@ -1402,7 +1404,8 @@ _ZH = {
|
||||
"s3_uploading_n": "正在上传 {count} 个文件...",
|
||||
"s3_uploaded_n": "已上传 {count} 个文件",
|
||||
"s3_upload_partial": "已上传 {ok}/{total} 个文件",
|
||||
"s3_copy_link": "复制链接",
|
||||
"s3_copy_link_48h": "复制链接 (48小时)",
|
||||
"s3_copy_link_permanent": "复制直接链接",
|
||||
"s3_generating_link": "生成链接中...",
|
||||
"s3_link_copied": "链接已复制",
|
||||
"s3_link_failed": "生成链接失败",
|
||||
|
||||
@@ -297,6 +297,11 @@ class S3Client:
|
||||
log.error("S3 presigned URL failed: %s", exc)
|
||||
return None
|
||||
|
||||
def get_direct_url(self, bucket: str, key: str) -> str:
|
||||
"""Build a direct (permanent) URL: endpoint/bucket/key."""
|
||||
endpoint = self._endpoint.rstrip("/")
|
||||
return f"{endpoint}/{bucket}/{key}"
|
||||
|
||||
def get_object_size(self, bucket: str, key: str) -> int:
|
||||
"""Get size of an object in bytes."""
|
||||
if not self._ensure_connected():
|
||||
|
||||
@@ -209,9 +209,14 @@ class S3Tab(ctk.CTkFrame):
|
||||
# Right-click context menu
|
||||
self._ctx_menu = tk.Menu(self._tree, tearoff=0)
|
||||
self._ctx_menu.add_command(
|
||||
label=icon_text("copy", t("s3_copy_link")),
|
||||
command=self._copy_link,
|
||||
label=icon_text("copy", t("s3_copy_link_48h")),
|
||||
command=self._copy_link_48h,
|
||||
)
|
||||
self._ctx_menu.add_command(
|
||||
label=icon_text("copy", t("s3_copy_link_permanent")),
|
||||
command=self._copy_link_permanent,
|
||||
)
|
||||
self._ctx_menu.add_separator()
|
||||
self._ctx_menu.add_command(
|
||||
label=icon_text("download", t("s3_download")),
|
||||
command=self._download,
|
||||
@@ -491,33 +496,54 @@ class S3Tab(ctk.CTkFrame):
|
||||
name = str(name)
|
||||
is_folder = name.endswith("/")
|
||||
# Enable/disable menu items based on type
|
||||
self._ctx_menu.entryconfigure(0, state="normal" if not is_folder else "disabled") # Copy link
|
||||
self._ctx_menu.entryconfigure(1, state="normal" if not is_folder else "disabled") # Download
|
||||
self._ctx_menu.entryconfigure(3, state="normal" if not is_folder else "disabled") # Delete
|
||||
state = "normal" if not is_folder else "disabled"
|
||||
self._ctx_menu.entryconfigure(0, state=state) # Link 48h
|
||||
self._ctx_menu.entryconfigure(1, state=state) # Link permanent
|
||||
self._ctx_menu.entryconfigure(3, state=state) # Download
|
||||
self._ctx_menu.entryconfigure(5, state=state) # Delete
|
||||
self._ctx_menu.tk_popup(event.x_root, event.y_root)
|
||||
|
||||
def _copy_link(self):
|
||||
"""Generate presigned URL and copy to clipboard."""
|
||||
def _get_selected_key(self) -> str | None:
|
||||
"""Return the full S3 key for the selected file, or None."""
|
||||
sel = self._tree.selection()
|
||||
if not sel or not self._client or not self._current_bucket:
|
||||
return
|
||||
return None
|
||||
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
|
||||
return None
|
||||
return self._current_prefix + name
|
||||
|
||||
key = self._current_prefix + name
|
||||
def _copy_link_48h(self):
|
||||
"""Generate presigned URL (48h) and copy to clipboard."""
|
||||
key = self._get_selected_key()
|
||||
if not key:
|
||||
return
|
||||
self._status_label.configure(text=t("s3_generating_link"))
|
||||
|
||||
def _do():
|
||||
url = self._client.generate_presigned_url(self._current_bucket, key)
|
||||
self.after(0, lambda: self._on_link_ready(url))
|
||||
url = self._client.generate_presigned_url(
|
||||
self._current_bucket, key, expires_in=48 * 3600)
|
||||
self.after(0, lambda: self._on_link_ready(url, "48h"))
|
||||
|
||||
threading.Thread(target=_do, daemon=True).start()
|
||||
|
||||
def _on_link_ready(self, url: str | None):
|
||||
def _copy_link_permanent(self):
|
||||
"""Build direct (permanent) URL and copy to clipboard."""
|
||||
key = self._get_selected_key()
|
||||
if not key:
|
||||
return
|
||||
url = self._client.get_direct_url(self._current_bucket, key)
|
||||
if url:
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(url)
|
||||
self._status_label.configure(text=t("s3_link_copied"))
|
||||
else:
|
||||
self._status_label.configure(text=t("s3_link_failed"))
|
||||
|
||||
def _on_link_ready(self, url: str | None, kind: str = ""):
|
||||
if url:
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(url)
|
||||
|
||||
BIN
releases/ServerManager-v1.9.3-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.9.3-win-x64.exe
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
"""Version info for ServerManager."""
|
||||
|
||||
__version__ = "1.9.2"
|
||||
__version__ = "1.9.3"
|
||||
__app_name__ = "ServerManager"
|
||||
__author__ = "aibot777"
|
||||
__description__ = "Desktop GUI for managing remote servers"
|
||||
|
||||
Reference in New Issue
Block a user