feat: 添加国际化检查脚本和诊断导出功能

新增国际化检查脚本 check-i18n.mjs 用于验证本地化文件的完整性
添加诊断导出功能,支持导出应用日志和配置信息到 zip 文件
更新多个语言文件,补充缺失的翻译项和描述
调整 CI 工作流中 pnpm 和 node 的安装顺序
This commit is contained in:
VirtualHotBar
2026-02-18 00:21:46 +08:00
parent 0b5e8c9e8b
commit 59aaae38d6
23 changed files with 743 additions and 119 deletions

View File

@@ -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:

View File

@@ -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"
}
}
}

132
scripts/check-i18n.mjs Normal file
View File

@@ -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.<raw>
// - Description key: description.<id>
// - Canonical <id>: 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.<normalized id>
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);
});

View File

@@ -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"

View File

@@ -190,6 +190,8 @@
"shell:default",
"process:default",
"os:default",
"dialog:allow-open"
"dialog:default",
"dialog:allow-open",
"dialog:allow-save"
]
}

View File

@@ -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.",

View File

@@ -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允许您随时随地在任何设备上保存和管理云存储中的所有内容。",

View File

@@ -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讓您隨時隨地在任何裝置上保存和管理雲端儲存的所有內容。",

View File

@@ -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) }

View File

@@ -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<Runtime>, path: &str) -> anyhow::Result<PathBuf> {
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<Runtime>) -> anyhow::Result<PathBuf> {
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<Runtime>, 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<Vec<u8>> {
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<W: Write + Seek>(
zip: &mut zip::ZipWriter<W>,
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<W: Write + Seek>(
zip: &mut zip::ZipWriter<W>,
name: &str,
s: &str,
) -> anyhow::Result<()> {
zip_add_bytes(zip, name, s.as_bytes())
}
fn maybe_add_tail_file<W: Write + Seek>(
app: &tauri::AppHandle<Runtime>,
zip: &mut zip::ZipWriter<W>,
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<W: Write + Seek>(
app: &tauri::AppHandle<Runtime>,
zip: &mut zip::ZipWriter<W>,
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<Runtime>,
out_path: String,
) -> anyhow_tauri::TAResult<String> {
fn inner(app: &tauri::AppHandle<Runtime>, out_path: &str) -> anyhow::Result<String> {
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<String> = 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)
}

View File

@@ -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| {
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,

View File

@@ -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<string, string> = await invoke("get_language_pack");
const rawPack: Record<string, string> = await invoke("get_language_pack");
const pack = normalizeI18nPack(rawPack);
i18n.addResourceBundle(lang, "translation", pack)
i18n.changeLanguage(lang);
hooks.setLocaleStr(getLangCode(lang));

View File

@@ -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 }
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 }

View File

@@ -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<StorageInfoType[
};
openlistStorageInfoList.push({
label: 'storage.' + config.name,
label: 'storage.' + normalizeStorageId(config.name ?? driverName),
type: driverName,
description: 'description.' + driverName.toLocaleLowerCase(),
description: 'description.' + normalizeStorageId(driverName),
framework: 'openlist',
defaultParams: {
name: config.name + '_new',
@@ -318,4 +322,4 @@ async function updateOpenlistStorageInfoListFallback(): Promise<StorageInfoType[
}
export { updateOpenlistStorageInfoList }
export { updateOpenlistStorageInfoList }

View File

@@ -1,6 +1,10 @@
import { FilterType, ParamItemOptionType, StorageInfoType, StorageParamItemType, RcloneProvider } from "../../../../type/controller/storage/info";
import { rclone_api_post } from "../../../../utils/rclone/request";
function normalizeStorageId(raw: string): string {
return String(raw ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
}
async function updateRcloneStorageInfoList() {
const response = await rclone_api_post('/config/providers');
const providers = (response?.providers as RcloneProvider[]) || []
@@ -125,9 +129,9 @@ async function updateRcloneStorageInfoList() {
rcloneStorageInfoList.push({
label: 'storage.'+ provider.Prefix,//provider.Name
label: 'storage.' + normalizeStorageId(provider.Prefix),//provider.Name
type: provider.Prefix,
description: 'description.'+provider.Prefix,//provider.Description
description: 'description.' + normalizeStorageId(provider.Prefix),//provider.Description
framework:'rclone',
defaultParams: {
name: provider.Name + '_new',
@@ -140,4 +144,4 @@ async function updateRcloneStorageInfoList() {
}
export{ updateRcloneStorageInfoList }
export{ updateRcloneStorageInfoList }

View File

@@ -1,15 +1,12 @@
import { nmConfig, saveNmConfig } from "../services/config";
import { saveNmConfig } from "../services/config";
import { webviewWindow } from "@tauri-apps/api";
export const window = webviewWindow.getCurrentWebviewWindow()
function listenWindow() {
// Close behavior is handled in Rust to ensure it works even if the frontend is busy.
// Keep this listener only for "hide on close" legacy behavior when enabled.
window.listen('tauri://close-requested', () => {
if (nmConfig.settings.closeToTray) {
windowsHide();
}
windowsHide();
return false
})

View File

@@ -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()
}} />
</FormItem>
<FormItem label={t('close_to_tray')}>
<Switch checked={nmConfig.settings.closeToTray} onChange={(value) => {
nmConfig.settings.closeToTray = value
<FormItem label={t('auto_recover_components')}>
<Switch checked={nmConfig.settings.autoRecoverComponents} onChange={(value) => {
nmConfig.settings.autoRecoverComponents = value
forceUpdate()
}} />
</FormItem>
@@ -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')}</Link>): {rcloneInfo.version.version}
<br />
<Link onClick={() => { shell.open(roConfig.url.openlist) }}>Openlist</Link>(<Link onClick={() => {
@@ -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')}</Link>): {openlistInfo.version.version}
<br />
<Space style={{ marginTop: '0.5rem' }}>
<Button onClick={async () => {
const dir = netmountLogDir()
if (osInfo.platform === 'windows') {
const ok = await showPathInExplorer(dir, true)
if (!ok) {
Message.error(dir)
}
} else {
Message.info(dir)
}
}}>{t('open_log_dir')}</Button>
<Button onClick={async () => {
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-')
const path = await dialog.save({
title: t('export_diagnostics'),
defaultPath: `netmount-diagnostics-${ts}.zip`,
filters: [{ name: 'Zip', extensions: ['zip'] }],
})
if (!path) return
const out = await invoke<string>('export_diagnostics', { outPath: path })
Message.success(`${t('diagnostics_exported')}: ${out}`)
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
}
}}>{t('export_diagnostics')}</Button>
</Space>
</Card>
<Card title={t('about')} style={{}} size='small'>
<Row >
@@ -188,7 +229,7 @@ export default function Setting_page() {
<br />
<Link onClick={() => { openUrlInBrowser(roConfig.url.docs) }}> {t('docs')} </Link>
<br />
<Link onClick={() => { open(roConfig.url.docs + '/license') }}> {t('licence')} </Link>
<Link onClick={() => { openUrlInBrowser(roConfig.url.docs + '/license') }}> {t('licence')} </Link>
<br />
</Col>
</Row>

View File

@@ -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

57
src/services/i18nPack.ts Normal file
View File

@@ -0,0 +1,57 @@
export type I18nPack = Record<string, string>;
// Key convention for storage i18n:
// - Prefer canonical ids in code: `storage.<id>` and `description.<id>`
// - Canonical <id> 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<string, string> = {
'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;
}

View File

@@ -13,7 +13,7 @@ interface NMConfig {
settings: {
themeMode: 'dark' | 'light' | 'auto' | string,
startHide: boolean,
closeToTray: boolean,
autoRecoverComponents: boolean,
language?: string,
path: {
cacheDir?:string

View File

@@ -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 }

View File

@@ -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[] = []

View File

@@ -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'