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

admin
🌐 经济型:买域名、轻量云服务器、用途:游戏 网站等 《腾讯云》特点:特价机便宜 适合初学者用 点我优惠购买
🚀 拓展型:买域名、轻量云服务器、用途:游戏 网站等 《阿里云》特点:中档服务器便宜 域名备案事多 点我优惠购买
🛡️ 稳定型:买域名、轻量云服务器、用途:游戏 网站等 《西部数码》 特点:比上两家略贵但是稳定性超好事也少 点我优惠购买
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(4.0, 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: #2c3.50;")
  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 主站原文页面

文章版权声明:除非注明,否则均为AI虎哥的工具库原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
验证码
评论列表 (暂无评论,10人围观)

还没有评论,来说两句吧...

目录[+]

取消
微信二维码
微信二维码
支付宝二维码