diff --git a/core/i18n.py b/core/i18n.py index b50fe4c..6adbece 100644 --- a/core/i18n.py +++ b/core/i18n.py @@ -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": "生成链接失败", diff --git a/core/s3_client.py b/core/s3_client.py index 6f052c7..91ce152 100644 --- a/core/s3_client.py +++ b/core/s3_client.py @@ -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(): diff --git a/gui/tabs/s3_tab.py b/gui/tabs/s3_tab.py index a795063..7e3dcec 100644 --- a/gui/tabs/s3_tab.py +++ b/gui/tabs/s3_tab.py @@ -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) diff --git a/releases/ServerManager-v1.9.3-win-x64.exe b/releases/ServerManager-v1.9.3-win-x64.exe new file mode 100644 index 0000000..b9e9cca Binary files /dev/null and b/releases/ServerManager-v1.9.3-win-x64.exe differ diff --git a/version.py b/version.py index a866cfb..99e65b7 100755 --- a/version.py +++ b/version.py @@ -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"