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

实用软件1周前更新 小编
2 0

Rclone  单 WebDav  图形化挂载工具

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

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

  

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

下载地址

https://wwbhx.lanzout.com/b00mqfsi0b

密码:8ysl

© 版权声明

相关文章