diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f09f2bf..88eac60 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,6 +98,11 @@ jobs: with: ref: ${{ github.sha }} + - name: setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + - name: setup node uses: actions/setup-node@v4 with: @@ -105,11 +110,6 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: diff --git a/package.json b/package.json index dc00c86..879f6f5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "check:i18n": "node scripts/check-i18n.mjs", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "tauri": "tauri", @@ -47,4 +48,4 @@ "vite": "^5.4.2", "zod": "^3.25.76" } -} \ No newline at end of file +} diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs new file mode 100644 index 0000000..89c920f --- /dev/null +++ b/scripts/check-i18n.mjs @@ -0,0 +1,132 @@ +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +// i18n key convention (storage/description): +// - Display label key: storage. +// - Description key: description. +// - Canonical : lowercase + trim + collapse whitespace (e.g. "WebDav" -> "webdav", "IPFS API" -> "ipfs api") +// Runtime also adds alias keys for a few historical case variants to keep backward compatibility. +const LOCALES_DIR = path.resolve('src-tauri', 'locales'); + +function normalizeStorageId(raw) { + return String(raw ?? '') + .trim() + .replace(/\s+/g, ' ') + .toLowerCase(); +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value.trim().length > 0; +} + +function formatKeyList(keys) { + return keys.length <= 8 ? keys.join(', ') : `${keys.slice(0, 8).join(', ')}, ... (+${keys.length - 8})`; +} + +async function loadJson(filePath) { + const text = await readFile(filePath, 'utf8'); + return JSON.parse(text); +} + +async function main() { + const entries = await readdir(LOCALES_DIR, { withFileTypes: true }); + const localeFiles = entries + .filter((e) => e.isFile() && e.name.endsWith('.json')) + .map((e) => path.join(LOCALES_DIR, e.name)) + .sort((a, b) => a.localeCompare(b)); + + if (localeFiles.length === 0) { + console.error(`[check:i18n] No locale json files found under: ${LOCALES_DIR}`); + process.exit(1); + } + + let hasError = false; + + for (const filePath of localeFiles) { + const localeName = path.basename(filePath); + let json; + try { + json = await loadJson(filePath); + } catch (e) { + console.error(`[check:i18n] Failed to parse ${localeName}: ${e?.message ?? String(e)}`); + hasError = true; + continue; + } + + if (!json || typeof json !== 'object' || Array.isArray(json)) { + console.error(`[check:i18n] ${localeName} must be a JSON object`); + hasError = true; + continue; + } + + const keys = Object.keys(json); + + // 1) No empty strings + const emptyKeys = keys.filter((k) => typeof json[k] === 'string' && json[k].trim() === ''); + if (emptyKeys.length > 0) { + console.error(`[check:i18n] ${localeName}: empty strings found: ${formatKeyList(emptyKeys)}`); + hasError = true; + } + + // 2) storage.* must have description. + const storageKeys = keys.filter((k) => k.startsWith('storage.')); + const missingDescriptions = []; + for (const storageKey of storageKeys) { + const suffix = storageKey.slice('storage.'.length); + const id = normalizeStorageId(suffix); + const descKey = `description.${id}`; + if (!isNonEmptyString(json[descKey])) { + missingDescriptions.push(`${storageKey} -> ${descKey}`); + } + } + if (missingDescriptions.length > 0) { + console.error( + `[check:i18n] ${localeName}: storage has but description missing (${missingDescriptions.length})`, + ); + for (const item of missingDescriptions.slice(0, 60)) { + console.error(` - ${item}`); + } + if (missingDescriptions.length > 60) { + console.error(` ... (+${missingDescriptions.length - 60})`); + } + hasError = true; + } + + // 3) Detect storage case/whitespace duplicates (same normalized id) + const groups = new Map(); + for (const storageKey of storageKeys) { + const suffix = storageKey.slice('storage.'.length); + const id = normalizeStorageId(suffix); + const group = groups.get(id) ?? []; + group.push(suffix); + groups.set(id, group); + } + const duplicates = []; + for (const [id, rawSuffixes] of groups.entries()) { + const unique = [...new Set(rawSuffixes)]; + if (unique.length > 1) { + duplicates.push({ id, variants: unique.sort((a, b) => a.localeCompare(b)) }); + } + } + if (duplicates.length > 0) { + console.warn(`[check:i18n] ${localeName}: storage key variants share the same id (${duplicates.length})`); + for (const d of duplicates.slice(0, 30)) { + console.warn(` - ${d.id}: ${d.variants.join(' | ')}`); + } + if (duplicates.length > 30) { + console.warn(` ... (+${duplicates.length - 30})`); + } + // Do not fail for duplicates; normalize/alias layer handles compatibility. + } + } + + if (hasError) { + process.exit(1); + } + console.log('[check:i18n] OK'); +} + +main().catch((e) => { + console.error(`[check:i18n] Unexpected error: ${e?.stack ?? e?.message ?? String(e)}`); + process.exit(1); +}); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 846ae34..5cf9ba1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,6 +43,7 @@ rand = "0.9" reqwest = { version = "0.13", features = ["json", "stream"] } tokio = { version = "1", features = ["full"] } futures-util = "0.3" +zip = "2.4.2" tauri-plugin-shell = "2.3.5" tauri-plugin-os = "2.3.2" tauri-plugin-fs = "2.4.5" diff --git a/src-tauri/capabilities/migrated.json b/src-tauri/capabilities/migrated.json index 503616f..fd97c08 100644 --- a/src-tauri/capabilities/migrated.json +++ b/src-tauri/capabilities/migrated.json @@ -190,6 +190,8 @@ "shell:default", "process:default", "os:default", - "dialog:allow-open" + "dialog:default", + "dialog:allow-open", + "dialog:allow-save" ] } diff --git a/src-tauri/locales/en.json b/src-tauri/locales/en.json index 5aeea7b..7d7b539 100644 --- a/src-tauri/locales/en.json +++ b/src-tauri/locales/en.json @@ -189,9 +189,16 @@ "components": "Components", "log": "Log", "start_hide": "Hide Window on Startup", - "close_to_tray": "Minimize to Tray on Close", + "auto_recover_components": "Auto-recover Components", "quit": "Quit", "tray_show": "Show Main Window", + "open_log_dir": "Open Log Folder", + "export_diagnostics": "Export Diagnostics", + "diagnostics_exported": "Diagnostics exported", + "rclone_restarting": "Rclone is unhealthy; restarting…", + "rclone_restarted": "Rclone restarted", + "openlist_restarting": "OpenList is unhealthy; restarting…", + "openlist_restarted": "OpenList restarted", "tray_hide": "Hide Main Window", "language": "Language", "update_available": "New Version Available", @@ -326,7 +333,6 @@ "storage.189Cloud": "189 Cloud", "storage.189CloudPC": "189 Cloud PC", "storage.OpenList V2": "OpenList", - "storage.Alias": "Alias", "storage.Aliyundrive": "Aliyun Drive", "storage.AliyundriveOpen": "Aliyun Drive Open", "storage.AliyundriveShare": "Aliyun Drive Share", @@ -335,30 +341,20 @@ "storage.BaiduShare": "Baidu Cloud Share", "storage.ChaoXingGroupDrive": "ChaoXing Group Drive", "storage.Cloudreve": "Cloudreve", - "storage.Crypt": "Crypt Encryption", "storage.Doge": "Doge Cloud", - "storage.Dropbox": "Dropbox", - "storage.FTP": "FTP Server", "storage.FeijiPan": "Feiji Pan", "storage.GoogleDrive": "Google Drive", "storage.GooglePhoto": "Google Photos", "storage.ILanZou": "ILanZou", "storage.IPFS API": "IPFS API", "storage.Lanzou": "Lanzou", - "storage.Local": "Local Storage", "storage.MediaTrack": "MediaTrack", "storage.Mega_nz": "Mega", "storage.MoPan": "MoPan", - "storage.Onedrive": "OneDrive", "storage.OnedriveAPP": "OneDrive APP", - "storage.PikPak": "PikPak", "storage.PikPakShare": "PikPak Share", "storage.Quark": "Quark Cloud", "storage.Quqi": "Quqi Cloud", - "storage.S3": "S3 Object Storage", - "storage.SFTP": "SFTP", - "storage.SMB": "SMB", - "storage.Seafile": "Seafile", "storage.Teambition": "Teambition", "storage.Terabox": "Terabox", "storage.Thunder": "Thunder Cloud", @@ -369,7 +365,6 @@ "storage.UrlTree": "URL Tree", "storage.VTencent": "V Tencent", "storage.Virtual": "Virtual Storage", - "storage.WebDav": "WebDAV", "storage.WeiYun": "Weiyun", "storage.WoPan": "WoPan", "storage.YandexDisk": "Yandex Disk", @@ -430,6 +425,7 @@ "storage.drive": "Google Drive", "storage.gcs": "Google Cloud Storage", "storage.onedriveshare": "OneDrive Share Link", + "storage.onedrive sharelink": "OneDrive Share Link", "storage.189cloudtv": "189 Cloud TV", "storage.115 open": "115 Cloud Open", "storage.NeteaseMusic": "NetEase Cloud Music", @@ -499,6 +495,7 @@ "description.baiduphoto": "Yi Ke Album is a photo stoshare, which is convenient for collaboration and distribution.", "description.123panshare": "123 Pan Share functionality enables users to share files stored on 123 Pan, facilitating collaboration and distribution.", "description.onedrive sharelink": "OneDrive Share Link allows users to share files stored on OneDrive with others, facilitating collaboration and distribution.", + "description.onedriveshare": "OneDrive Share Link allows users to share files stored on OneDrive with others, facilitating collaboration and distribution.", "description.189cloudtv": "189 Cloud TV interface", "description.115 open": "115 Official Open API", "description.lark": "Lark Drive lets you save and manage all your content in cloud storage anytime, anywhere, on any device.", @@ -521,6 +518,63 @@ "description.lenovonasshare": "Lenovo NAS Share: Access shared resources on Lenovo NAS devices.", "description.misskey": "Misskey is a decentralized social platform, and this driver is used to access its file/attachment resources.", "description.thunderbrowserexpert": "Thunder Browser Expert Edition related file resource access method.", + "description.115 cloud": "115 Cloud Storage: Store, manage, and access files in your 115 Cloud account.", + "description.115 share": "115 Cloud Share: Access files via 115 share links.", + "description.123pan": "123 Pan is a cloud storage service for storing and managing files online.", + "description.123panlink": "123 Pan Direct Link: Access files via a direct link.", + "description.azureblob": "Azure Blob Storage is Microsoft Azure's object storage service, suitable for storing massive amounts of unstructured data.", + "description.azurefiles": "Azure Files provides fully managed SMB file shares in Microsoft Azure.", + "description.b2": "Backblaze B2 is an object storage service offering durable, low-cost storage for files and backups.", + "description.baidushare": "Baidu Cloud Share: Access files via Baidu share links.", + "description.cache": "Cache storage: Temporarily stores data to improve performance.", + "description.chunker": "Chunker: Splits large files into smaller chunks for transfer or processing.", + "description.combine": "Combine: Combines multiple sources into a single unified view.", + "description.compress": "Compress: Stores files in compressed form to save space.", + "description.drive": "Google Drive is a cloud storage service provided by Google, integrated with the Google ecosystem, supporting document collaboration, backup, and sharing.", + "description.feijipan": "Feiji Pan is a cloud storage service for storing and managing files online.", + "description.fichier": "Fichier is a file hosting service that provides online storage and sharing.", + "description.filefabric": "FileFabric is an enterprise file management and content platform for secure access and sharing.", + "description.gcs": "Google Cloud Storage is a cloud storage service from Google Cloud Platform, offering various storage tiers suitable for different performance, cost, and durability requirements.", + "description.gphotos": "Google Photos is a photo backup and sharing service provided by Google, allowing users to easily store, organize, and share photos and videos.", + "description.hasher": "Hasher: Generates checksums/hashes for files to verify integrity.", + "description.hdfs": "HDFS (Hadoop Distributed File System) is a distributed file system designed for large-scale data storage.", + "description.hidrive": "HiDrive is a cloud storage service offering file sync, backup, and sharing.", + "description.ilanzou": "ILanZou is a Lanzou-related cloud storage/drive service for online files.", + "description.imagekit": "ImageKit is a media storage and delivery service for images and videos.", + "description.internetarchive": "Internet Archive is a non-profit digital library offering free access to archived content.", + "description.ipfs api": "IPFS API: Access IPFS-backed content via an API endpoint.", + "description.koofr": "Koofr is a cloud storage service supporting file sync, backup, and sharing.", + "description.lanzou": "Lanzou is a cloud storage service commonly used for file sharing and distribution.", + "description.linkbox": "Linkbox is a cloud storage/file sharing service for managing online files.", + "description.local": "Local storage on this device (use existing folders and disks as a backend).", + "description.mailru": "Mail.ru Cloud is a cloud storage service for file synchronization and sharing.", + "description.mediatrack": "MediaTrack: Access and manage media resources provided by the MediaTrack service.", + "description.mega_nz": "Mega is a cloud storage service that emphasizes user privacy protection, offering end-to-end encryption and large-capacity storage options.", + "description.memory": "Memory: Stores data in memory (temporary, not persisted to disk).", + "description.mopan": "MoPan is a cloud storage service for storing and managing files online.", + "description.netstorage": "NetStorage is a cloud storage service for file hosting and distribution.", + "description.oos": "Oracle Object Storage provides durable and scalable object storage on Oracle Cloud.", + "description.openlist v2": "OpenList is an open-source file hosting and management application that supports various cloud storage services.", + "description.pikpak": "PikPak is a cloud storage service focused on saving and managing files from multiple sources.", + "description.pikpakshare": "PikPak Share: Access files via PikPak share links.", + "description.premiumizeme": "Premiumize.me is a multi-hoster and cloud service for downloading, caching, and storing files.", + "description.protondrive": "Proton Drive is an encrypted cloud storage service focused on privacy and security.", + "description.putio": "Put.io is a cloud storage and download manager that can fetch content from various sources.", + "description.quark": "Quark Cloud is a cloud storage service for file storage, synchronization, and sharing.", + "description.quatrix": "Quatrix is a secure file sharing and collaboration platform for enterprises.", + "description.sharefile": "ShareFile is a secure file sharing and collaboration service (Citrix ShareFile).", + "description.sia": "Sia is a decentralized cloud storage platform.", + "description.smb": "SMB (Server Message Block) is a network file sharing protocol for accessing shared folders and files on a LAN.", + "description.storj": "Storj is a decentralized cloud object storage service.", + "description.sugarsync": "SugarSync is a cloud file synchronization and backup service.", + "description.tardigrade": "Tardigrade (Storj) provides decentralized object storage built on the Storj network.", + "description.thunderexpert": "Thunder Expert Edition: Access Thunder-related cloud disk resources via the Expert driver.", + "description.uc": "UC Cloud is a cloud storage service provided by UC Browser, allowing users to conveniently access and manage files through UC Browser.", + "description.union": "Union: Combines multiple remotes into a single virtual remote.", + "description.uptobox": "UpToBox is a file hosting service providing online storage and sharing.", + "description.urltree": "URL Tree: Organize and access multiple URLs as a tree-like virtual filesystem.", + "description.virtual": "Virtual storage: A virtualized backend that combines or transforms other storages.", + "description.zoho": "Zoho WorkDrive/Zoho storage services provide cloud file storage and collaboration.", "restartself_to_take_effect": "Restart to take effect", "unable_to_obtain_transmission_speed":"The specific transmission speed may not be available at present, but the transmission is still in progress.", diff --git a/src-tauri/locales/zh-cn.json b/src-tauri/locales/zh-cn.json index 2a4b8be..7878bbc 100644 --- a/src-tauri/locales/zh-cn.json +++ b/src-tauri/locales/zh-cn.json @@ -197,9 +197,16 @@ "components": "组件", "log": "日志", "start_hide": "启动时隐藏窗口", - "close_to_tray": "关闭窗口最小化到托盘", + "auto_recover_components": "组件自愈", "quit": "退出", "tray_show": "显示主窗口", + "open_log_dir": "打开日志目录", + "export_diagnostics": "导出诊断包", + "diagnostics_exported": "诊断包已导出", + "rclone_restarting": "Rclone 异常,正在自动重启…", + "rclone_restarted": "Rclone 已重启", + "openlist_restarting": "OpenList 异常,正在自动重启…", + "openlist_restarted": "OpenList 已重启", "tray_hide": "隐藏主窗口", "language": "语言", "update_available": "发现新版本", @@ -338,7 +345,6 @@ "storage.189Cloud": "天翼云盘", "storage.189CloudPC": "天翼云盘 PC", "storage.OpenList": "OpenList", - "storage.Alias": "别名", "storage.Aliyundrive": "阿里云盘", "storage.AliyundriveOpen": "阿里云盘 Open", "storage.AliyundriveShare": "阿里云盘分享", @@ -347,30 +353,20 @@ "storage.BaiduShare": "百度网盘分享", "storage.ChaoXingGroupDrive": "超星小组网盘", "storage.Cloudreve": "Cloudreve", - "storage.Crypt": "Crypt 加密", "storage.Doge": "多吉云", - "storage.Dropbox": "Dropbox", - "storage.FTP": "FTP服务器", "storage.FeijiPan": "小飞机网盘", "storage.GoogleDrive": "Google Drive", "storage.GooglePhoto": "Google 相册", "storage.ILanZou": "蓝奏优享版", "storage.IPFS API": "IPFS API", "storage.Lanzou": "蓝奏云", - "storage.Local": "本地存储", "storage.MediaTrack": "分秒帧", "storage.Mega_nz": "Mega", "storage.MoPan": "MoPan魔盘", - "storage.Onedrive": "OneDrive", "storage.OnedriveAPP": "OneDrive APP", - "storage.PikPak": "PikPak", "storage.PikPakShare": "PikPak分享", "storage.Quark": "夸克网盘", "storage.Quqi": "曲奇云盘", - "storage.S3": "S3 对象存储", - "storage.SFTP": "SFTP", - "storage.SMB": "SMB", - "storage.Seafile": "Seafile网盘", "storage.Teambition": "Teambition网盘", "storage.Terabox": "Terabox网盘", "storage.Thunder": "迅雷云盘", @@ -381,7 +377,6 @@ "storage.UrlTree": "链接树", "storage.VTencent": "腾讯智能创作平台", "storage.Virtual": "虚拟存储", - "storage.WebDav": "WebDAV", "storage.WeiYun": "腾讯微云", "storage.WoPan": "联通云盘", "storage.YandexDisk": "Yandex Disk", @@ -442,6 +437,7 @@ "storage.drive": "Google Drive", "storage.gcs": "Google Cloud Storage", "storage.onedriveshare": "OneDrive 分享链接", + "storage.onedrive sharelink": "OneDrive 分享链接", "storage.189cloudtv": "天翼云盘 TV", "storage.115 open": "115网盘 Open", "storage.NeteaseMusic": "网易云音乐", @@ -568,6 +564,7 @@ "description.123panlink": "123网盘直链服务允许用户获取文件的直接下载链接,便于快速访问和传输文件。", "description.123panshare": "123网盘分享功能使用户能够将存储在123网盘上的文件与他人共享,便于协作和分发。", "description.onedrive sharelink": "OneDrive 分享链接允许用户将存储在 OneDrive 上的文件与他人共享,便于协作和分发。", + "description.onedriveshare": "OneDrive 分享链接允许用户将存储在 OneDrive 上的文件与他人共享,便于协作和分发。", "description.189cloudtv": "天翼云盘的TV接口", "description.115 open": "115 官方开放API", "description.lark": "Lark Drive允许您随时随地在任何设备上保存和管理云存储中的所有内容。", diff --git a/src-tauri/locales/zh-hant.json b/src-tauri/locales/zh-hant.json index 11a6bdc..efe33d9 100644 --- a/src-tauri/locales/zh-hant.json +++ b/src-tauri/locales/zh-hant.json @@ -197,9 +197,16 @@ "components": "組件", "log": "日誌", "start_hide": "啟動時隱藏視窗", - "close_to_tray": "關閉視窗最小化到托盤", + "auto_recover_components": "元件異常自動修復", "quit": "退出", "tray_show": "顯示主視窗", + "open_log_dir": "打開日誌目錄", + "export_diagnostics": "匯出診斷包", + "diagnostics_exported": "診斷包已匯出", + "rclone_restarting": "Rclone 異常,正在自動重啟…", + "rclone_restarted": "Rclone 已重啟", + "openlist_restarting": "OpenList 異常,正在自動重啟…", + "openlist_restarted": "OpenList 已重啟", "tray_hide": "隱藏主視窗", "language": "語言", "update_available": "發現新版本", @@ -338,7 +345,6 @@ "storage.189Cloud": "天翼雲盤", "storage.189CloudPC": "天翼雲盤 PC", "storage.OpenList": "OpenList", - "storage.Alias": "別名", "storage.Aliyundrive": "阿裡雲盤", "storage.AliyundriveOpen": "阿裡雲盤 Open", "storage.AliyundriveShare": "阿裡雲盤分享", @@ -347,30 +353,20 @@ "storage.BaiduShare": "百度網盤分享", "storage.ChaoXingGroupDrive": "超星小組網盤", "storage.Cloudreve": "Cloudreve", - "storage.Crypt": "Crypt 加密", "storage.Doge": "多吉雲", - "storage.Dropbox": "Dropbox", - "storage.FTP": "FTP伺服器", "storage.FeijiPan": "小飛機網盤", "storage.GoogleDrive": "Google Drive", "storage.GooglePhoto": "Google 相冊", "storage.ILanZou": "藍奏優享版", "storage.IPFS API": "IPFS API", "storage.Lanzou": "藍奏雲", - "storage.Local": "本機存放區", "storage.MediaTrack": "分秒幀", "storage.Mega_nz": "Mega", "storage.MoPan": "MoPan魔盤", - "storage.Onedrive": "OneDrive", "storage.OnedriveAPP": "OneDrive APP", - "storage.PikPak": "PikPak", "storage.PikPakShare": "PikPak分享", "storage.Quark": "誇克網盤", "storage.Quqi": "曲奇雲盤", - "storage.S3": "S3 物件存儲", - "storage.SFTP": "SFTP", - "storage.SMB": "SMB", - "storage.Seafile": "Seafile網盤", "storage.Teambition": "Teambition網盤", "storage.Terabox": "Terabox網盤", "storage.Thunder": "迅雷雲盤", @@ -381,7 +377,6 @@ "storage.UrlTree": "連結樹", "storage.VTencent": "騰訊智能創作平臺", "storage.Virtual": "虛擬存儲", - "storage.WebDav": "WebDAV", "storage.WeiYun": "騰訊微雲", "storage.WoPan": "聯通雲盤", "storage.YandexDisk": "Yandex Disk", @@ -442,6 +437,7 @@ "storage.drive": "Google Drive", "storage.gcs": "Google Cloud Storage", "storage.onedriveshare": "OneDrive 分享連結", + "storage.onedrive sharelink": "OneDrive 分享連結", "storage.189cloudtv": "天翼雲盤 TV", "storage.115 open": "115網盤 Open", "storage.NeteaseMusic": "網易雲音樂", @@ -568,6 +564,7 @@ "description.123panlink": "123網盤直鏈服務允許使用者獲取檔的直接下載連結,便於快速訪問和傳輸檔。", "description.123panshare": "123網盤分享功能使使用者能夠將存儲在123網盤上的檔與他人共用,便於協作和分發。", "description.onedrive sharelink": "OneDrive 分享連結允許用戶將存儲在 OneDrive 上的文件與他人共享,便於協作和分發。", + "description.onedriveshare": "OneDrive 分享連結允許用戶將存儲在 OneDrive 上的文件與他人共享,便於協作和分發。", "description.189cloudtv": "天翼雲盤的TV接口", "description.115 open": "115 官方開放API", "description.lark": "Lark Drive讓您隨時隨地在任何裝置上保存和管理雲端儲存的所有內容。", diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 1644c59..6c4df87 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -17,7 +17,7 @@ impl Default for Config { "mount": { "lists": [] }, "task": [], "api": { "url": "https://api.hotpe.top/API/NetMount" }, - "settings": { "themeMode": "auto", "startHide": false, "closeToTray": true }, + "settings": { "themeMode": "auto", "startHide": false, "autoRecoverComponents": true }, "framework": { "rclone": { "user": random_str(32), "password": random_str(128) }, "openlist": { "user": "admin", "password": random_str(16) } diff --git a/src-tauri/src/diagnostics.rs b/src-tauri/src/diagnostics.rs new file mode 100644 index 0000000..66a5439 --- /dev/null +++ b/src-tauri/src/diagnostics.rs @@ -0,0 +1,258 @@ +use std::{ + fs, + io::{Read, Seek, SeekFrom, Write}, + path::{Path, PathBuf}, +}; + +use tauri::Manager as _; + +use crate::Runtime; + +fn resolve_tilde(app: &tauri::AppHandle, path: &str) -> anyhow::Result { + if path.starts_with('~') { + let home = app + .path() + .home_dir() + .map_err(|e| anyhow::anyhow!("Failed to get home dir: {}", e))?; + let rest = path + .trim_start_matches('~') + .trim_start_matches(['/', '\\']); + if rest.is_empty() { + Ok(home) + } else { + Ok(home.join(rest)) + } + } else { + Ok(Path::new(path).to_owned()) + } +} + +fn app_data_dir(app: &tauri::AppHandle) -> anyhow::Result { + Ok(app + .path() + .home_dir() + .map_err(|e| anyhow::anyhow!("Failed to get home dir: {}", e))? + .join(".netmount")) +} + +fn ensure_under_app_data_dir(app: &tauri::AppHandle, candidate: &Path) -> anyhow::Result<()> { + let base = app_data_dir(app)?; + let base = base.canonicalize().unwrap_or(base); + let candidate = candidate + .canonicalize() + .unwrap_or_else(|_| candidate.to_owned()); + if !candidate.starts_with(&base) { + return Err(anyhow::anyhow!( + "Access denied: only files under {} are allowed", + base.display() + )); + } + Ok(()) +} + +fn read_file_tail(path: &Path, max_bytes: u64) -> anyhow::Result> { + let max_bytes = max_bytes.max(1024); + let mut file = fs::File::open(path)?; + let len = file.metadata().map(|m| m.len()).unwrap_or(0); + let start = len.saturating_sub(max_bytes); + file.seek(SeekFrom::Start(start))?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + Ok(buf) +} + +fn redact_json(value: &mut serde_json::Value) { + use serde_json::Value; + match value { + Value::Object(map) => { + for (k, v) in map.iter_mut() { + let key = k.to_ascii_lowercase(); + let is_sensitive = matches!( + key.as_str(), + "password" + | "pass" + | "passwd" + | "token" + | "secret" + | "access_key" + | "accesskey" + | "refresh_token" + | "client_secret" + | "private_key" + | "apikey" + | "api_key" + ); + if is_sensitive { + *v = Value::String("***REDACTED***".into()); + } else { + redact_json(v); + } + } + } + Value::Array(arr) => { + for v in arr.iter_mut() { + redact_json(v); + } + } + _ => {} + } +} + +fn zip_add_bytes( + zip: &mut zip::ZipWriter, + name: &str, + bytes: &[u8], +) -> anyhow::Result<()> { + let options: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default() + .compression_method(zip::CompressionMethod::Deflated); + zip.start_file(name, options)?; + zip.write_all(bytes)?; + Ok(()) +} + +fn zip_add_string( + zip: &mut zip::ZipWriter, + name: &str, + s: &str, +) -> anyhow::Result<()> { + zip_add_bytes(zip, name, s.as_bytes()) +} + +fn maybe_add_tail_file( + app: &tauri::AppHandle, + zip: &mut zip::ZipWriter, + entry_name: &str, + path: &Path, + max_bytes: u64, +) -> anyhow::Result<()> { + if !path.exists() { + return Ok(()); + } + ensure_under_app_data_dir(app, path)?; + let data = read_file_tail(path, max_bytes)?; + zip_add_bytes(zip, entry_name, &data)?; + Ok(()) +} + +fn maybe_add_redacted_json_file( + app: &tauri::AppHandle, + zip: &mut zip::ZipWriter, + entry_name: &str, + path: &Path, +) -> anyhow::Result<()> { + if !path.exists() { + return Ok(()); + } + ensure_under_app_data_dir(app, path)?; + let content = fs::read_to_string(path)?; + let mut json: serde_json::Value = serde_json::from_str(&content)?; + redact_json(&mut json); + let pretty = serde_json::to_string_pretty(&json)?; + zip_add_string(zip, entry_name, &pretty)?; + Ok(()) +} + +#[tauri::command] +pub fn export_diagnostics( + app: tauri::AppHandle, + out_path: String, +) -> anyhow_tauri::TAResult { + fn inner(app: &tauri::AppHandle, out_path: &str) -> anyhow::Result { + let out_path = out_path.trim(); + if out_path.is_empty() { + return Err(anyhow::anyhow!("Output path is required")); + } + if !out_path.to_ascii_lowercase().ends_with(".zip") { + return Err(anyhow::anyhow!("Output file must end with .zip")); + } + + let out = resolve_tilde(app, out_path)?; + if let Some(parent) = out.parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent).map_err(anyhow::Error::from)?; + } + } + + let file = fs::File::create(&out).map_err(anyhow::Error::from)?; + let mut zip = zip::ZipWriter::new(file); + let mut warnings: Vec = Vec::new(); + + let meta = serde_json::json!({ + "app": "NetMount", + "app_version": env!("CARGO_PKG_VERSION"), + "os": std::env::consts::OS, + "arch": std::env::consts::ARCH, + "timestamp_unix_ms": now_ms(), + }); + let meta_str = serde_json::to_string_pretty(&meta).map_err(anyhow::Error::from)?; + zip_add_string(&mut zip, "meta.json", &meta_str)?; + + let data_dir = app_data_dir(app)?; + + // redacted configs + if let Err(e) = maybe_add_redacted_json_file( + app, + &mut zip, + "netmount/config.redacted.json", + &data_dir.join("config.json"), + ) { + warnings.push(format!("netmount config: {}", e)); + } + if let Err(e) = maybe_add_redacted_json_file( + app, + &mut zip, + "openlist/config.redacted.json", + &data_dir.join("openlist").join("config.json"), + ) { + warnings.push(format!("openlist config: {}", e)); + } + + // log tails + if let Err(e) = maybe_add_tail_file( + app, + &mut zip, + "logs/rclone.log.tail", + &data_dir.join("log").join("rclone.log"), + 512 * 1024, + ) { + warnings.push(format!("rclone log: {}", e)); + } + if let Err(e) = maybe_add_tail_file( + app, + &mut zip, + "logs/netmount.log.tail", + &data_dir.join("log").join("netmount.log"), + 512 * 1024, + ) { + warnings.push(format!("netmount log: {}", e)); + } + if let Err(e) = maybe_add_tail_file( + app, + &mut zip, + "logs/openlist.log.tail", + &data_dir.join("openlist").join("log").join("log.log"), + 512 * 1024, + ) { + warnings.push(format!("openlist log: {}", e)); + } + + if !warnings.is_empty() { + let content = warnings.join("\n"); + let _ = zip_add_string(&mut zip, "warnings.txt", &content); + } + + zip.finish().map_err(anyhow::Error::from)?; + Ok(out.to_string_lossy().to_string()) + } + + inner(&app, &out_path).map_err(Into::into) +} + +fn now_ms() -> u128 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) +} + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 78e80db..fba51fc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,6 +12,7 @@ use locale::Locale; use tray::Tray; mod config; +mod diagnostics; mod fs; mod locale; mod sidecar; @@ -181,29 +182,6 @@ pub fn init() -> anyhow::Result<()> { .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) - .on_window_event(|window, event| match event { - tauri::WindowEvent::CloseRequested { api, .. } => { - let close_to_tray = window - .app_handle() - .with_app_state::(|config| { - config - .0 - .get("settings") - .and_then(|s| s.get("closeToTray")) - .and_then(|v| v.as_bool()) - .unwrap_or(true) - }); - - if close_to_tray { - api.prevent_close(); - let _ = window.hide(); - } else { - // Ensure sidecars don't get orphaned on Unix when quitting via window close. - sidecar::cleanup(); - } - } - _ => {} - }) .plugin(tauri_plugin_single_instance::init(|app, _, _| { if let Some(window) = app.app_main_window() { let _ = window.toggle_visibility(Some(true)); @@ -219,6 +197,7 @@ pub fn init() -> anyhow::Result<()> { update_config, get_language_pack, download_file, + diagnostics::export_diagnostics, get_autostart_state, set_autostart_state, get_winfsp_install_state, diff --git a/src/controller/language/localized.ts b/src/controller/language/localized.ts index 9389f41..ba021f8 100644 --- a/src/controller/language/localized.ts +++ b/src/controller/language/localized.ts @@ -4,11 +4,13 @@ import { invoke } from "@tauri-apps/api/core"; import i18n from "../../services/i18n"; import { roConfig } from "../../services/config"; import { hooks } from "../../services/hook"; +import { normalizeI18nPack } from "../../services/i18nPack"; async function setLocalized(lang: string) { lang = lang.toLowerCase(); - const pack: Record = await invoke("get_language_pack"); + const rawPack: Record = await invoke("get_language_pack"); + const pack = normalizeI18nPack(rawPack); i18n.addResourceBundle(lang, "translation", pack) i18n.changeLanguage(lang); hooks.setLocaleStr(getLangCode(lang)); diff --git a/src/controller/main.ts b/src/controller/main.ts index 354c8e3..d6a2bc0 100644 --- a/src/controller/main.ts +++ b/src/controller/main.ts @@ -6,9 +6,9 @@ import { startUpdateCont } from "./stats/continue" import { reupMount } from "./storage/mount/mount" import { reupStorage } from "./storage/storage" import { listenWindow, windowsHide } from "./window" -import { formatPath, sleep } from "../utils/utils" +import { sleep } from "../utils/utils" import { t } from "i18next" -import { startRclone, stopRclone } from "../utils/rclone/process" +import { restartRclone, startRclone, stopRclone } from "../utils/rclone/process" import { getOsInfo } from "../utils/tauri/osInfo" import { startTaskScheduler } from "./task/task" import { autoMount } from "./task/autoMount" @@ -16,15 +16,20 @@ import { setThemeMode } from "./setting/setting" import { setLocalized } from "./language/localized" import { checkNotice } from "./update/notice" import { updateStorageInfoList } from "./storage/allList" -import { startOpenlist, stopOpenlist } from "../utils/openlist/process" +import { restartOpenlist, startOpenlist, stopOpenlist } from "../utils/openlist/process" import { homeDir } from "@tauri-apps/api/path" -import { openlist_api_get } from "../utils/openlist/request" +import { openlist_api_get, openlist_api_ping } from "../utils/openlist/request" import { openlistInfo } from "../services/openlist" import { addOpenlistInRclone } from "../utils/openlist/openlist" import { Notification } from "@arco-design/web-react" +import { rclone_api_noop } from "../utils/rclone/request" +import { defaultCacheDir } from "../utils/netmountPaths" type SetStartStrFn = (str: string) => void; +let componentWatchdogTimer: number | undefined +let componentWatchdogStopping = false + async function init(setStartStr: SetStartStrFn) { setStartStr(t('init')) @@ -54,7 +59,7 @@ async function init(setStartStr: SetStartStrFn) { //设置缓存路径 if (!nmConfig.settings.path.cacheDir) { - nmConfig.settings.path.cacheDir=formatPath(roConfig.env.path.homeDir+'/.cache/netmount', osInfo.platform === "windows") + nmConfig.settings.path.cacheDir = defaultCacheDir() } setThemeMode(nmConfig.settings.themeMode) @@ -84,6 +89,8 @@ async function init(setStartStr: SetStartStrFn) { //开始任务队列 await startTaskScheduler() + startComponentWatchdog() + await main() } @@ -157,6 +164,11 @@ async function reupOpenlistVersion() { async function exit(isRestartSelf: boolean = false) { + componentWatchdogStopping = true + if (componentWatchdogTimer) { + clearInterval(componentWatchdogTimer) + componentWatchdogTimer = undefined + } try { await saveNmConfig() await stopRclone() @@ -172,4 +184,87 @@ async function exit(isRestartSelf: boolean = false) { } } -export { init, main, exit } \ No newline at end of file +function startComponentWatchdog() { + componentWatchdogStopping = false + if (componentWatchdogTimer) { + clearInterval(componentWatchdogTimer) + componentWatchdogTimer = undefined + } + + let running = false + let rcloneFailCount = 0 + let openlistFailCount = 0 + let rcloneCooldownUntil = 0 + let openlistCooldownUntil = 0 + + const COOLDOWN_MS = 60_000 + const INTERVAL_MS = 10_000 + const FAIL_THRESHOLD = 3 + + componentWatchdogTimer = window.setInterval(async () => { + if (componentWatchdogStopping) return + if (!nmConfig.settings.autoRecoverComponents) return + if (running) return + running = true + + try { + const now = Date.now() + + if (rcloneInfo.process.child) { + const ok = await rclone_api_noop() + rcloneFailCount = ok ? 0 : rcloneFailCount + 1 + if (!ok && rcloneFailCount >= FAIL_THRESHOLD && now >= rcloneCooldownUntil) { + rcloneCooldownUntil = now + COOLDOWN_MS + rcloneFailCount = 0 + Notification.warning({ + id: 'rclone_auto_recover', + title: t('transmit'), + content: t('rclone_restarting'), + }) + try { + await restartRclone() + Notification.success({ + id: 'rclone_auto_recover_ok', + title: t('success'), + content: t('rclone_restarted'), + }) + } catch (e) { + console.error('restartRclone failed:', e) + } + } + } else { + rcloneFailCount = 0 + } + + if (openlistInfo.process.child) { + const ok = await openlist_api_ping() + openlistFailCount = ok ? 0 : openlistFailCount + 1 + if (!ok && openlistFailCount >= FAIL_THRESHOLD && now >= openlistCooldownUntil) { + openlistCooldownUntil = now + COOLDOWN_MS + openlistFailCount = 0 + Notification.warning({ + id: 'openlist_auto_recover', + title: t('storage'), + content: t('openlist_restarting'), + }) + try { + await restartOpenlist() + Notification.success({ + id: 'openlist_auto_recover_ok', + title: t('success'), + content: t('openlist_restarted'), + }) + } catch (e) { + console.error('restartOpenlist failed:', e) + } + } + } else { + openlistFailCount = 0 + } + } finally { + running = false + } + }, INTERVAL_MS) +} + +export { init, main, exit } diff --git a/src/controller/storage/framework/openlist/providers.ts b/src/controller/storage/framework/openlist/providers.ts index ebd1f14..91998bc 100644 --- a/src/controller/storage/framework/openlist/providers.ts +++ b/src/controller/storage/framework/openlist/providers.ts @@ -1,6 +1,10 @@ import { StorageInfoType, StorageParamItemType } from "../../../../type/controller/storage/info"; import { openlist_api_get } from "../../../../utils/openlist/request"; +function normalizeStorageId(raw: string): string { + return String(raw ?? '').trim().replace(/\s+/g, ' ').toLowerCase(); +} + // 结构A:对象映射 { driverName: {config, common, additional} } // 结构B:数组 [ {name, config, common, additional} ] @@ -207,9 +211,9 @@ async function updateOpenlistStorageInfoList() { }; openlistStorageInfoList.push({ - label: 'storage.' + config.name, + label: 'storage.' + normalizeStorageId(config.name ?? key), type: key, - description: 'description.' + key.toLocaleLowerCase(), + description: 'description.' + normalizeStorageId(key), framework: 'openlist', defaultParams: { name: config.name + '_new', @@ -287,9 +291,9 @@ async function updateOpenlistStorageInfoListFallback(): Promise { - if (nmConfig.settings.closeToTray) { - windowsHide(); - } + windowsHide(); + return false }) diff --git a/src/page/setting/setting.tsx b/src/page/setting/setting.tsx index d2134e1..4876f0c 100644 --- a/src/page/setting/setting.tsx +++ b/src/page/setting/setting.tsx @@ -7,12 +7,14 @@ import { getVersion } from '@tauri-apps/api/app'; import * as shell from '@tauri-apps/plugin-shell'; import { rcloneInfo } from '../../services/rclone'; import { setLocalized } from '../../controller/language/localized'; -import { formatPath, openUrlInBrowser, set_devtools_state } from '../../utils/utils'; +import { formatPath, openUrlInBrowser, set_devtools_state, showPathInExplorer } from '../../utils/utils'; import { showLog } from '../other/modal'; import { openlistInfo } from '../../services/openlist'; import * as dialog from '@tauri-apps/plugin-dialog'; import { exit } from '../../controller/main'; import { readTextFileTail } from '../../utils/logs'; +import { invoke } from '@tauri-apps/api/core'; +import { netmountLogDir, openlistLogFile, rcloneLogFile } from '../../utils/netmountPaths'; // const CollapseItem = Collapse.Item; const FormItem = Form.Item; @@ -111,9 +113,9 @@ export default function Setting_page() { forceUpdate() }} /> - - { - nmConfig.settings.closeToTray = value + + { + nmConfig.settings.autoRecoverComponents = value forceUpdate() }} /> @@ -159,7 +161,7 @@ export default function Setting_page() { showLog(modal, rcloneInfo.process.log!) return } - showLogFromFileTail(rcloneInfo.process.logFile || '~/.netmount/log/rclone.log') + showLogFromFileTail(rcloneInfo.process.logFile || rcloneLogFile()) }}>{t('log')}): {rcloneInfo.version.version}
{ shell.open(roConfig.url.openlist) }}>Openlist( { @@ -167,9 +169,48 @@ export default function Setting_page() { showLog(modal, openlistInfo.process.log!) return } - showLogFromFileTail(openlistInfo.process.logFile || '~/.netmount/openlist/log/log.log') + showLogFromFileTail(openlistInfo.process.logFile || openlistLogFile()) }}>{t('log')}): {openlistInfo.version.version}
+ + + + @@ -188,7 +229,7 @@ export default function Setting_page() {
{ openUrlInBrowser(roConfig.url.docs) }}> {t('docs')}
- { open(roConfig.url.docs + '/license') }}> {t('licence')} + { openUrlInBrowser(roConfig.url.docs + '/license') }}> {t('licence')}
diff --git a/src/services/config.ts b/src/services/config.ts index 1162c49..da96a75 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -65,7 +65,7 @@ let nmConfig: NMConfig = { settings: { themeMode: roConfig.options.setting.themeMode.select[roConfig.options.setting.themeMode.defIndex]!, startHide: false, - closeToTray: true, + autoRecoverComponents: true, language: undefined, path: { cacheDir: undefined as string | undefined diff --git a/src/services/i18nPack.ts b/src/services/i18nPack.ts new file mode 100644 index 0000000..cb32a26 --- /dev/null +++ b/src/services/i18nPack.ts @@ -0,0 +1,57 @@ +export type I18nPack = Record; + +// Key convention for storage i18n: +// - Prefer canonical ids in code: `storage.` and `description.` +// - Canonical is: lowercase + trim + collapse whitespace +// - Keep compatibility with historical case variants via alias keys. +function normalizeStorageId(raw: string): string { + return String(raw ?? '') + .trim() + .replace(/\s+/g, ' ') + .toLowerCase(); +} + +const STORAGE_KEY_ALIASES: Record = { + 'storage.Alias': 'storage.alias', + 'storage.Crypt': 'storage.crypt', + 'storage.Dropbox': 'storage.dropbox', + 'storage.FTP': 'storage.ftp', + 'storage.Local': 'storage.local', + 'storage.Onedrive': 'storage.onedrive', + 'storage.PikPak': 'storage.pikpak', + 'storage.S3': 'storage.s3', + 'storage.SFTP': 'storage.sftp', + 'storage.SMB': 'storage.smb', + 'storage.Seafile': 'storage.seafile', + 'storage.WebDav': 'storage.webdav', +}; + +export function normalizeI18nPack(pack: I18nPack): I18nPack { + const out: I18nPack = { ...pack }; + + for (const [key, value] of Object.entries(pack)) { + if (typeof value !== 'string') continue; + + if (key.startsWith('storage.')) { + const suffix = key.slice('storage.'.length); + const canonical = `storage.${normalizeStorageId(suffix)}`; + if (!(canonical in out)) out[canonical] = value; + continue; + } + + if (key.startsWith('description.')) { + const suffix = key.slice('description.'.length); + const canonical = `description.${normalizeStorageId(suffix)}`; + if (!(canonical in out)) out[canonical] = value; + continue; + } + } + + for (const [aliasKey, canonicalKey] of Object.entries(STORAGE_KEY_ALIASES)) { + if (!(aliasKey in out) && canonicalKey in out) { + out[aliasKey] = out[canonicalKey]!; + } + } + + return out; +} diff --git a/src/type/config.d.ts b/src/type/config.d.ts index 645e69f..a062561 100644 --- a/src/type/config.d.ts +++ b/src/type/config.d.ts @@ -13,7 +13,7 @@ interface NMConfig { settings: { themeMode: 'dark' | 'light' | 'auto' | string, startHide: boolean, - closeToTray: boolean, + autoRecoverComponents: boolean, language?: string, path: { cacheDir?:string diff --git a/src/utils/netmountPaths.ts b/src/utils/netmountPaths.ts index 40decfb..3f12dd6 100644 --- a/src/utils/netmountPaths.ts +++ b/src/utils/netmountPaths.ts @@ -2,24 +2,36 @@ import { osInfo, roConfig } from '../services/config' import { formatPath } from './utils' function netmountDataDir(): string { - return formatPath(roConfig.env.path.homeDir + '/.netmount/', osInfo.osType === 'windows') + return formatPath(roConfig.env.path.homeDir + '/.netmount/', osInfo.platform === 'windows') +} + +function defaultCacheDir(): string { + return formatPath(roConfig.env.path.homeDir + '/.cache/netmount', osInfo.platform === 'windows') +} + +function rcloneConfigFile(): string { + return formatPath(netmountDataDir() + '/rclone.conf', osInfo.platform === 'windows') } function netmountLogDir(): string { - return formatPath(netmountDataDir() + '/log/', osInfo.osType === 'windows') + return formatPath(netmountDataDir() + '/log/', osInfo.platform === 'windows') } function rcloneLogFile(): string { - return formatPath(netmountLogDir() + '/rclone.log', osInfo.osType === 'windows') + return formatPath(netmountLogDir() + '/rclone.log', osInfo.platform === 'windows') } function openlistLogFile(): string { - return formatPath(netmountDataDir() + '/openlist/log/log.log', osInfo.osType === 'windows') + return formatPath(openlistDataDir() + '/log/log.log', osInfo.platform === 'windows') +} + +function openlistDataDir(): string { + return formatPath(netmountDataDir() + '/openlist/', osInfo.platform === 'windows') } function sidecarLogFile(name: string): string { const safe = (name || 'sidecar').replace(/[^\w.-]+/g, '_') - return formatPath(netmountLogDir() + `/sidecar-${safe}.log`, osInfo.osType === 'windows') + return formatPath(netmountLogDir() + `/sidecar-${safe}.log`, osInfo.platform === 'windows') } -export { netmountDataDir, netmountLogDir, rcloneLogFile, openlistLogFile, sidecarLogFile } +export { defaultCacheDir, netmountDataDir, netmountLogDir, rcloneConfigFile, rcloneLogFile, openlistDataDir, openlistLogFile, sidecarLogFile } diff --git a/src/utils/openlist/paths.ts b/src/utils/openlist/paths.ts index b4e0515..2c925ae 100644 --- a/src/utils/openlist/paths.ts +++ b/src/utils/openlist/paths.ts @@ -1,9 +1,4 @@ -import { osInfo, roConfig } from "../../services/config"; -import { formatPath } from "../utils"; - -const openlistDataDir = () => { - return formatPath(roConfig.env.path.homeDir + '/.netmount/openlist/', osInfo.osType === "windows") -} +import { openlistDataDir } from "../netmountPaths"; const addParams = (): string[] => { const params: string[] = [] diff --git a/src/utils/rclone/process.ts b/src/utils/rclone/process.ts index ce25ad7..eb09348 100644 --- a/src/utils/rclone/process.ts +++ b/src/utils/rclone/process.ts @@ -5,14 +5,10 @@ import { rclone_api_noop, rclone_api_post } from "./request"; import { formatPath, getAvailablePorts } from "../utils"; import { openlistInfo } from "../../services/openlist"; import { delStorage } from "../../controller/storage/storage"; -import { nmConfig, osInfo, roConfig } from "../../services/config"; -import { netmountLogDir, rcloneLogFile } from "../netmountPaths"; +import { nmConfig, osInfo } from "../../services/config"; +import { netmountLogDir, rcloneConfigFile, rcloneLogFile } from "../netmountPaths"; import { restartSidecar, startSidecarAndWait, stopSidecarGracefully } from "../sidecarService"; -const rcloneDataDir = () => { - return formatPath(roConfig.env.path.homeDir + '/.netmount/', osInfo.osType === "windows") -} - async function startRclone() { if (rcloneInfo.process.child) { await stopRclone() @@ -42,7 +38,7 @@ async function startRclone() { `--rc-user=${nmConfig.framework.rclone.user}`, `--rc-pass=${nmConfig.framework.rclone.password}`, '--rc-allow-origin=' + window.location.origin || '*', - '--config=' + formatPath(rcloneDataDir() + '/rclone.conf', osInfo.osType === "windows"), + `--config=${rcloneConfigFile()}`, '--cache-dir=' + rcloneInfo.localArgs.path.tempDir, `--log-file=${logFile}`, '--log-level=INFO'