自用 python写的 Rclone WebDav 挂载工具 挂载本地磁盘
最近更新 2026年06月27日
资源编号 21719

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

郑重承诺丨三色资源网提供安全交易、信息保真!
增值服务:
¥ 免费 元宝
VIP折扣
    折扣详情
  • 体验VIP会员

    免费

  • 月卡VIP会员

    免费

  • 年卡VIP会员

    免费

  • 永久VIP会员

    免费

开通VIP尊享优惠特权
立即获取 升级会员
详情介绍

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_())
 

 

付费下载
当前内容需要支付免费 元宝才能下载
VIP折扣
    折扣详情
  • 体验VIP会员

    免费

  • 月卡VIP会员

    免费

  • 年卡VIP会员

    免费

  • 永久VIP会员

    免费

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

本站所有资源来源于网络,仅限用于学习研究;无任何技术支持!不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除内容。如果您喜欢,请支持正版。如有侵权请邮件与我们联系处理。

常见问题
  • 网盘有时候会因为名字 关键词导致失效 大家可以给管理员提供失效信息,我们会给大家适当积分进行奖励 我们会第一时间进行补充修正 感谢大家的配合 让我们共同努力 打造良好的资源分享平台
查看详情

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务