diff --git a/core/i18n.py b/core/i18n.py index c0d7f4e..ba71677 100644 --- a/core/i18n.py +++ b/core/i18n.py @@ -390,6 +390,12 @@ _EN = { "s3_uploading_n": "Uploading {count} files...", "s3_uploaded_n": "Uploaded {count} files", "s3_upload_partial": "Uploaded {ok}/{total} files", + "s3_create_bucket": "Create Bucket", + "s3_bucket_name_prompt": "Bucket name:", + "s3_delete_bucket": "Delete Bucket", + "s3_delete_bucket_confirm": "Delete bucket \"{name}\"? It must be empty.", + "s3_bucket_created": "Bucket \"{name}\" created", + "s3_bucket_deleted": "Bucket \"{name}\" deleted", "s3_new_folder": "New Folder", "s3_folder_name_prompt": "Folder name:", "s3_creating_folder": "Creating folder...", @@ -907,6 +913,12 @@ _RU = { "s3_uploading_n": "Загрузка {count} файлов...", "s3_uploaded_n": "Загружено {count} файлов", "s3_upload_partial": "Загружено {ok}/{total} файлов", + "s3_create_bucket": "Создать бакет", + "s3_bucket_name_prompt": "Имя бакета:", + "s3_delete_bucket": "Удалить бакет", + "s3_delete_bucket_confirm": "Удалить бакет \"{name}\"? Он должен быть пустым.", + "s3_bucket_created": "Бакет \"{name}\" создан", + "s3_bucket_deleted": "Бакет \"{name}\" удалён", "s3_new_folder": "Новая папка", "s3_folder_name_prompt": "Имя папки:", "s3_creating_folder": "Создание папки...", @@ -1424,6 +1436,12 @@ _ZH = { "s3_uploading_n": "正在上传 {count} 个文件...", "s3_uploaded_n": "已上传 {count} 个文件", "s3_upload_partial": "已上传 {ok}/{total} 个文件", + "s3_create_bucket": "创建存储桶", + "s3_bucket_name_prompt": "存储桶名称:", + "s3_delete_bucket": "删除存储桶", + "s3_delete_bucket_confirm": "删除存储桶 \"{name}\"?必须为空。", + "s3_bucket_created": "存储桶 \"{name}\" 已创建", + "s3_bucket_deleted": "存储桶 \"{name}\" 已删除", "s3_new_folder": "新建文件夹", "s3_folder_name_prompt": "文件夹名称:", "s3_creating_folder": "创建文件夹中...", diff --git a/core/s3_client.py b/core/s3_client.py index b68e2dd..5c96eec 100644 --- a/core/s3_client.py +++ b/core/s3_client.py @@ -518,3 +518,29 @@ class S3Client: return resp.get("ContentLength", 0) except Exception: return 0 + + def create_bucket(self, bucket_name: str) -> bool: + """Create a new S3 bucket.""" + if not self._ensure_connected(): + return False + try: + self._client.create_bucket(Bucket=bucket_name) + self._last_ok = time.time() + log.info("S3 bucket created: %s", bucket_name) + return True + except Exception as exc: + log.error("S3 create_bucket failed: %s", exc) + return False + + def delete_bucket(self, bucket_name: str) -> bool: + """Delete an empty S3 bucket.""" + if not self._ensure_connected(): + return False + try: + self._client.delete_bucket(Bucket=bucket_name) + self._last_ok = time.time() + log.info("S3 bucket deleted: %s", bucket_name) + return True + except Exception as exc: + log.error("S3 delete_bucket failed: %s", exc) + return False diff --git a/gui/tabs/s3_tab.py b/gui/tabs/s3_tab.py index 228e605..497e352 100644 --- a/gui/tabs/s3_tab.py +++ b/gui/tabs/s3_tab.py @@ -153,7 +153,24 @@ class S3Tab(ctk.CTkFrame): bucket_frame, variable=self._bucket_var, values=[""], width=200, command=self._on_bucket_change, ) - self._bucket_menu.pack(side="left", padx=(0, 15)) + self._bucket_menu.pack(side="left", padx=(0, 5)) + + # Create bucket [+] + self._create_bucket_btn = ctk.CTkButton( + bucket_frame, text="+", width=28, height=28, + corner_radius=6, font=ctk.CTkFont(size=14, weight="bold"), + command=self._create_bucket, + ) + self._create_bucket_btn.pack(side="left", padx=(0, 3)) + + # Delete bucket [🗑] + self._delete_bucket_btn = ctk.CTkButton( + bucket_frame, text="\U0001f5d1", width=28, height=28, + corner_radius=6, fg_color="#dc2626", hover_color="#b91c1c", + font=ctk.CTkFont(size=13), + command=self._delete_bucket, + ) + self._delete_bucket_btn.pack(side="left", padx=(0, 15)) # Path display self._path_label = ctk.CTkLabel( @@ -626,6 +643,64 @@ class S3Tab(ctk.CTkFrame): threading.Thread(target=_do, daemon=True).start() + def _create_bucket(self): + """Prompt for bucket name and create it.""" + if not self._client: + return + dialog = ctk.CTkInputDialog( + text=t("s3_bucket_name_prompt"), + title=t("s3_create_bucket"), + ) + name = dialog.get_input() + if not name or not name.strip(): + return + name = name.strip() + self._status_label.configure(text="...") + + def _do(): + ok = self._client.create_bucket(name) + self.after(0, lambda: self._on_bucket_created(ok, name)) + + threading.Thread(target=_do, daemon=True).start() + + def _on_bucket_created(self, ok: bool, name: str): + if ok: + self._status_label.configure( + text=t("s3_bucket_created").format(name=name)) + self._current_bucket = name + self._load_buckets() + else: + self._status_label.configure(text=t("s3_folder_failed")) + + def _delete_bucket(self): + """Delete the currently selected bucket (must be empty).""" + if not self._client or not self._current_bucket: + return + from tkinter import messagebox + ok = messagebox.askyesno( + t("s3_delete_bucket"), + t("s3_delete_bucket_confirm").format(name=self._current_bucket), + ) + if not ok: + return + bucket_name = self._current_bucket + self._status_label.configure(text="...") + + def _do(): + ok = self._client.delete_bucket(bucket_name) + self.after(0, lambda: self._on_bucket_deleted(ok, bucket_name)) + + threading.Thread(target=_do, daemon=True).start() + + def _on_bucket_deleted(self, ok: bool, name: str): + if ok: + self._status_label.configure( + text=t("s3_bucket_deleted").format(name=name)) + self._current_bucket = "" + self._load_buckets() + else: + self._status_label.configure(text=t("s3_delete_failed")) + def _go_back(self): if self._nav_stack: self._current_prefix = self._nav_stack.pop() diff --git a/releases/ServerManager-v1.9.18-win-x64.exe b/releases/ServerManager-v1.9.23-win-x64.exe similarity index 98% rename from releases/ServerManager-v1.9.18-win-x64.exe rename to releases/ServerManager-v1.9.23-win-x64.exe index b7ece76..8a5c5c6 100644 Binary files a/releases/ServerManager-v1.9.18-win-x64.exe and b/releases/ServerManager-v1.9.23-win-x64.exe differ diff --git a/version.py b/version.py index e04b2fe..94dfdf1 100755 --- a/version.py +++ b/version.py @@ -1,6 +1,6 @@ """Version info for ServerManager.""" -__version__ = "1.9.22" +__version__ = "1.9.23" __app_name__ = "ServerManager" __author__ = "aibot777" __description__ = "Desktop GUI for managing remote servers"