Rclone 单 WebDav 图形化挂载工具
功能简单, 托盘图标, 自定义图标(同目录app.ico)
本地磁盘和网络磁盘位置可选
自定义显示容量大小(只是显示大小,和实际容量无关)
需要安装winfsp,同目录需要rclone.exe。
欢迎修改打包分享!
- import os
- import json
- import ctypes
- import subprocess
- import time
- from PyQt5.QtWidgets import (
- QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
- QListWidget, QListWidgetItem, QMessageBox, QSystemTrayIcon,
- QMenu, QAction, QStyle, QDialog, QFormLayout, QLineEdit,
- QComboBox, QSpinBox, QCheckBox, QLabel, QDialogButtonBox,
- )
- from PyQt5.QtCore import Qt, QTimer
- from PyQt5.QtGui import QFont, QIcon
- DEBUG = True
- def debug_print(*args, **kwargs):
- if DEBUG:
- print(“[DEBUG]”, *args, **kwargs)
- RCLONE_EXE = “rclone.exe”
- CONFIG_FILE = “mounts.json”
- # ———- 工具函数 ———-
- def get_used_drives():
- drives = set()
- try:
- mask = ctypes.windll.kernel32.GetLogicalDrives()
- for i in range(26):
- if mask & (1 << i):
- drives.add(chr(ord(‘A’) + i))
- except:
- for letter in “ABCDEFGHIJKLMNOPQRSTUVWXYZ”:
- if os.path.exists(f”{letter}:\”):
- drives.add(letter)
- return drives
- def rclone_reveal(obscured: str) -> str:
- cmd = [RCLONE_EXE, “reveal”, obscured]
- try:
- proc = subprocess.run(cmd, capture_output=True, text=True,
- encoding=’utf-8′, errors=’replace’,
- creationflags=subprocess.CREATE_NO_WINDOW)
- if proc.returncode == 0:
- return proc.stdout.strip()
- except:
- pass
- return “”
- def get_remote_password(remote_name: str) -> str:
- ret, out, err = rclone_cmd([“config”, “dump”])
- if ret != 0:
- return “”
- try:
- config = json.loads(out)
- remote_config = config.get(remote_name, {})
- obscured = remote_config.get(“pass”, “”)
- if obscured:
- return rclone_reveal(obscured)
- except:
- pass
- return “”
- def rclone_cmd(args, cwd=None):
- cmd = [RCLONE_EXE] + args
- debug_print(“执行:”, ” “.join(cmd))
- try:
- proc = subprocess.run(
- cmd, capture_output=True, text=True, encoding=’utf-8′,
- errors=’replace’, cwd=cwd,
- creationflags=subprocess.CREATE_NO_WINDOW if os.name == ‘nt’ else 0
- )
- stdout = proc.stdout.strip() if proc.stdout else “”
- stderr = proc.stderr.strip() if proc.stderr else “”
- debug_print(“返回码:”, proc.returncode)
- if stdout:
- debug_print(“stdout:”, stdout[:200])
- if stderr:
- debug_print(“stderr:”, stderr[:200])
- return proc.returncode, stdout, stderr
- except Exception as e:
- debug_print(“命令执行异常:”, e)
- return -1, “”, str(e)
- # ———- 编辑对话框 ———-
- class MountEditDialog(QDialog):
- def __init__(self, parent=None, existing=None, existing_password=None):
- super().__init__(parent)
- self.existing = existing
- self.setWindowTitle(“编辑挂载配置” if existing else “添加挂载配置”)
- self.setFixedSize(420, 350)
- self.setStyleSheet(parent.styleSheet())
- layout = QFormLayout(self)
- layout.setSpacing(12)
- self.name_edit = QLineEdit()
- self.name_edit.setPlaceholderText(“显示名称(也是 remote 名)”)
- layout.addRow(“配置名称:”, self.name_edit)
- self.url_edit = QLineEdit()
- self.url_edit.setPlaceholderText(“http://192.168.1.5:5244/alist/dav”)
- layout.addRow(“DAV 地址:”, self.url_edit)
- self.user_edit = QLineEdit()
- self.user_edit.setPlaceholderText(“用户名”)
- layout.addRow(“用户名:”, self.user_edit)
- self.pass_edit = QLineEdit()
- self.pass_edit.setEchoMode(QLineEdit.Password)
- self.pass_edit.setPlaceholderText(“密码(留空则不修改)” if existing else “密码”)
- layout.addRow(“密码:”, self.pass_edit)
- drive_layout = QHBoxLayout()
- self.drive_combo = QComboBox()
- self.drive_combo.setFixedWidth(80)
- drive_layout.addWidget(self.drive_combo)
- drive_layout.addStretch()
- layout.addRow(“盘符:”, drive_layout)
- size_layout = QHBoxLayout()
- self.size_spin = QSpinBox()
- self.size_spin.setRange(1, 99999)
- self.size_spin.setValue(20)
- self.size_spin.setFixedWidth(100)
- size_layout.addWidget(self.size_spin)
- self.unit_combo = QComboBox()
- self.unit_combo.addItems([“M”, “G”, “T”, “P”, “E”])
- self.unit_combo.setCurrentText(“G”)
- self.unit_combo.setFixedWidth(80)
- size_layout.addWidget(self.unit_combo)
- size_layout.addStretch()
- layout.addRow(“磁盘容量:”, size_layout)
- self.network_check = QCheckBox(“网络位置”)
- self.network_check.setChecked(False)
- layout.addRow(“”, self.network_check)
- self.auto_mount_check = QCheckBox(“启动时自动挂载”)
- self.auto_mount_check.setChecked(False)
- layout.addRow(“”, self.auto_mount_check)
- # 自定义确认/取消按钮
- btn_confirm = QPushButton(“确认”)
- btn_confirm.setFixedSize(80, 30)
- btn_confirm.setStyleSheet(“””
- QPushButton {
- background-color: #3498DB; color: white; border-radius: 4px; font-weight: bold;
- }
- QPushButton:hover {
- background-color: #2980B9;
- }
- “””)
- btn_confirm.clicked.connect(self.accept)
- btn_cancel = QPushButton(“取消”)
- btn_cancel.setFixedSize(80, 30)
- btn_cancel.setStyleSheet(“””
- QPushButton {
- background-color: #95A5A6; color: white; border-radius: 4px; font-weight: bold;
- }
- QPushButton:hover {
- background-color: #7F8C8D;
- }
- “””)
- btn_cancel.clicked.connect(self.reject)
- btn_layout = QHBoxLayout()
- btn_layout.addStretch()
- btn_layout.addWidget(btn_confirm)
- btn_layout.addWidget(btn_cancel)
- layout.addRow(btn_layout)
- if existing:
- self.name_edit.setText(existing.get(“name”, “”))
- self.url_edit.setText(existing.get(“url”, “”))
- self.user_edit.setText(existing.get(“user”, “”))
- self.size_spin.setValue(existing.get(“size_value”, 20))
- old_unit = existing.get(“size_unit”, “G”)
- if old_unit.endswith(“B”): old_unit = old_unit[0]
- self.unit_combo.setCurrentText(old_unit)
- self.network_check.setChecked(existing.get(“network_mode”, True))
- self.auto_mount_check.setChecked(existing.get(“auto_mount”, False))
- if existing_password is not None:
- self.pass_edit.setText(existing_password)
- def populate_drives(self, used_letters, reserved_letters=set()):
- self.drive_combo.clear()
- all_letters = set(chr(i) for i in range(ord(‘D’), ord(‘Z’)+1))
- excluded = used_letters – reserved_letters
- available = sorted(all_letters – excluded)
- self.drive_combo.addItems([f”{d}:” for d in available])
- if self.existing and self.existing.get(“drive”):
- idx = self.drive_combo.findText(self.existing[“drive”])
- if idx >= 0:
- self.drive_combo.setCurrentIndex(idx)
- def get_data(self):
- return {
- “name”: self.name_edit.text().strip(),
- “url”: self.url_edit.text().strip(),
- “user”: self.user_edit.text().strip(),
- “password”: self.pass_edit.text(),
- “drive”: self.drive_combo.currentText(),
- “size_value”: self.size_spin.value(),
- “size_unit”: self.unit_combo.currentText(),
- “network_mode”: self.network_check.isChecked(),
- “auto_mount”: self.auto_mount_check.isChecked()
- }
- # ———- 列表项部件 ———-
- class MountItemWidget(QWidget):
- BTN_OFFSET = “margin-top: -3px;”
- @staticmethod
- def _btn_style(bg_color, extra=””):
- return f”{MountItemWidget.BTN_OFFSET} background-color: {bg_color}; color: white; border-radius:4px; font-weight:bold; {extra}”
- def __init__(self, entry, parent_app):
- super().__init__()
- self.entry = entry
- self.app = parent_app
- self.is_mounted = False
- layout = QHBoxLayout(self)
- layout.setContentsMargins(8, 6, 8, 6)
- layout.setSpacing(6)
- layout.setAlignment(Qt.AlignVCenter)
- drive = entry.get(“drive”, “”) or “?”
- self.drive_label = QLabel(drive)
- self.drive_label.setFixedWidth(30)
- self.drive_label.setStyleSheet(“font-weight: bold; color: #2c3e50;”)
- mode = “网络” if entry.get(“network_mode”, True) else “本地”
- self.mode_label = QLabel(mode)
- self.mode_label.setFixedWidth(40)
- color = “#2980b9” if mode == “网络” else “#27ae60”
- self.mode_label.setStyleSheet(f”color: {color}; font-weight: bold;”)
- self.name_label = QLabel(entry[“name”])
- self.name_label.setFixedWidth(140)
- self.name_label.setStyleSheet(“font-weight: bold; color: #2c3e50;”)
- cap = f”{entry.get(‘size_value’,20)}{entry.get(‘size_unit’,’G’)}”
- self.cap_label = QLabel(cap)
- self.cap_label.setFixedWidth(60)
- self.cap_label.setStyleSheet(“color: #7f8c8d;”)
- self.btn_auto = QPushButton(“自动挂载”)
- self.btn_auto.setFixedSize(64, 30)
- self.btn_auto.setCheckable(True)
- self.btn_auto.setChecked(entry.get(“auto_mount”, False))
- self.btn_auto.clicked.connect(self.on_auto_toggled)
- self.update_auto_style()
- self.btn_action = QPushButton(“启动”)
- self.btn_action.setFixedSize(64, 30)
- self.btn_action.clicked.connect(self.on_action_clicked)
- self.btn_action.setStyleSheet(self._btn_style(“#3498DB”))
- self.btn_edit = QPushButton(“编辑”)
- self.btn_edit.setFixedSize(64, 30)
- self.btn_edit.clicked.connect(self.edit_entry)
- self.btn_edit.setStyleSheet(self._btn_style(“#3498DB”))
- self.btn_delete = QPushButton(“删除”)
- self.btn_delete.setFixedSize(64, 30)
- self.btn_delete.clicked.connect(self.delete_entry)
- self.btn_delete.setStyleSheet(self._btn_style(“#E74C3C”))
- layout.addWidget(self.drive_label)
- layout.addWidget(self.mode_label)
- layout.addWidget(self.name_label)
- layout.addWidget(self.cap_label)
- layout.addWidget(self.btn_auto)
- layout.addWidget(self.btn_action)
- layout.addWidget(self.btn_edit)
- layout.addWidget(self.btn_delete)
- def on_auto_toggled(self):
- checked = self.btn_auto.isChecked()
- self.entry[“auto_mount”] = checked
- self.app.update_entry_metadata(self.entry)
- self.update_auto_style()
- def update_auto_style(self):
- if self.btn_auto.isChecked():
- self.btn_auto.setText(“自动:开”)
- self.btn_auto.setStyleSheet(self._btn_style(“#27AE60”))
- else:
- self.btn_auto.setText(“自动:关”)
- self.btn_auto.setStyleSheet(self._btn_style(“#95A5A6”))
- def on_action_clicked(self):
- if not self.entry.get(“drive”):
- QMessageBox.warning(self, “错误”, “请先设置盘符!”)
- return
- if self.is_mounted:
- self.open_drive()
- else:
- self.app.mount_entry(self.entry)
- def open_drive(self):
- drive = self.entry.get(“drive”)
- if not drive:
- QMessageBox.warning(self, “错误”, “盘符未设置!”)
- return
- for _ in range(30):
- if os.path.exists(drive + “\”):
- os.startfile(drive + “\”)
- return
- time.sleep(0.1)
- QMessageBox.warning(self, “错误”, f”盘符 {drive} 不可用,请等待挂载完成。”)
- def edit_entry(self):
- self.app.edit_entry_dialog(self.entry)
- def delete_entry(self):
- self.app.delete_entry(self.entry)
- # ———- 主窗口 ———-
- class RcloneTrayApp(QWidget):
- def __init__(self):
- super().__init__()
- self.base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
- self.metadata = {}
- self.active_mounts = {}
- self.load_metadata()
- self.init_ui()
- self.init_tray()
- self.sync_from_rclone()
- self.refresh_list()
- QTimer.singleShot(1000, self.auto_mount_selected)
- debug_print(“启动完成,程序目录:”, self.base_dir)
- def get_app_icon(self):
- “””优先使用 app.ico,找不到则使用系统标准图标”””
- # 1. 程序所在目录
- ico_path = os.path.join(self.base_dir, “app.ico”)
- if os.path.exists(ico_path):
- return QIcon(ico_path)
- # 2. PyInstaller 打包后的释放目录
- if getattr(sys, ‘frozen’, False):
- meipass = getattr(sys, ‘_MEIPASS’, None)
- if meipass:
- ico_path = os.path.join(meipass, “app.ico”)
- if os.path.exists(ico_path):
- return QIcon(ico_path)
- # 3. 兜底:Qt 内置图标
- return self.style().standardIcon(QStyle.SP_DriveNetIcon)
- def load_metadata(self):
- try:
- with open(CONFIG_FILE, “r”, encoding=”utf-8″) as f:
- self.metadata = json.load(f)
- except:
- self.metadata = {}
- def save_metadata(self):
- with open(CONFIG_FILE, “w”, encoding=”utf-8″) as f:
- json.dump(self.metadata, f, indent=2, ensure_ascii=False)
- def update_entry_metadata(self, entry):
- self.metadata[entry[“id”]] = {
- k: entry[k] for k in (“name”, “url”, “user”, “drive”,
- “size_value”, “size_unit”, “network_mode”,
- “auto_mount”)
- }
- self.save_metadata()
- def sync_from_rclone(self):
- ret, out, err = rclone_cmd([“listremotes”], cwd=self.base_dir)
- if ret != 0:
- debug_print(“rclone listremotes 失败:”, err)
- return
- remotes = [r.strip(“:”) for r in out.splitlines() if r.strip()]
- debug_print(“发现远程配置:”, remotes)
- for remote in remotes:
- if remote not in self.metadata:
- self.metadata[remote] = {
- “name”: remote, “url”: “”, “user”: “”,
- “drive”: “”, “size_value”: 20, “size_unit”: “G”,
- “network_mode”: True, “auto_mount”: False
- }
- for remote in list(self.metadata.keys()):
- if remote not in remotes:
- del self.metadata[remote]
- self.save_metadata()
- def get_all_entries(self):
- entries = []
- for rid, meta in self.metadata.items():
- entry = {“id”: rid}
- entry.update(meta)
- entries.append(entry)
- return entries
- def init_ui(self):
- self.setWindowTitle(“Rclone WebDav 挂载管理”)
- self.setFixedSize(620, 480)
- self.setWindowIcon(self.get_app_icon())
- # 全局样式
- self.setStyleSheet(“””
- QWidget {
- background: #FFFFFF;
- font-family: “Microsoft YaHei”, “微软雅黑”;
- font-size: 13px;
- color: #2c3e50;
- }
- QLineEdit, QComboBox, QSpinBox {
- background: #FFFFFF;
- border: 1px solid #BDC3C7;
- border-radius: 4px;
- padding: 5px 8px;
- color: #2c3e50;
- selection-background-color: #3498DB;
- }
- QLineEdit:focus, QComboBox:focus, QSpinBox:focus {
- border-color: #3498DB;
- }
- QComboBox::drop-down {
- width: 0px;
- border: none;
- }
- QSpinBox::up-button, QSpinBox::down-button {
- width: 0px;
- border: none;
- }
- QCheckBox {
- spacing: 6px;
- color: #2c3e50;
- }
- QListWidget {
- background: transparent;
- border: none;
- outline: none;
- }
- QListWidget::item {
- background: #FFFFFF;
- border: none;
- border-bottom: 1px solid #E0E4E8;
- margin: 0px;
- padding: 0px;
- }
- QListWidget::item:selected {
- background: #EBF5FB;
- }
- “””)
- main_layout = QVBoxLayout(self)
- main_layout.setContentsMargins(10, 10, 10, 10)
- main_layout.setSpacing(6)
- top_layout = QHBoxLayout()
- top_layout.setSpacing(10)
- self.btn_add = QPushButton(“+ 添加配置”)
- self.btn_add.setFixedSize(120, 30)
- self.btn_add.setStyleSheet(
- “QPushButton { background-color: #3498DB; color: white; border-radius: 4px; font-weight: bold; }”
- “QPushButton:hover { background-color: #2980B9; }”
- )
- self.btn_add.clicked.connect(self.add_entry_dialog)
- self.btn_disconnect = QPushButton(“断开挂载”)
- self.btn_disconnect.setFixedSize(120, 30)
- self.btn_disconnect.setStyleSheet(
- “QPushButton { background-color: #E74C3C; color: white; border-radius: 4px; font-weight: bold; }”
- “QPushButton:hover { background-color: #C0392B; }”
- )
- self.btn_disconnect.clicked.connect(self.disconnect_all_mounts)
- top_layout.addWidget(self.btn_add)
- top_layout.addWidget(self.btn_disconnect)
- top_layout.addStretch()
- main_layout.addLayout(top_layout)
- self.list_widget = QListWidget()
- self.list_widget.setSpacing(0)
- self.list_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
- main_layout.addWidget(self.list_widget)
- def init_tray(self):
- tray_icon = self.get_app_icon()
- self.tray_icon = QSystemTrayIcon(self)
- self.tray_icon.setIcon(tray_icon)
- tray_menu = QMenu()
- show_act = QAction(“显示窗口”, self, triggered=self.show_window)
- exit_act = QAction(“退出”, self, triggered=self.quit_app)
- tray_menu.addAction(show_act)
- tray_menu.addAction(exit_act)
- self.tray_icon.setContextMenu(tray_menu)
- self.tray_icon.activated.connect(self.on_tray_activated)
- self.tray_icon.show()
- def show_window(self):
- self.showNormal()
- self.activateWindow()
- self.raise_()
- def close_to_tray(self):
- self.hide()
- def quit_app(self):
- subprocess.run(“taskkill /IM rclone.exe /F”, shell=True,
- capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
- self.active_mounts.clear()
- self.tray_icon.hide()
- QApplication.quit()
- def on_tray_activated(self, reason):
- if reason == QSystemTrayIcon.DoubleClick:
- self.show_window()
- def closeEvent(self, event):
- event.ignore()
- self.hide()
- def disconnect_all_mounts(self):
- subprocess.run(“taskkill /IM rclone.exe /F”, shell=True,
- capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
- self.active_mounts.clear()
- self.refresh_list()
- QMessageBox.information(self, “提示”, “所有挂载已断开”)
- def refresh_list(self):
- self.list_widget.clear()
- entries = self.get_all_entries()
- for entry in entries:
- item = QListWidgetItem()
- widget = MountItemWidget(entry, self)
- item.setSizeHint(widget.sizeHint())
- self.list_widget.addItem(item)
- self.list_widget.setItemWidget(item, widget)
- auto_on = entry.get(“auto_mount”, False)
- widget.btn_auto.setChecked(auto_on)
- widget.update_auto_style()
- is_mounted = (entry[“drive”] in self.active_mounts)
- widget.is_mounted = is_mounted
- if is_mounted:
- widget.btn_action.setText(“打开”)
- widget.btn_action.setStyleSheet(MountItemWidget._btn_style(“#27AE60”))
- else:
- widget.btn_action.setText(“启动”)
- widget.btn_action.setStyleSheet(MountItemWidget._btn_style(“#3498DB”))
- def get_used_drive_letters(self):
- used = get_used_drives()
- for entry in self.get_all_entries():
- if entry.get(“drive”):
- used.add(entry[“drive”][0])
- return used
- def auto_mount_selected(self):
- auto_entries = [e for e in self.get_all_entries() if e.get(“auto_mount”) and e.get(“drive”)]
- debug_print(f”自动挂载条目数: {len(auto_entries)}”)
- for entry in auto_entries:
- debug_print(f”自动挂载: {entry[‘name’]} -> {entry[‘drive’]}”)
- self.mount_entry(entry)
- # ———- 增删改 ———-
- def add_entry_dialog(self):
- used = self.get_used_drive_letters()
- dlg = MountEditDialog(self)
- dlg.populate_drives(used)
- if dlg.exec_() != QDialog.Accepted:
- return
- data = dlg.get_data()
- if not data[“name”] or not data[“url”] or not data[“user”] or not data[“password”]:
- QMessageBox.warning(self, “警告”, “必填项不能为空”)
- return
- remote_name = data[“name”]
- if remote_name in self.metadata:
- QMessageBox.warning(self, “警告”, “配置名称已存在”)
- return
- ret, out, err = rclone_cmd([
- “config”, “create”, remote_name, “webdav”,
- “vendor=other”, f”url={data[‘url’]}”, f”user={data[‘user’]}”,
- f”pass={data[‘password’]}”, “–obscure”
- ], cwd=self.base_dir)
- if ret != 0:
- QMessageBox.warning(self, “创建失败”, err)
- return
- self.metadata[remote_name] = {
- “name”: data[“name”], “url”: data[“url”], “user”: data[“user”],
- “drive”: data[“drive”], “size_value”: data[“size_value”],
- “size_unit”: data[“size_unit”], “network_mode”: data[“network_mode”],
- “auto_mount”: data[“auto_mount”]
- }
- self.save_metadata()
- self.refresh_list()
- def edit_entry_dialog(self, entry):
- used = self.get_used_drive_letters()
- reserved = set(entry[“drive”][0]) if entry[“drive”] else set()
- existing_password = get_remote_password(entry[“id”])
- dlg = MountEditDialog(self, existing=entry, existing_password=existing_password)
- dlg.populate_drives(used, reserved)
- if dlg.exec_() != QDialog.Accepted:
- return
- data = dlg.get_data()
- if not data[“name”] or not data[“url”] or not data[“user”]:
- QMessageBox.warning(self, “警告”, “名称、地址、用户名不能为空”)
- return
- old_remote = entry[“id”]
- new_name = data[“name”]
- name_changed = (new_name != old_remote)
- new_pass = data[“password”]
- need_recreate = name_changed or bool(new_pass)
- if need_recreate:
- if name_changed and new_name in self.metadata:
- QMessageBox.warning(self, “警告”, “新配置名称已存在”)
- return
- actual_pass = new_pass if new_pass else existing_password
- if not actual_pass:
- QMessageBox.warning(self, “错误”, “无法获取原始密码,请重新输入密码”)
- return
- if name_changed:
- rclone_cmd([“config”, “delete”, old_remote], cwd=self.base_dir)
- ret, _, err = rclone_cmd([
- “config”, “create”, new_name, “webdav”,
- “vendor=other”, f”url={data[‘url’]}”, f”user={data[‘user’]}”,
- f”pass={actual_pass}”, “–obscure”
- ], cwd=self.base_dir)
- if ret != 0:
- QMessageBox.warning(self, “更新失败”, err)
- return
- if name_changed:
- del self.metadata[old_remote]
- self.metadata[new_name] = {
- “name”: data[“name”], “url”: data[“url”], “user”: data[“user”],
- “drive”: data[“drive”], “size_value”: data[“size_value”],
- “size_unit”: data[“size_unit”], “network_mode”: data[“network_mode”],
- “auto_mount”: data[“auto_mount”]
- }
- self.save_metadata()
- if entry[“drive”] in self.active_mounts:
- self.unmount_entry(entry)
- self.refresh_list()
- def delete_entry(self, entry):
- if QMessageBox.question(self, “确认”, f”删除 ‘{entry[‘name’]}’?”) != QMessageBox.Yes:
- return
- if entry[“drive”] in self.active_mounts:
- self.unmount_entry(entry)
- rclone_cmd([“config”, “delete”, entry[“id”]], cwd=self.base_dir)
- del self.metadata[entry[“id”]]
- self.save_metadata()
- self.refresh_list()
- # ———- 挂载/卸载 ———-
- def mount_entry(self, entry):
- drive = entry.get(“drive”)
- if not drive:
- QMessageBox.warning(self, “挂载失败”, “未设置盘符”)
- return
- if drive in self.active_mounts:
- self.unmount_entry(entry)
- cache_subdir = os.path.join(self.base_dir, “Temp”, entry[“name”])
- os.makedirs(cache_subdir, exist_ok=True)
- total_size = f”{entry.get(‘size_value’,20)}{entry.get(‘size_unit’,’G’)}”
- args = [
- “mount”, f”{entry[‘id’]}:”, drive,
- “–cache-dir”, f”./Temp/{entry[‘name’]}”,
- “–vfs-cache-mode”, “full”,
- “–allow-non-empty”,
- “–dir-cache-time”, “5s”,
- “–allow-other”,
- “–vfs-disk-space-total-size”, total_size
- ]
- if entry.get(“network_mode”, True):
- args.append(“–network-mode”)
- debug_print(“挂载命令:”, f’rclone {” “.join(args)}’)
- try:
- proc = subprocess.Popen(
- [RCLONE_EXE] + args,
- cwd=self.base_dir,
- creationflags=subprocess.CREATE_NO_WINDOW
- )
- self.active_mounts[drive] = proc
- self.refresh_list()
- debug_print(f”挂载成功: {drive}”)
- except Exception as e:
- QMessageBox.warning(self, “挂载失败”, str(e))
- def unmount_entry(self, entry):
- drive = entry[“drive”]
- if drive not in self.active_mounts:
- return
- proc = self.active_mounts.pop(drive)
- rclone_cmd([“unmount”, drive], cwd=self.base_dir)
- if proc.poll() is None:
- try:
- proc.terminate()
- proc.wait(timeout=5)
- except:
- proc.kill()
- self.refresh_list()
- if __name__ == “__main__”:
- app = QApplication(sys.argv)
- app.setQuitOnLastWindowClosed(False)
- app.setFont(QFont(“Microsoft YaHei”, 9))
- window = RcloneTrayApp()
- window.show()
- sys.exit(app.exec_())

