Rclone  单 WebDav  图形化挂载工具

功能简单,  托盘图标,  自定义图标(同目录app.ico)
本地磁盘和网络磁盘位置可选
自定义显示容量大小(只是显示大小,和实际容量无关)
需要安装winfsp,同目录需要rclone.exe。
欢迎修改打包分享!

自用 python写的 Rclone WebDav 挂载工具 挂载本地磁盘 自用 python写的 Rclone WebDav 挂载工具 挂载本地磁盘
import sys

  1. import os
  2. import json
  3. import ctypes
  4. import subprocess
  5. import time
  6. from PyQt5.QtWidgets import (
  7.     QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
  8.     QListWidget, QListWidgetItem, QMessageBox, QSystemTrayIcon,
  9.     QMenu, QAction, QStyle, QDialog, QFormLayout, QLineEdit,
  10.     QComboBox, QSpinBox, QCheckBox, QLabel, QDialogButtonBox,
  11. )
  12. from PyQt5.QtCore import Qt, QTimer
  13. from PyQt5.QtGui import QFont, QIcon
  14. DEBUG = True
  15. def debug_print(*args, **kwargs):
  16.     if DEBUG:
  17.         print(“[DEBUG]”, *args, **kwargs)
  18. RCLONE_EXE = “rclone.exe”
  19. CONFIG_FILE = “mounts.json”
  20. # ———- 工具函数 ———-
  21. def get_used_drives():
  22.     drives = set()
  23.     try:
  24.         mask = ctypes.windll.kernel32.GetLogicalDrives()
  25.         for i in range(26):
  26.             if mask & (1 << i):
  27.                 drives.add(chr(ord(‘A’) + i))
  28.     except:
  29.         for letter in “ABCDEFGHIJKLMNOPQRSTUVWXYZ”:
  30.             if os.path.exists(f”{letter}:\”):
  31.                 drives.add(letter)
  32.     return drives
  33. def rclone_reveal(obscured: str) -> str:
  34.     cmd = [RCLONE_EXE, “reveal”, obscured]
  35.     try:
  36.         proc = subprocess.run(cmd, capture_output=True, text=True,
  37.                               encoding=’utf-8′, errors=’replace’,
  38.                               creationflags=subprocess.CREATE_NO_WINDOW)
  39.         if proc.returncode == 0:
  40.             return proc.stdout.strip()
  41.     except:
  42.         pass
  43.     return “”
  44. def get_remote_password(remote_name: str) -> str:
  45.     ret, out, err = rclone_cmd([“config”, “dump”])
  46.     if ret != 0:
  47.         return “”
  48.     try:
  49.         config = json.loads(out)
  50.         remote_config = config.get(remote_name, {})
  51.         obscured = remote_config.get(“pass”, “”)
  52.         if obscured:
  53.             return rclone_reveal(obscured)
  54.     except:
  55.         pass
  56.     return “”
  57. def rclone_cmd(args, cwd=None):
  58.     cmd = [RCLONE_EXE] + args
  59.     debug_print(“执行:”, ” “.join(cmd))
  60.     try:
  61.         proc = subprocess.run(
  62.             cmd, capture_output=True, text=True, encoding=’utf-8′,
  63.             errors=’replace’, cwd=cwd,
  64.             creationflags=subprocess.CREATE_NO_WINDOW if os.name == ‘nt’ else 0
  65.         )
  66.         stdout = proc.stdout.strip() if proc.stdout else “”
  67.         stderr = proc.stderr.strip() if proc.stderr else “”
  68.         debug_print(“返回码:”, proc.returncode)
  69.         if stdout:
  70.             debug_print(“stdout:”, stdout[:200])
  71.         if stderr:
  72.             debug_print(“stderr:”, stderr[:200])
  73.         return proc.returncode, stdout, stderr
  74.     except Exception as e:
  75.         debug_print(“命令执行异常:”, e)
  76.         return -1, “”, str(e)
  77. # ———- 编辑对话框 ———-
  78. class MountEditDialog(QDialog):
  79.     def __init__(self, parent=None, existing=None, existing_password=None):
  80.         super().__init__(parent)
  81.         self.existing = existing
  82.         self.setWindowTitle(“编辑挂载配置” if existing else “添加挂载配置”)
  83.         self.setFixedSize(420, 350)
  84.         self.setStyleSheet(parent.styleSheet())
  85.         layout = QFormLayout(self)
  86.         layout.setSpacing(12)
  87.         self.name_edit = QLineEdit()
  88.         self.name_edit.setPlaceholderText(“显示名称(也是 remote 名)”)
  89.         layout.addRow(“配置名称:”, self.name_edit)
  90.         self.url_edit = QLineEdit()
  91.         self.url_edit.setPlaceholderText(“http://192.168.1.5:5244/alist/dav”)
  92.         layout.addRow(“DAV 地址:”, self.url_edit)
  93.         self.user_edit = QLineEdit()
  94.         self.user_edit.setPlaceholderText(“用户名”)
  95.         layout.addRow(“用户名:”, self.user_edit)
  96.         self.pass_edit = QLineEdit()
  97.         self.pass_edit.setEchoMode(QLineEdit.Password)
  98.         self.pass_edit.setPlaceholderText(“密码(留空则不修改)” if existing else “密码”)
  99.         layout.addRow(“密码:”, self.pass_edit)
  100.         drive_layout = QHBoxLayout()
  101.         self.drive_combo = QComboBox()
  102.         self.drive_combo.setFixedWidth(80)
  103.         drive_layout.addWidget(self.drive_combo)
  104.         drive_layout.addStretch()
  105.         layout.addRow(“盘符:”, drive_layout)
  106.         size_layout = QHBoxLayout()
  107.         self.size_spin = QSpinBox()
  108.         self.size_spin.setRange(1, 99999)
  109.         self.size_spin.setValue(20)
  110.         self.size_spin.setFixedWidth(100)
  111.         size_layout.addWidget(self.size_spin)
  112.         self.unit_combo = QComboBox()
  113.         self.unit_combo.addItems([“M”, “G”, “T”, “P”, “E”])
  114.         self.unit_combo.setCurrentText(“G”)
  115.         self.unit_combo.setFixedWidth(80)
  116.         size_layout.addWidget(self.unit_combo)
  117.         size_layout.addStretch()
  118.         layout.addRow(“磁盘容量:”, size_layout)
  119.         self.network_check = QCheckBox(“网络位置”)
  120.         self.network_check.setChecked(False)
  121.         layout.addRow(“”, self.network_check)
  122.         self.auto_mount_check = QCheckBox(“启动时自动挂载”)
  123.         self.auto_mount_check.setChecked(False)
  124.         layout.addRow(“”, self.auto_mount_check)
  125.         # 自定义确认/取消按钮
  126.         btn_confirm = QPushButton(“确认”)
  127.         btn_confirm.setFixedSize(80, 30)
  128.         btn_confirm.setStyleSheet(“””
  129.             QPushButton {
  130.                 background-color: #3498DB; color: white; border-radius: 4px; font-weight: bold;
  131.             }
  132.             QPushButton:hover {
  133.                 background-color: #2980B9;
  134.             }
  135.         “””)
  136.         btn_confirm.clicked.connect(self.accept)
  137.         btn_cancel = QPushButton(“取消”)
  138.         btn_cancel.setFixedSize(80, 30)
  139.         btn_cancel.setStyleSheet(“””
  140.             QPushButton {
  141.                 background-color: #95A5A6; color: white; border-radius: 4px; font-weight: bold;
  142.             }
  143.             QPushButton:hover {
  144.                 background-color: #7F8C8D;
  145.             }
  146.         “””)
  147.         btn_cancel.clicked.connect(self.reject)
  148.         btn_layout = QHBoxLayout()
  149.         btn_layout.addStretch()
  150.         btn_layout.addWidget(btn_confirm)
  151.         btn_layout.addWidget(btn_cancel)
  152.         layout.addRow(btn_layout)
  153.         if existing:
  154.             self.name_edit.setText(existing.get(“name”, “”))
  155.             self.url_edit.setText(existing.get(“url”, “”))
  156.             self.user_edit.setText(existing.get(“user”, “”))
  157.             self.size_spin.setValue(existing.get(“size_value”, 20))
  158.             old_unit = existing.get(“size_unit”, “G”)
  159.             if old_unit.endswith(“B”): old_unit = old_unit[0]
  160.             self.unit_combo.setCurrentText(old_unit)
  161.             self.network_check.setChecked(existing.get(“network_mode”, True))
  162.             self.auto_mount_check.setChecked(existing.get(“auto_mount”, False))
  163.             if existing_password is not None:
  164.                 self.pass_edit.setText(existing_password)
  165.     def populate_drives(self, used_letters, reserved_letters=set()):
  166.         self.drive_combo.clear()
  167.         all_letters = set(chr(i) for i in range(ord(‘D’), ord(‘Z’)+1))
  168.         excluded = used_letters – reserved_letters
  169.         available = sorted(all_letters – excluded)
  170.         self.drive_combo.addItems([f”{d}:” for d in available])
  171.         if self.existing and self.existing.get(“drive”):
  172.             idx = self.drive_combo.findText(self.existing[“drive”])
  173.             if idx >= 0:
  174.                 self.drive_combo.setCurrentIndex(idx)
  175.     def get_data(self):
  176.         return {
  177.             “name”: self.name_edit.text().strip(),
  178.             “url”: self.url_edit.text().strip(),
  179.             “user”: self.user_edit.text().strip(),
  180.             “password”: self.pass_edit.text(),
  181.             “drive”: self.drive_combo.currentText(),
  182.             “size_value”: self.size_spin.value(),
  183.             “size_unit”: self.unit_combo.currentText(),
  184.             “network_mode”: self.network_check.isChecked(),
  185.             “auto_mount”: self.auto_mount_check.isChecked()
  186.         }
  187. # ———- 列表项部件 ———-
  188. class MountItemWidget(QWidget):
  189.     BTN_OFFSET = “margin-top: -3px;”
  190.     @staticmethod
  191.     def _btn_style(bg_color, extra=””):
  192.         return f”{MountItemWidget.BTN_OFFSET} background-color: {bg_color}; color: white; border-radius:4px; font-weight:bold; {extra}”
  193.     def __init__(self, entry, parent_app):
  194.         super().__init__()
  195.         self.entry = entry
  196.         self.app = parent_app
  197.         self.is_mounted = False
  198.         layout = QHBoxLayout(self)
  199.         layout.setContentsMargins(8, 6, 8, 6)
  200.         layout.setSpacing(6)
  201.         layout.setAlignment(Qt.AlignVCenter)
  202.         drive = entry.get(“drive”, “”) or “?”
  203.         self.drive_label = QLabel(drive)
  204.         self.drive_label.setFixedWidth(30)
  205.         self.drive_label.setStyleSheet(“font-weight: bold; color: #2c3e50;”)
  206.         mode = “网络” if entry.get(“network_mode”, True) else “本地”
  207.         self.mode_label = QLabel(mode)
  208.         self.mode_label.setFixedWidth(40)
  209.         color = “#2980b9” if mode == “网络” else “#27ae60”
  210.         self.mode_label.setStyleSheet(f”color: {color}; font-weight: bold;”)
  211.         self.name_label = QLabel(entry[“name”])
  212.         self.name_label.setFixedWidth(140)
  213.         self.name_label.setStyleSheet(“font-weight: bold; color: #2c3e50;”)
  214.         cap = f”{entry.get(‘size_value’,20)}{entry.get(‘size_unit’,’G’)}”
  215.         self.cap_label = QLabel(cap)
  216.         self.cap_label.setFixedWidth(60)
  217.         self.cap_label.setStyleSheet(“color: #7f8c8d;”)
  218.         self.btn_auto = QPushButton(“自动挂载”)
  219.         self.btn_auto.setFixedSize(64, 30)
  220.         self.btn_auto.setCheckable(True)
  221.         self.btn_auto.setChecked(entry.get(“auto_mount”, False))
  222.         self.btn_auto.clicked.connect(self.on_auto_toggled)
  223.         self.update_auto_style()
  224.         self.btn_action = QPushButton(“启动”)
  225.         self.btn_action.setFixedSize(64, 30)
  226.         self.btn_action.clicked.connect(self.on_action_clicked)
  227.         self.btn_action.setStyleSheet(self._btn_style(“#3498DB”))
  228.         self.btn_edit = QPushButton(“编辑”)
  229.         self.btn_edit.setFixedSize(64, 30)
  230.         self.btn_edit.clicked.connect(self.edit_entry)
  231.         self.btn_edit.setStyleSheet(self._btn_style(“#3498DB”))
  232.         self.btn_delete = QPushButton(“删除”)
  233.         self.btn_delete.setFixedSize(64, 30)
  234.         self.btn_delete.clicked.connect(self.delete_entry)
  235.         self.btn_delete.setStyleSheet(self._btn_style(“#E74C3C”))
  236.         layout.addWidget(self.drive_label)
  237.         layout.addWidget(self.mode_label)
  238.         layout.addWidget(self.name_label)
  239.         layout.addWidget(self.cap_label)
  240.         layout.addWidget(self.btn_auto)
  241.         layout.addWidget(self.btn_action)
  242.         layout.addWidget(self.btn_edit)
  243.         layout.addWidget(self.btn_delete)
  244.     def on_auto_toggled(self):
  245.         checked = self.btn_auto.isChecked()
  246.         self.entry[“auto_mount”] = checked
  247.         self.app.update_entry_metadata(self.entry)
  248.         self.update_auto_style()
  249.     def update_auto_style(self):
  250.         if self.btn_auto.isChecked():
  251.             self.btn_auto.setText(“自动:开”)
  252.             self.btn_auto.setStyleSheet(self._btn_style(“#27AE60”))
  253.         else:
  254.             self.btn_auto.setText(“自动:关”)
  255.             self.btn_auto.setStyleSheet(self._btn_style(“#95A5A6”))
  256.     def on_action_clicked(self):
  257.         if not self.entry.get(“drive”):
  258.             QMessageBox.warning(self, “错误”, “请先设置盘符!”)
  259.             return
  260.         if self.is_mounted:
  261.             self.open_drive()
  262.         else:
  263.             self.app.mount_entry(self.entry)
  264.     def open_drive(self):
  265.         drive = self.entry.get(“drive”)
  266.         if not drive:
  267.             QMessageBox.warning(self, “错误”, “盘符未设置!”)
  268.             return
  269.         for _ in range(30):
  270.             if os.path.exists(drive + “\”):
  271.                 os.startfile(drive + “\”)
  272.                 return
  273.             time.sleep(0.1)
  274.         QMessageBox.warning(self, “错误”, f”盘符 {drive} 不可用,请等待挂载完成。”)
  275.     def edit_entry(self):
  276.         self.app.edit_entry_dialog(self.entry)
  277.     def delete_entry(self):
  278.         self.app.delete_entry(self.entry)
  279. # ———- 主窗口 ———-
  280. class RcloneTrayApp(QWidget):
  281.     def __init__(self):
  282.         super().__init__()
  283.         self.base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
  284.         self.metadata = {}
  285.         self.active_mounts = {}
  286.         self.load_metadata()
  287.         self.init_ui()
  288.         self.init_tray()
  289.         self.sync_from_rclone()
  290.         self.refresh_list()
  291.         QTimer.singleShot(1000, self.auto_mount_selected)
  292.         debug_print(“启动完成,程序目录:”, self.base_dir)
  293.     def get_app_icon(self):
  294.         “””优先使用 app.ico,找不到则使用系统标准图标”””
  295.         # 1. 程序所在目录
  296.         ico_path = os.path.join(self.base_dir, “app.ico”)
  297.         if os.path.exists(ico_path):
  298.             return QIcon(ico_path)
  299.         # 2. PyInstaller 打包后的释放目录
  300.         if getattr(sys, ‘frozen’, False):
  301.             meipass = getattr(sys, ‘_MEIPASS’, None)
  302.             if meipass:
  303.                 ico_path = os.path.join(meipass, “app.ico”)
  304.                 if os.path.exists(ico_path):
  305.                     return QIcon(ico_path)
  306.         # 3. 兜底:Qt 内置图标
  307.         return self.style().standardIcon(QStyle.SP_DriveNetIcon)
  308.     def load_metadata(self):
  309.         try:
  310.             with open(CONFIG_FILE, “r”, encoding=”utf-8″) as f:
  311.                 self.metadata = json.load(f)
  312.         except:
  313.             self.metadata = {}
  314.     def save_metadata(self):
  315.         with open(CONFIG_FILE, “w”, encoding=”utf-8″) as f:
  316.             json.dump(self.metadata, f, indent=2, ensure_ascii=False)
  317.     def update_entry_metadata(self, entry):
  318.         self.metadata[entry[“id”]] = {
  319.             k: entry[k] for k in (“name”, “url”, “user”, “drive”,
  320.                                  “size_value”, “size_unit”, “network_mode”,
  321.                                  “auto_mount”)
  322.         }
  323.         self.save_metadata()
  324.     def sync_from_rclone(self):
  325.         ret, out, err = rclone_cmd([“listremotes”], cwd=self.base_dir)
  326.         if ret != 0:
  327.             debug_print(“rclone listremotes 失败:”, err)
  328.             return
  329.         remotes = [r.strip(“:”) for r in out.splitlines() if r.strip()]
  330.         debug_print(“发现远程配置:”, remotes)
  331.         for remote in remotes:
  332.             if remote not in self.metadata:
  333.                 self.metadata[remote] = {
  334.                     “name”: remote, “url”: “”, “user”: “”,
  335.                     “drive”: “”, “size_value”: 20, “size_unit”: “G”,
  336.                     “network_mode”: True, “auto_mount”: False
  337.                 }
  338.         for remote in list(self.metadata.keys()):
  339.             if remote not in remotes:
  340.                 del self.metadata[remote]
  341.         self.save_metadata()
  342.     def get_all_entries(self):
  343.         entries = []
  344.         for rid, meta in self.metadata.items():
  345.             entry = {“id”: rid}
  346.             entry.update(meta)
  347.             entries.append(entry)
  348.         return entries
  349.     def init_ui(self):
  350.         self.setWindowTitle(“Rclone WebDav 挂载管理”)
  351.         self.setFixedSize(620, 480)
  352.         self.setWindowIcon(self.get_app_icon())
  353.         # 全局样式
  354.         self.setStyleSheet(“””
  355.             QWidget {
  356.                 background: #FFFFFF;
  357.                 font-family: “Microsoft YaHei”, “微软雅黑”;
  358.                 font-size: 13px;
  359.                 color: #2c3e50;
  360.             }
  361.             QLineEdit, QComboBox, QSpinBox {
  362.                 background: #FFFFFF;
  363.                 border: 1px solid #BDC3C7;
  364.                 border-radius: 4px;
  365.                 padding: 5px 8px;
  366.                 color: #2c3e50;
  367.                 selection-background-color: #3498DB;
  368.             }
  369.             QLineEdit:focus, QComboBox:focus, QSpinBox:focus {
  370.                 border-color: #3498DB;
  371.             }
  372.             QComboBox::drop-down {
  373.                 width: 0px;
  374.                 border: none;
  375.             }
  376.             QSpinBox::up-button, QSpinBox::down-button {
  377.                 width: 0px;
  378.                 border: none;
  379.             }
  380.             QCheckBox {
  381.                 spacing: 6px;
  382.                 color: #2c3e50;
  383.             }
  384.             QListWidget {
  385.                 background: transparent;
  386.                 border: none;
  387.                 outline: none;
  388.             }
  389.             QListWidget::item {
  390.                 background: #FFFFFF;
  391.                 border: none;
  392.                 border-bottom: 1px solid #E0E4E8;
  393.                 margin: 0px;
  394.                 padding: 0px;
  395.             }
  396.             QListWidget::item:selected {
  397.                 background: #EBF5FB;
  398.             }
  399.         “””)
  400.         main_layout = QVBoxLayout(self)
  401.         main_layout.setContentsMargins(10, 10, 10, 10)
  402.         main_layout.setSpacing(6)
  403.         top_layout = QHBoxLayout()
  404.         top_layout.setSpacing(10)
  405.         self.btn_add = QPushButton(“+ 添加配置”)
  406.         self.btn_add.setFixedSize(120, 30)
  407.         self.btn_add.setStyleSheet(
  408.             “QPushButton { background-color: #3498DB; color: white; border-radius: 4px; font-weight: bold; }”
  409.             “QPushButton:hover { background-color: #2980B9; }”
  410.         )
  411.         self.btn_add.clicked.connect(self.add_entry_dialog)
  412.         self.btn_disconnect = QPushButton(“断开挂载”)
  413.         self.btn_disconnect.setFixedSize(120, 30)
  414.         self.btn_disconnect.setStyleSheet(
  415.             “QPushButton { background-color: #E74C3C; color: white; border-radius: 4px; font-weight: bold; }”
  416.             “QPushButton:hover { background-color: #C0392B; }”
  417.         )
  418.         self.btn_disconnect.clicked.connect(self.disconnect_all_mounts)
  419.         top_layout.addWidget(self.btn_add)
  420.         top_layout.addWidget(self.btn_disconnect)
  421.         top_layout.addStretch()
  422.         main_layout.addLayout(top_layout)
  423.         self.list_widget = QListWidget()
  424.         self.list_widget.setSpacing(0)
  425.         self.list_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  426.         main_layout.addWidget(self.list_widget)
  427.     def init_tray(self):
  428.         tray_icon = self.get_app_icon()
  429.         self.tray_icon = QSystemTrayIcon(self)
  430.         self.tray_icon.setIcon(tray_icon)
  431.         tray_menu = QMenu()
  432.         show_act = QAction(“显示窗口”, self, triggered=self.show_window)
  433.         exit_act = QAction(“退出”, self, triggered=self.quit_app)
  434.         tray_menu.addAction(show_act)
  435.         tray_menu.addAction(exit_act)
  436.         self.tray_icon.setContextMenu(tray_menu)
  437.         self.tray_icon.activated.connect(self.on_tray_activated)
  438.         self.tray_icon.show()
  439.     def show_window(self):
  440.         self.showNormal()
  441.         self.activateWindow()
  442.         self.raise_()
  443.     def close_to_tray(self):
  444.         self.hide()
  445.     def quit_app(self):
  446.         subprocess.run(“taskkill /IM rclone.exe /F”, shell=True,
  447.                        capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
  448.         self.active_mounts.clear()
  449.         self.tray_icon.hide()
  450.         QApplication.quit()
  451.     def on_tray_activated(self, reason):
  452.         if reason == QSystemTrayIcon.DoubleClick:
  453.             self.show_window()
  454.     def closeEvent(self, event):
  455.         event.ignore()
  456.         self.hide()
  457.     def disconnect_all_mounts(self):
  458.         subprocess.run(“taskkill /IM rclone.exe /F”, shell=True,
  459.                        capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
  460.         self.active_mounts.clear()
  461.         self.refresh_list()
  462.         QMessageBox.information(self, “提示”, “所有挂载已断开”)
  463.     def refresh_list(self):
  464.         self.list_widget.clear()
  465.         entries = self.get_all_entries()
  466.         for entry in entries:
  467.             item = QListWidgetItem()
  468.             widget = MountItemWidget(entry, self)
  469.             item.setSizeHint(widget.sizeHint())
  470.             self.list_widget.addItem(item)
  471.             self.list_widget.setItemWidget(item, widget)
  472.             auto_on = entry.get(“auto_mount”, False)
  473.             widget.btn_auto.setChecked(auto_on)
  474.             widget.update_auto_style()
  475.             is_mounted = (entry[“drive”] in self.active_mounts)
  476.             widget.is_mounted = is_mounted
  477.             if is_mounted:
  478.                 widget.btn_action.setText(“打开”)
  479.                 widget.btn_action.setStyleSheet(MountItemWidget._btn_style(“#27AE60”))
  480.             else:
  481.                 widget.btn_action.setText(“启动”)
  482.                 widget.btn_action.setStyleSheet(MountItemWidget._btn_style(“#3498DB”))
  483.     def get_used_drive_letters(self):
  484.         used = get_used_drives()
  485.         for entry in self.get_all_entries():
  486.             if entry.get(“drive”):
  487.                 used.add(entry[“drive”][0])
  488.         return used
  489.     def auto_mount_selected(self):
  490.         auto_entries = [e for e in self.get_all_entries() if e.get(“auto_mount”) and e.get(“drive”)]
  491.         debug_print(f”自动挂载条目数: {len(auto_entries)}”)
  492.         for entry in auto_entries:
  493.             debug_print(f”自动挂载: {entry[‘name’]} -> {entry[‘drive’]}”)
  494.             self.mount_entry(entry)
  495.     # ———- 增删改 ———-
  496.     def add_entry_dialog(self):
  497.         used = self.get_used_drive_letters()
  498.         dlg = MountEditDialog(self)
  499.         dlg.populate_drives(used)
  500.         if dlg.exec_() != QDialog.Accepted:
  501.             return
  502.         data = dlg.get_data()
  503.         if not data[“name”] or not data[“url”] or not data[“user”] or not data[“password”]:
  504.             QMessageBox.warning(self, “警告”, “必填项不能为空”)
  505.             return
  506.         remote_name = data[“name”]
  507.         if remote_name in self.metadata:
  508.             QMessageBox.warning(self, “警告”, “配置名称已存在”)
  509.             return
  510.         ret, out, err = rclone_cmd([
  511.             “config”, “create”, remote_name, “webdav”,
  512.             “vendor=other”, f”url={data[‘url’]}”, f”user={data[‘user’]}”,
  513.             f”pass={data[‘password’]}”, “–obscure”
  514.         ], cwd=self.base_dir)
  515.         if ret != 0:
  516.             QMessageBox.warning(self, “创建失败”, err)
  517.             return
  518.         self.metadata[remote_name] = {
  519.             “name”: data[“name”], “url”: data[“url”], “user”: data[“user”],
  520.             “drive”: data[“drive”], “size_value”: data[“size_value”],
  521.             “size_unit”: data[“size_unit”], “network_mode”: data[“network_mode”],
  522.             “auto_mount”: data[“auto_mount”]
  523.         }
  524.         self.save_metadata()
  525.         self.refresh_list()
  526.     def edit_entry_dialog(self, entry):
  527.         used = self.get_used_drive_letters()
  528.         reserved = set(entry[“drive”][0]) if entry[“drive”] else set()
  529.         existing_password = get_remote_password(entry[“id”])
  530.         dlg = MountEditDialog(self, existing=entry, existing_password=existing_password)
  531.         dlg.populate_drives(used, reserved)
  532.         if dlg.exec_() != QDialog.Accepted:
  533.             return
  534.         data = dlg.get_data()
  535.         if not data[“name”] or not data[“url”] or not data[“user”]:
  536.             QMessageBox.warning(self, “警告”, “名称、地址、用户名不能为空”)
  537.             return
  538.         old_remote = entry[“id”]
  539.         new_name = data[“name”]
  540.         name_changed = (new_name != old_remote)
  541.         new_pass = data[“password”]
  542.         need_recreate = name_changed or bool(new_pass)
  543.         if need_recreate:
  544.             if name_changed and new_name in self.metadata:
  545.                 QMessageBox.warning(self, “警告”, “新配置名称已存在”)
  546.                 return
  547.             actual_pass = new_pass if new_pass else existing_password
  548.             if not actual_pass:
  549.                 QMessageBox.warning(self, “错误”, “无法获取原始密码,请重新输入密码”)
  550.                 return
  551.             if name_changed:
  552.                 rclone_cmd([“config”, “delete”, old_remote], cwd=self.base_dir)
  553.             ret, _, err = rclone_cmd([
  554.                 “config”, “create”, new_name, “webdav”,
  555.                 “vendor=other”, f”url={data[‘url’]}”, f”user={data[‘user’]}”,
  556.                 f”pass={actual_pass}”, “–obscure”
  557.             ], cwd=self.base_dir)
  558.             if ret != 0:
  559.                 QMessageBox.warning(self, “更新失败”, err)
  560.                 return
  561.         if name_changed:
  562.             del self.metadata[old_remote]
  563.         self.metadata[new_name] = {
  564.             “name”: data[“name”], “url”: data[“url”], “user”: data[“user”],
  565.             “drive”: data[“drive”], “size_value”: data[“size_value”],
  566.             “size_unit”: data[“size_unit”], “network_mode”: data[“network_mode”],
  567.             “auto_mount”: data[“auto_mount”]
  568.         }
  569.         self.save_metadata()
  570.         if entry[“drive”] in self.active_mounts:
  571.             self.unmount_entry(entry)
  572.         self.refresh_list()
  573.     def delete_entry(self, entry):
  574.         if QMessageBox.question(self, “确认”, f”删除 ‘{entry[‘name’]}’?”) != QMessageBox.Yes:
  575.             return
  576.         if entry[“drive”] in self.active_mounts:
  577.             self.unmount_entry(entry)
  578.         rclone_cmd([“config”, “delete”, entry[“id”]], cwd=self.base_dir)
  579.         del self.metadata[entry[“id”]]
  580.         self.save_metadata()
  581.         self.refresh_list()
  582.     # ———- 挂载/卸载 ———-
  583.     def mount_entry(self, entry):
  584.         drive = entry.get(“drive”)
  585.         if not drive:
  586.             QMessageBox.warning(self, “挂载失败”, “未设置盘符”)
  587.             return
  588.         if drive in self.active_mounts:
  589.             self.unmount_entry(entry)
  590.         cache_subdir = os.path.join(self.base_dir, “Temp”, entry[“name”])
  591.         os.makedirs(cache_subdir, exist_ok=True)
  592.         total_size = f”{entry.get(‘size_value’,20)}{entry.get(‘size_unit’,’G’)}”
  593.         args = [
  594.             “mount”, f”{entry[‘id’]}:”, drive,
  595.             “–cache-dir”, f”./Temp/{entry[‘name’]}”,
  596.             “–vfs-cache-mode”, “full”,
  597.             “–allow-non-empty”,
  598.             “–dir-cache-time”, “5s”,
  599.             “–allow-other”,
  600.             “–vfs-disk-space-total-size”, total_size
  601.         ]
  602.         if entry.get(“network_mode”, True):
  603.             args.append(“–network-mode”)
  604.         debug_print(“挂载命令:”, f’rclone {” “.join(args)}’)
  605.         try:
  606.             proc = subprocess.Popen(
  607.                 [RCLONE_EXE] + args,
  608.                 cwd=self.base_dir,
  609.                 creationflags=subprocess.CREATE_NO_WINDOW
  610.             )
  611.             self.active_mounts[drive] = proc
  612.             self.refresh_list()
  613.             debug_print(f”挂载成功: {drive}”)
  614.         except Exception as e:
  615.             QMessageBox.warning(self, “挂载失败”, str(e))
  616.     def unmount_entry(self, entry):
  617.         drive = entry[“drive”]
  618.         if drive not in self.active_mounts:
  619.             return
  620.         proc = self.active_mounts.pop(drive)
  621.         rclone_cmd([“unmount”, drive], cwd=self.base_dir)
  622.         if proc.poll() is None:
  623.             try:
  624.                 proc.terminate()
  625.                 proc.wait(timeout=5)
  626.             except:
  627.                 proc.kill()
  628.         self.refresh_list()
  629. if __name__ == “__main__”:
  630.     app = QApplication(sys.argv)
  631.     app.setQuitOnLastWindowClosed(False)
  632.     app.setFont(QFont(“Microsoft YaHei”, 9))
  633.     window = RcloneTrayApp()
  634.     window.show()
  635.     sys.exit(app.exec_())
 

 

该资源来源于WordPress主站,点击下方按钮前往原文页面获取完整内容:

跳转至 WordPress 主站原文页面

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。