diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..cceffdb --- /dev/null +++ b/src/app.tsx @@ -0,0 +1,284 @@ +import React, { useEffect, useState } from 'react' +import { Layout, Menu, Breadcrumb, Button, Message, Grid, ConfigProvider } from '@arco-design/web-react'; +import "@arco-themes/react-vhbs/css/arco.css"; +//import "@arco-design/web-react/dist/css/arco.css"; +import { Routes, Route, Link, useNavigate, useLocation } from "react-router-dom"; +import { useTranslation } from 'react-i18next'; + +import { Test } from './controller/test'; +import { Routers } from './type/routers'; +import { Home_page } from './page/home/home'; +import { Storage_page } from './page/storage/storage'; +import { AddStorage_page } from './page/storage/add'; +import { Explorer_page } from './page/storage/explorer'; +import { Mount_page } from './page/mount/mount'; +import { Transmit_page } from './page/transmit/transmit'; +import { Task_page } from './page/task/task'; +import Setting_page from './page/setting/setting'; +import AddMount_page from './page/mount/add'; +import { IconAttachment, IconClose, IconCloud, IconHome, IconLink, IconList, IconMinus, IconSettings, IconStorage, IconSwap } from '@arco-design/web-react/icon'; +import { windowsHide, windowsMini } from './controller/window'; +import { rcloneInfo } from './services/rclone'; +import { AddTask_page } from './page/task/add'; +import { hooks } from './services/hook'; +import { getLocale } from './controller/language/language'; +import { nmConfig } from './services/config'; +import { getLangCode } from './controller/language/localized'; +import { Locale } from '@arco-design/web-react/es/locale/interface'; + +const { Item: MenuItem, SubMenu } = Menu; +const { Sider, Header, Content, Footer } = Layout; +const Row = Grid.Row; +const Col = Grid.Col; + + +//递归查询对应的路由 +function searchRoute( + path: string, + routes: Routers[] +): Routers | null { + for (let item of routes) { + if (item.path === path) { + return item; + } + if (item.children) { + const found = searchRoute(path, item.children); + if (found) { + return found; // 当在子路由中找到匹配项时,直接返回 + } + } + } + return null; +} + +//生成菜单 +function mapMenuItem(routes: Routers[]): JSX.Element { + return <>{ + routes.map((item) => { + if (item.hide) { + return <> + } else if (item.children && item.children.length > 0 && !item.hideChildren) { + return ({mapMenuItem(item.children)}) + } else { + return ( {item.title}) + } + }) + } + +} + +//生成页面 +function mapRouters(routes: Routers[]): JSX.Element { + return <>{ + routes.map((item) => { // 添加index作为map方法的第二个参数,用于生成唯一键 + if (item.children && item.children.length > 0) { + return {/* 给包含子路由的Fragment添加一个唯一的key */} + {mapRouters(item.children)} + {item.component ? : <>} + ; + } else { + return ; + } + }) + } +} + +//生成面包屑 +function generateBreadcrumb(pathname: string, routes: Routers[]): JSX.Element[] { + const pathSnippets = pathname.split('/').filter(i => i); + const breadcrumbItems: JSX.Element[] = []; + + if (pathSnippets.length == 1) { + return []; + } + // 创建面包屑项(根据是否有子菜单和是否隐藏子菜单决定是否为链接) + function createBreadcrumbItem(route: Routers): JSX.Element { + if (route.children && route.children.length > 0 && !route.hideChildren) { + return <>{route.title}; + } else { + return {route.title}; + } + } + + pathSnippets.reduce((prevPath, pathSnippet) => { + const currentPath = `${prevPath}/${pathSnippet}`; + const route = searchRoute(currentPath, routes); + + let breadcrumbItem: JSX.Element; + + if (route) { + breadcrumbItem = ( + + {createBreadcrumbItem(route)} + + ); + } else { + breadcrumbItem = ( + + {pathSnippet} + + ); + } + breadcrumbItems.push(breadcrumbItem); + return currentPath; + }, ''); + + return breadcrumbItems; +} + + +function App() { + //const [router, setRouter] = useState(); + const navigate = useNavigate(); + const location = useLocation(); + const { t } = useTranslation() + const [localeStr, setLocaleStr] = useState(getLangCode(nmConfig.settings.language!)) + const [selectedKeys, setSelectedKeys] = useState(['/']); + + + const routers: Array = [ + { + title: <>{t('home')}, + path: '/', + component: , + }, + { + title: <>{t('storage')}, + path: '/storage', + children: [ + { + title: t('manage'), + path: '/storage/manage', + component: , + hideChildren: true, + children: [ + { + title: t('add'), + path: '/storage/manage/add', + key: '/storage/manage',//因为父菜单隐藏了子菜单项,在此页面时设置父菜单key以选择父菜单项 + component: , + } + ] + }, + { + title: t('explorer'), + path: '/storage/explorer', + component: + } + ] + }, { + title: <>{t('mount')}, + path: '/mount', + component: , + hideChildren: true, + children: [ + { + title: t('add'), + path: '/mount/add', + key: '/mount',//因为父菜单隐藏了子菜单项,在此页面时设置父菜单key以选择父菜单项 + component: , + } + ] + }, + { + title: <>{t('transmit')} /* +(rcloneInfo.stats.transferring? '(' + rcloneInfo.stats.transferring.length + ')': '') */, + path: '/transmit', + component: , + }, + { + title: <>{t('task')}, + path: '/task', + component: , + hideChildren: true, + children: [ + { + title: t('add'), + path: '/task/add', + key: '/task',//因为父菜单隐藏了子菜单项,在此页面时设置父菜单key以选择父菜单项 + component: , + } + ] + }, + { + title: <>{t('setting')}, + path: '/setting', + component: , + } + ] + + useEffect(() => { + hooks.setLocaleStr = setLocaleStr + }, []) + + useEffect(() => { + + hooks.navigate = (path: string) => { + if (path != location.pathname) { + location.pathname.includes('add') && Message.warning(t('prompt_for_leaving_the_add_or_edit_page')) + navigate(path) + } + } + + //setRouter(searchRoute(location.pathname, routers)); + const route = searchRoute(location.pathname, routers); + if (route) { + if (route.key) { + setSelectedKeys([route.key]); + } else { + setSelectedKeys([route.path]); + } + } + }, [location]); + + + /* + +
Header
+ + Sider + Content + +
Footer
+
*/ + return ( + + +
+ + + + NetMount + + +
+ + + + { + hooks.navigate(path) + }} + >{mapMenuItem(routers)} + + + {/* {generateBreadcrumb(location.pathname, routers)} */} + {mapRouters(routers)} + + +
+
+ ) +} + +export { App } diff --git a/src/assets/font/HarmonyOS_Sans_Bold.ttf b/src/assets/font/HarmonyOS_Sans_Bold.ttf new file mode 100644 index 0000000..8ea60c1 Binary files /dev/null and b/src/assets/font/HarmonyOS_Sans_Bold.ttf differ diff --git a/src/assets/font/HarmonyOS_Sans_Regular.ttf b/src/assets/font/HarmonyOS_Sans_Regular.ttf new file mode 100644 index 0000000..d7eccac Binary files /dev/null and b/src/assets/font/HarmonyOS_Sans_Regular.ttf differ diff --git a/src/assets/font/seguiemj.woff2 b/src/assets/font/seguiemj.woff2 new file mode 100644 index 0000000..7e9dd7a Binary files /dev/null and b/src/assets/font/seguiemj.woff2 differ diff --git a/src/controller/errorHandling.ts b/src/controller/errorHandling.ts new file mode 100644 index 0000000..c3cff1e --- /dev/null +++ b/src/controller/errorHandling.ts @@ -0,0 +1,49 @@ +//日志处理(错误处理) +import { Modal } from "@arco-design/web-react"; +import { t } from "i18next"; +import { ReactNode } from "react"; +window.onerror = async function (msg, url, lineNo, columnNo, error) { + let message = [ + 'Message: ' + msg, + 'URL: ' + url, + 'Line: ' + lineNo, + 'Column: ' + columnNo, + 'Error object: ' + JSON.stringify(error) + ].join(' - '); + + await errorThrowToUser(message) + return false; +}; + +window.addEventListener('unhandledrejection', async function (event) { + await errorThrowToUser(event.reason) +}); + +window.addEventListener('error', async (event) => { + await errorThrowToUser(event.message) +}, true); + +async function errorThrowToUser(message: string) { + //排除这个错误 + if (message.toString().includes('ResizeObserver')) { return } + + let content = t('error_tips') + ',Error:' + message + + //提示错误 + await errorDialog(t('error'), content) +} +//错误对话框 +function errorDialog(title: string, content: ReactNode) { + return new Promise((resolve) => { + Modal.error( + { + title: title, + content: content, + onOk: () => { resolve(true) }, + onCancel: () => { resolve(false) }, + maskClosable: false, + closable: false + } + ) + }) +} \ No newline at end of file diff --git a/src/controller/language/language.ts b/src/controller/language/language.ts new file mode 100644 index 0000000..779b590 --- /dev/null +++ b/src/controller/language/language.ts @@ -0,0 +1,45 @@ +import zhCN from '@arco-design/web-react/es/locale/zh-CN'; +import enUS from '@arco-design/web-react/es/locale/en-US'; +import jaJP from '@arco-design/web-react/es/locale/ja-JP'; +import koKR from '@arco-design/web-react/es/locale/ko-KR'; +import idID from '@arco-design/web-react/es/locale/id-ID'; +import thTH from '@arco-design/web-react/es/locale/th-TH'; +import zhHK from '@arco-design/web-react/es/locale/zh-HK'; +import frFR from '@arco-design/web-react/es/locale/fr-FR'; +import esES from '@arco-design/web-react/es/locale/es-ES'; +import deDE from '@arco-design/web-react/es/locale/de-DE'; +import itIT from '@arco-design/web-react/es/locale/it-IT'; +import viVN from '@arco-design/web-react/es/locale/vi-VN'; +import { Locale } from '@arco-design/web-react/es/locale/interface'; + +function getLocale(locale:string):Locale { + switch (locale) { + case 'zh-cn': + return zhCN; + case 'en-us': + return enUS; + case 'ja-jp': + return jaJP; + case 'ko-kr': + return koKR as unknown as Locale; + case 'id-id': + return idID as unknown as Locale; + case 'th-th': + return thTH as unknown as Locale; + case 'zh-hk': + return zhHK; + case 'fr-fr': + return frFR as unknown as Locale; + case 'es-es': + return esES as unknown as Locale; + case 'de-de': + return deDE as unknown as Locale; + case 'it-it': + return itIT as unknown as Locale; + case 'vi-vn': + return viVN as unknown as Locale; + default: + return zhCN; + } +} +export{getLocale} \ No newline at end of file diff --git a/src/controller/language/localized.ts b/src/controller/language/localized.ts new file mode 100644 index 0000000..bc70b34 --- /dev/null +++ b/src/controller/language/localized.ts @@ -0,0 +1,33 @@ +//本地化 + +import { invoke } from "@tauri-apps/api"; +import { t } from "i18next"; +import i18n from "../../services/i18n"; +import { roConfig } from "../../services/config"; +import { hooks } from "../../services/hook"; + +async function setLocalized(lang: string) { + lang = lang.toLowerCase() + + hooks.setLocaleStr(getLangCode(lang)) + + i18n.changeLanguage(lang) + await invoke('set_localized', { + localizedData: { + quit: t("quit"), + show: t("tray_show"), + hide: t("tray_hide") + } + }) +} + +function getLangCode(lang: string): string { + for (const value of roConfig.options.setting.language.select) { + if (lang === value.value) { + return value.langCode + } + } + return roConfig.options.setting.language.select[roConfig.options.setting.language.defIndex].langCode +} + +export { setLocalized, getLangCode } \ No newline at end of file diff --git a/src/controller/language/zh-cn.json b/src/controller/language/zh-cn.json new file mode 100644 index 0000000..be01c58 --- /dev/null +++ b/src/controller/language/zh-cn.json @@ -0,0 +1,265 @@ +{ + "starting": "正在启动", + "home": "首页", + "name": "名称", + "type": "类型", + "storage": "存储", + "explorer": "浏览", + "setting": "设置", + "add": "添加", + "refresh": "刷新", + "delete": "删除", + "manage": "管理", + "storage_type": "存储类型", + "please_select": "请选择", + "Webdav_introduce": "WebDAV是一组基于超文本传输协议的技术集合,有利于用户间协同编辑和管理存储在万维网服务器文档。", + "url": "地址", + "vendor": "提供商", + "user": "用户", + "pass": "密码", + "save": "保存", + "step_back": "返回", + "step_next": "下一步", + "storage_introduce": "存储介绍", + "show_advanced_options": "显示高级选项", + "please_input": "请输入", + "Input_and_press_enter": "输入并回车", + "missing_parameter": "缺少参数", + "StorageName": "存储名称", + "storage_name_illegal": "存储名称不合法", + "Storage_added_successfully": "存储添加成功", + "Storage_added_failed": "存储添加失败", + "edit": "编辑", + "please_select_storage": "请选择存储", + "actions": "操作", + "size": "大小", + "modified_time": "修改时间", + "mount": "挂载", + "task": "任务", + "transmit": "传输", + "error": "错误", + "success": "成功", + "dev_tips": "正在开发,敬请期待", + "confirm_delete_question": "确定要删除吗?", + "dir": "目录", + "create_directory": "创建目录", + "dir_name_cannot_empty": "目录名不能为空", + "upload_file": "上传文件", + "show_all_options": "显示所有选项", + "path": "路径", + "mount_path": "挂载路径", + "auto_drive_letter": "自动分配盘符", + "simulate_hard_drive": "模拟本地硬盘", + "read_only": "只读", + "mount_options": "挂载选项", + "mount_storage_successfully": "挂载存储成功", + "mounted_time": "挂载时间", + "storage_name": "存储名称", + "unmount": "卸载", + "mount_path_already_exists": "挂载路径已存在", + "auto_mount": "软件启动时挂载", + "mounted": "已挂载", + "unmounted": "未挂载", + "mount_status": "挂载状态", + "no_data": "暂无数据", + "transferring": "传输中", + "overview": "总览", + "transferred": "已传输", + "used_time": "用时", + "time": "时间", + "speed": "速度", + "eta": "剩余时间", + "speed_avg": "平均速度", + "target": "目标", + "source": "来源", + "read_config": "读取配置", + "init": "初始化", + "transm_task_created": "已创建任务,请到[传输]页查看信息", + "clip_board": "剪切板", + "paste": "粘贴", + "empty_the_clipboard": "清空剪贴板", + "parent_directory": "上级目录", + "cut": "剪切", + "more": "更多", + "copy": "复制", + "rename": "重命名", + "name_cannot_empty": "名称不能为空", + "task_name": "任务名称", + "state": "状态", + "enabled": "启用", + "disabled": "禁用", + "cycle": "周期", + "run_info": "运行信息", + "task_type": "任务类型", + "task_run_mode_start": "启动时", + "task_run_mode_start_opt": "启动时(软件启动时执行)", + "task_run_mode_time": "定时", + "task_run_mode_time_opt": "定时(在特定时间执行)", + "task_run_mode_interval": "间隔", + "task_run_mode_interval_opt": "间隔(每隔一段时间执行)", + "task_run_mode_disposable": "一次性", + "task_run_mode_disposable_opt": "一次性(添加后立即执行,并自动删除任务)", + "task_run_mode": "执行模式", + "move": "移动", + "sync": "同步", + "source_path": "源路径", + "target_path": "目标路径", + "prompt_for_leaving_the_add_or_edit_page": "离开添加或编辑页面,未保存的设置将丢失。", + "explain_for_task_path_format": "路径格式:目录以斜杠结尾,文件以不以斜杠结尾", + "interval": "间隔", + "day": "天", + "week": "周", + "month": "月", + "hour": "小时", + "minute": "分钟", + "second": "秒", + "the_task_name_is_illegal": "任务名称不合法", + "task_added_successfully": "任务添加成功", + "the_path_is_illegal": "路径不合法", + "same_source_and_target": "源和目标相同", + "bisync": "双向同步", + "add_storage": "添加存储", + "add_mount": "添加挂载", + "add_task": "添加任务", + "auto_themeMode": "跟随系统", + "light_themeMode": "浅色模式", + "dark_themeMode": "深色模式", + "theme_mode": "主题模式", + "tools": "工具", + "encoding": "编码", + "headers": "标头", + "bearer_token_command": "Bearer token命令", + "pacer_min_sleep": "最小休眠时间", + "nextcloud_chunk_size": "Nextcloud块大小", + "owncloud_exclude_shares": "OwnCloud排除共享", + "description": "说明", + "VolumeName": "卷名", + "AllowNonEmpty": "允许非空", + "AllowOther": "允许其他", + "AllowRoot": "允许Root", + "AsyncRead": "异步读取", + "AttrTimeout": "属性超时", + "Daemon": "守护进程", + "DaemonTimeout": "守护进程超时", + "DefaultPermissions": "默认权限", + "ExtraFlags": "附加参数", + "ExtraOptions": "附加选项", + "MaxReadAhead": "最大预读", + "NoAppleDouble": "无AppleDouble", + "NoAppleXattr": "无AppleXattr", + "WritebackCache": "写回缓存", + "DaemonWait": "守护程序等待", + "DeviceName": "设备名", + "NetworkMode": "挂载为网络驱动器", + "ReadOnly": "只读", + "CacheMaxAge": "缓存最大有效期", + "CacheMaxSize": "缓存最大大小", + "CacheMode": "缓存模式", + "CachePollInterval": "缓存轮询间隔", + "CaseInsensitive": "不区分大小写", + "ChunkSize": "块大小", + "ChunkSizeLimit": "块大小限制", + "DirCacheTime": "目录缓存时间", + "DirPerms": "目录权限", + "FilePerms": "文件权限", + "NoChecksum": "无校验和", + "NoModTime": "忽略文件修改时间", + "NoSeek": "禁用文件定位", + "PollInterval": "轮询间隔", + "ReadAhead": "读取预取大小", + "ReadWait": "读取等待时间", + "WriteBack": "写回策略", + "WriteWait": "写入等待时间", + "Refresh": "刷新", + "BlockNormDupes": "阻止重复块的规范化", + "UsedIsSize": "将已使用空间视为文件大小", + "FastFingerprint": "快速指纹计算", + "DiskSpaceTotalSize": "磁盘总容量", + "UID": "UID", + "GID": "GID", + "Umask": "文件权限掩码", + "about": "关于", + "autostart": "开机自启", + "install": "安装", + "winfsp_not_installed": "需要安装依赖(WinFsp),才能挂载存储。", + "install_failed": "安装失败", + "install_success": "安装成功", + "components": "组件", + "log": "日志", + "start_hide": "启动时隐藏窗口", + "quit": "退出", + "tray_show": "显示主窗口", + "tray_hide": "隐藏主窗口", + "language": "语言", + "update_available": "发现新版本", + "current_version": "当前版本", + "latest_version": "最新版本", + "goto_the_website_get_latest_version_ask": "是否前往官网获取最新版?", + "netmount_slogan": "统一管理和挂载云存储设施", + "please_add_storage_tip": "当前无可用存储,请添加存储", + "transmission_overview": "传输概览", + "view_more": "详细", + "technology_stack": "技术栈", + "about_text": "由独立开发者 VirtualHotBar 开发并发布", + "licence": "许可证", + "error_tips": "请尝试重启程序,并记录控制台错误信息向开发者反馈", + "host": "主机", + "port": "端口", + "tls": "传输加密(TLS)", + "explicit_tls": "显式TLS", + "client_id": "客户端ID", + "client_secret": "客户端秘钥", + "region": "区域", + "global": "全球版", + "us": "美国版", + "de": "德国版", + "cn": "中国版", + "documentLibrary": "SharePoint(文档库)", + "personal": "个人版", + "business": "商业版", + "drive_type": "类型", + "token": "Token", + "root_folder_id": "根目录ID", + "drive_id": "驱动器ID", + "local_introduce": "本地存储", + "provider": "提供商", + "bucket": "桶", + "access_key_id": "Access Key ID", + "secret_access_key": "Secret Access Key", + "endpoint": "Endpoint", + "acl": "ACL", + "env_auth": "环境认证", + "location_constraint": "位置约束", + "storage_class": "存储类别", + "server_side_encryption": "服务器端加密", + "sse_kms_key_id": "SSE-KMS密钥ID", + "remote": "存储", + "filename_encryption": "文件名加密", + "directory_name_encryption": "目录名加密", + "password": "密码", + "password2": "加盐", + "key": "秘钥", + + "ftp_introduce": "FTP(File Transfer Protocol)是一种广泛应用的网络协议,旨在通过客户端-服务器架构实现互联网上可靠、交互式的文件传输、共享与管理。", + "onedrive_introduce": "OneDrive 是微软提供的云存储服务,具备跨平台文件同步、分享功能,用户可从任何设备安全访问文档、照片及文件夹,同时提供付费方案以获得更大存储空间。", + "s3_introduce": "S3协议是Amazon定义的一套用于在云环境中进行对象存储操作的标准API接口,支持包括上传、下载、检索、管理对象及其元数据在内的多种功能,已被众多云存储提供商广泛采用作为兼容接口。", + "googledrive_introduce": "Google Drive是Google提供的云存储服务,集成于Google生态系统,支持文档协作、备份和共享。", + "dropbox_introduce": "Dropbox是知名个人与团队文件同步、备份和共享平台,具有跨设备访问和版本控制功能。", + "webdav_introduce": "WebDAV是一种基于HTTP协议的网络文件系统标准,允许用户通过标准文件操作(如复制、移动、删除)远程管理服务器上的文件。", + "box_introduce": "Box是企业级云内容管理平台,专注于安全文件共享、协作和流程自动化。", + "googlecloudstorage_introduce": "Google Cloud Storage是谷歌云平台的云存储服务,提供多种存储级别,适用于不同性能、成本和持久性需求。", + "http_introduce": "HTTP在此处可能指通过HTTP协议直接访问存储在Web服务器上的静态文件。", + "swift_introduce": "OpenStack Object Storage (Swift)是开源云存储项目,提供大规模、弹性、API可编程的对象存储服务。", + "jottacloud_introduce": "Jottacloud是挪威云备份和同步服务,注重数据隐私和安全性,提供无限存储计划。", + "mega_introduce": "Mega强调用户隐私保护的云存储服务,提供端到端加密和大容量存储选项。", + "opendrive_introduce": "OpenDrive提供文件备份、同步、共享及在线办公套件的全方位云存储解决方案。", + "pcloud_introduce": "Pcloud是云存储服务商,提供跨平台文件访问、自动照片备份及音乐播放等功能。", + "qingstor_introduce": "Qingstor是青云QingCloud的对象存储服务,具备高性能、高可靠性和丰富的数据处理能力。", + "sftp_introduce": "SFTP是安全文件传输协议,基于SSH,提供加密的文件访问、传输和管理功能。", + "yandex_introduce": "Yandex Disk是Yandex提供的云存储服务,支持文件同步、备份与分享,以及与Yandex生态系统的集成。", + "crypt_introduce": "加密,确保用户数据在云端存储时的安全性。", + "alias_introduce": "别名,便于访问和管理。", + "alist_introduce":"一个支持多种存储的文件列表程序,使用 Gin 和 Solidjs。", + "storage_name_already_exists":"存储名称已存在" + +} \ No newline at end of file diff --git a/src/controller/main.ts b/src/controller/main.ts new file mode 100644 index 0000000..40ff7a7 --- /dev/null +++ b/src/controller/main.ts @@ -0,0 +1,82 @@ +import { invoke, process } from "@tauri-apps/api" +import { nmConfig, readNmConfig, roConfig, saveNmConfig, setNmConfig } from "../services/config" +import { rcloneInfo } from "../services/rclone" +import { rclone_api_post } from "../utils/rclone/request" +import { startUpdateCont } from "./stats/continue" +import { reupMount } from "./storage/mount/mount" +import { reupStorage } from "./storage/storage" +import { listenWindow, windowsHide } from "./window" +import { NMConfig } from "../type/config" +import { randomString } from "../utils/utils" +import { t } from "i18next" +import { startRclone, stopRclone } from "../utils/rclone/process" +import { getOsInfo } from "../utils/tauri/osInfo" +import { startTaskScheduler } from "./task/task" +import { autoMount } from "./task/autoMount" +import { setThemeMode } from "./setting/setting" +import { setLocalized } from "./language/localized" +import { checkNotice } from "./update/notice" + +async function init(setStartStr: Function) { + + setStartStr(t('init')) + + listenWindow() + + await getOsInfo() + + setStartStr(t('read_config')) + await readNmConfig() + + + if (nmConfig.settings.startHide) { + windowsHide() + } + + if (nmConfig.settings.language) { + await setLocalized(nmConfig.settings.language); + } else { + const matchingLang = roConfig.options.setting.language.select.find( + (lang) => lang.langCode === navigator.language.toLowerCase() + ); + nmConfig.settings.language = matchingLang?.value || roConfig.options.setting.language.select[roConfig.options.setting.language.defIndex].value; + await setLocalized(nmConfig.settings.language); + } + + setThemeMode(nmConfig.settings.themeMode) + + await checkNotice() + + await startRclone() + + startUpdateCont() + await reupRcloneVersion() + await reupStorage() + await reupMount() + + //自动挂载 + await autoMount() + + //开始任务队列 + await startTaskScheduler() +} + +async function reupRcloneVersion() { + const ver = await rclone_api_post( + '/core/version', + ) + rcloneInfo.version = ver + console.log(rcloneInfo.version); +} + +function main() { + +} + +async function exit() { + await stopRclone() + await saveNmConfig() + await process.exit(); +} + +export { init, main, exit } \ No newline at end of file diff --git a/src/controller/setting/setting.ts b/src/controller/setting/setting.ts new file mode 100644 index 0000000..603c310 --- /dev/null +++ b/src/controller/setting/setting.ts @@ -0,0 +1,45 @@ +import { invoke } from "@tauri-apps/api"; +import { nmConfig } from "../../services/config"; + +// 设置颜色模式 +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {//监听 + nmConfig.settings.themeMode === 'auto' && setThemeMode(nmConfig.settings.themeMode) +}); +function setThemeMode(mode: 'dark' | 'light' | 'auto' | string): void { + const body = document.body; + let isDarkMode: boolean = false; + + switch (mode) { + case 'dark': + isDarkMode = true; + break; + case 'light': + isDarkMode = false; + break; + case 'auto': + isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; + break; + } + + + // 根据模式设置页面主题和背景颜色 + if (isDarkMode) { + document.body.setAttribute('arco-theme', 'dark'); + body.style.backgroundColor = "#2E2E2E"; + } else { + document.body.removeAttribute('arco-theme'); + body.style.backgroundColor = "#FFFFFF"; + } +} + +//获取是否启用自启 +async function getAutostartState(): Promise { + return (await invoke('get_autostart_state')) as boolean; +} + +//设置自启 +async function setAutostartState(state: boolean): Promise { + return (await invoke('set_autostart_state',{enabled:state})) as boolean; +} + +export { setThemeMode ,getAutostartState,setAutostartState} diff --git a/src/controller/stats/continue.ts b/src/controller/stats/continue.ts new file mode 100644 index 0000000..21ec026 --- /dev/null +++ b/src/controller/stats/continue.ts @@ -0,0 +1,18 @@ +import { reupStats } from "./stats"; + + +function startUpdateCont() { + const intervalId = setInterval(async () => { + try { + await reupStats(); + } catch (error) { + // 处理错误,例如记录日志或清理状态 + console.error('Error occurred while updating stats:', error); + } + }, 500); // 每n毫秒调用一次 + + // 返回清除定时器的函数,方便在需要停止更新时调用 + return () => clearInterval(intervalId); +} + +export { startUpdateCont } \ No newline at end of file diff --git a/src/controller/stats/stats.ts b/src/controller/stats/stats.ts new file mode 100644 index 0000000..f983bdd --- /dev/null +++ b/src/controller/stats/stats.ts @@ -0,0 +1,34 @@ +import { hooks } from "../../services/hook"; +import { rcloneInfo, rcloneStatsHistory } from "../../services/rclone"; +import { RcloneStats } from "../../type/rclone/stats"; +import { rclone_api_post } from "../../utils/rclone/request"; + +async function reupStats() { + const stats: RcloneStats = await rclone_api_post( + '/core/stats', + ) + + let realSpeed: number = 0 + + if (stats.transferring && stats.transferring.length > 0) { + stats.transferring.forEach(item => { + realSpeed += item.speed + }) + } + + rcloneInfo.stats = { + ...stats, + realSpeed: realSpeed + } + + //历史状态 + rcloneStatsHistory.push(stats) + + if (rcloneStatsHistory.length > 32) { + rcloneStatsHistory.splice(0, rcloneStatsHistory.length - 32); + } + + hooks.upStats() +} + +export { reupStats } \ No newline at end of file diff --git a/src/controller/storage/create.ts b/src/controller/storage/create.ts new file mode 100644 index 0000000..63b19d7 --- /dev/null +++ b/src/controller/storage/create.ts @@ -0,0 +1,52 @@ + +import { DefaultParams } from "../../type/rclone/storage/defaults"; +import { rclone_api_post } from "../../utils/rclone/request"; +import { isEmptyObject } from "../../utils/utils"; +import { reupStorage } from "./storage"; + + +async function createStorage(name: string, type: string, parameters: object) { + + const back = await rclone_api_post("/config/create", { + "name": name, + "type": type, + "parameters": parameters + }) + + reupStorage() + + return isEmptyObject(back); +} + +//检查必填参数的合法性 +function checkParams(storageName: string, parameters: { [key: string]: any }, defaultParams: DefaultParams, t?: Function): { isOk: boolean, msg: string } { + let isOk = true; + let msg = ''; + + if (!t) { + t = (v: string) => { return v } + } + + if (!storageName) { + isOk = false; + msg += `${t('storage_name_illegal')}`; + } + + if (isOk) { + for (const param of defaultParams.required) { + if (!parameters[param]) { + isOk = false; + msg += `${t('missing_parameter')}:${t(param)}`; + break + } + } + } + + // 返回结果前清理末尾的换行符,避免多余的空白 + msg = msg.trimEnd(); + + return { isOk: isOk, msg: msg }; +} + + +export { createStorage, checkParams } \ No newline at end of file diff --git a/src/controller/storage/listAll.ts b/src/controller/storage/listAll.ts new file mode 100644 index 0000000..ed06f9a --- /dev/null +++ b/src/controller/storage/listAll.ts @@ -0,0 +1,250 @@ +import { StorageListAll } from "../../type/rclone/storage/storageListAll"; +import { aliasDefaults } from "./parameters/defaults/alias"; +import { alistDefaults } from "./parameters/defaults/alist"; +import { boxDefaults } from "./parameters/defaults/box"; +import { cryptDefaults } from "./parameters/defaults/crypt"; +import { dropboxDefaults } from "./parameters/defaults/dropbox"; +import { ftpDefaults } from "./parameters/defaults/ftp"; +import { googleCloudStorageDefaults } from "./parameters/defaults/googleCloudStorage"; +import { googleDriveDefaults } from "./parameters/defaults/googledrive"; +import { httpDefaults } from "./parameters/defaults/http"; +import { jottacloudDefaults } from "./parameters/defaults/jottacloud"; +import { localDefaults } from "./parameters/defaults/local"; +import { megaDefaults } from "./parameters/defaults/mega"; +import { onedriveDefaults } from "./parameters/defaults/onedrive"; +import { opendriveDefaults } from "./parameters/defaults/opendrive"; +import { pcloudDefaults } from "./parameters/defaults/pcloud"; +import { qingstorDefaults } from "./parameters/defaults/qingstor"; +import { s3Defaults } from "./parameters/defaults/s3"; +import { sftpDefaults } from "./parameters/defaults/sftp"; +import { swiftDefaults } from "./parameters/defaults/swift"; +import { webdavDefaults } from "./parameters/defaults/webdav"; +import { yandexDefaults } from "./parameters/defaults/yandex"; + +const storageListAll = [ + { + name: 'Alist', + type: 'webdav', + displayType:'alist', + introduce: 'alist_introduce', + defaultParams: alistDefaults + }, + { + name: 'OneDrive', + type: 'onedrive', + introduce: 'onedrive_introduce', + defaultParams: onedriveDefaults + }, + { + name: 'WebDav', + type: 'webdav', + introduce: 'Webdav_introduce', + defaultParams: webdavDefaults + }, { + name: 'Google Drive', + type: 'drive', + introduce: 'googledrive_introduce', + defaultParams: googleDriveDefaults + }, + { + name: 'Dropbox', + type: 'dropbox', + introduce: 'dropbox_introduce', + defaultParams: dropboxDefaults + }, + { + name: 'S3 Object Storage', + type: 's3', + introduce: 's3_introduce', + defaultParams: s3Defaults + }, + { + name: 'Google Cloud Storage', + type: 'google cloud storage', + introduce: 'googlecloudstorage_introduce', + defaultParams: googleCloudStorageDefaults + }, + { + name: 'FTP', + type: 'ftp', + introduce: 'ftp_introduce', + defaultParams: ftpDefaults + }, + { + name: 'Local Disk', + type: 'local', + introduce: 'local_introduce', + defaultParams: localDefaults + }, + { + name: 'Box', + type: 'box', + introduce: 'box_introduce', + defaultParams: boxDefaults + }, + { + name: 'HTTP', + type: 'http', + introduce: 'http_introduce', + defaultParams: httpDefaults + }, + { + name: 'OpenStack Object Storage', + type: 'swift', + introduce: 'swift_introduce', + defaultParams: swiftDefaults + }, + { + name: 'Pcloud', + type: 'pcloud', + introduce: 'pcloud_introduce', + defaultParams: pcloudDefaults + }, + { + name: 'Qingstor', + type: 'qingstor', + introduce: 'qingstor_introduce', + defaultParams: qingstorDefaults + }, + { + name: 'SFTP', + type: 'sftp', + introduce: 'sftp_introduce', + defaultParams: sftpDefaults + }, + { + name: 'Yandex Disk', + type: 'yandex', + introduce: 'yandex_introduce', + defaultParams: yandexDefaults + }, + { + name: 'Mega', + type: 'mega', + introduce: 'mega_introduce', + defaultParams: megaDefaults + }, + { + name: 'OpenDrive', + type: 'opendrive', + introduce: 'opendrive_introduce', + defaultParams: opendriveDefaults + }, + { + name: 'Jottacloud', + type: 'jottacloud', + introduce: 'jottacloud_introduce', + defaultParams: jottacloudDefaults + }, + { + name: 'Crypt', + type: 'crypt', + introduce: 'crypt_introduce', + defaultParams: cryptDefaults + }, + { + name: 'Alias', + type: 'alias', + introduce: 'alias_introduce', + defaultParams: aliasDefaults + }, +]; + + +//根据标识返回StorageListAll +function searchStorage(v: string | undefined, displayType: boolean=false): StorageListAll { + for (const storageItem of storageListAll) { + if(!displayType){ + if (( storageItem.introduce === v|| storageItem.name === v||storageItem.type === v )&&!storageItem.displayType) { + return storageItem + } + }else{ + if (storageItem.introduce === v|| storageItem.name === v||storageItem.displayType === v ) { + return storageItem + } + } + } + return storageListAll[0] +} + + + +/* 根据上面内容,生成s3声明及初始值(ts),以下面的格式: + + +import { ParamsSelectType } from "../defaults"; + +interface OneDriveParamsStandard { + client_id?: string; + client_secret?: string; + region?: ParamsSelectType; +} + + +interface OneDriveParamsAdvanced { + token?: string; + auth_url?: string; + token_url?: string; + chunk_size?: string; // Assuming this to be a string for simplification + drive_id?: string; + drive_type?: ParamsSelectType; + root_folder_id?: string; + access_scopes?: string; + disable_site_permission?: boolean; + expose_onenote_files?: boolean; + server_side_across_configs?: boolean; + list_chunk?: number; + no_versions?: boolean; + link_scope?: string; + link_type?: string; + link_password?: string; + hash_type?: string; + av_override?: boolean; + delta?: boolean; + metadata_permissions?: string; + encoding?: string; + description?: string; +} +const standard: OneDriveParamsStandard = { + client_id: "", + client_secret: "", + region: { + select: 'global', + values: ['global', 'us', 'de', 'cn'] + }, +} + +const advanced: OneDriveParamsAdvanced = { + drive_type: { select: 'personal', values: ['personal', 'business','documentLibrary'] }, + token: "", + auth_url: "", + token_url: "", + chunk_size: "10Mi", + drive_id: "", + root_folder_id: "", + access_scopes: "Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access", + disable_site_permission: false, + expose_onenote_files: false, + server_side_across_configs: false, + list_chunk: 1000, + no_versions: false, + link_scope: "anonymous", + link_type: "view", + link_password: "", + hash_type: "auto", + av_override: false, + delta: false, + metadata_permissions: "off", + encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,Del,Ctl,LeftSpace,LeftTilde,RightSpace,RightPeriod,InvalidUtf8,Dot", + description: "" +} + +const onedriveDefaults: DefaultParams = { + "name": "OneDrive", + "standard": standard, + "advanced": advanced, + "required": ['client_id', 'client_secret'] +} + */ + +export { storageListAll, searchStorage } \ No newline at end of file diff --git a/src/controller/storage/mount/mount.ts b/src/controller/storage/mount/mount.ts new file mode 100644 index 0000000..cc1b4af --- /dev/null +++ b/src/controller/storage/mount/mount.ts @@ -0,0 +1,110 @@ +import { invoke } from "@tauri-apps/api" +import { nmConfig } from "../../../services/config" +import { hooks } from "../../../services/hook" +import { rcloneInfo } from "../../../services/rclone" +import { MountListItem } from "../../../type/config" +import { ParametersType } from "../../../type/rclone/storage/defaults" +import { rclone_api_post } from "../../../utils/rclone/request" + + +//列举存储 +async function reupMount(noRefreshUI?: boolean) { + const mountPoints = (await rclone_api_post( + '/mount/listmounts', + )).mountPoints + rcloneInfo.mountList = []; + + if (mountPoints) { + mountPoints.forEach((tiem: any) => { + const name = tiem.Fs + rcloneInfo.mountList.push({ + storageName: name.substring(0, name.length - 1), + mountPath: tiem.MountPoint, + mountedTime: new Date(tiem.MountedOn), + }) + }); + } + !noRefreshUI && hooks.upMount() +} + +function getMountStorage(mountPath: string): MountListItem | undefined { + return nmConfig.mount.lists.find((item) => item.mountPath === mountPath) +} + +function isMounted(mountPath: string): boolean { + return rcloneInfo.mountList.findIndex((item) => item.mountPath === mountPath) !== -1 +} + +async function addMountStorage(storageName: string, mountPath: string, parameters: ParametersType, autoMount?: boolean) { + + if (getMountStorage(mountPath)) { + return false + } + + const mountInfo: MountListItem = { + storageName: storageName, + mountPath: mountPath, + parameters: parameters, + autoMount: (autoMount || false), + } + nmConfig.mount.lists.push(mountInfo) + + await reupMount() +} + +async function delMountStorage(mountPath: string) { + if (isMounted(mountPath)) { + await unmountStorage(mountPath) + } + + nmConfig.mount.lists.forEach((item, index) => { + if (item.mountPath === mountPath) { + nmConfig.mount.lists.splice(index, 1) + } + }) + + await reupMount() +} + +async function editMountStorage(mountInfo: MountListItem) { + + await reupMount() + rcloneInfo.mountList.forEach((item) => { + if (item.mountPath === mountInfo.mountPath) { + return false + } + }) + + const index = nmConfig.mount.lists.findIndex((item) => item.mountPath === mountInfo.mountPath) + + if (index !== -1) { + nmConfig.mount.lists[index] = mountInfo + } +} + +async function mountStorage(mountInfo: MountListItem) { + + const back = await rclone_api_post('/mount/mount', { + fs: mountInfo.storageName + ":", + mountPoint: mountInfo.mountPath, + ...(mountInfo.parameters) + }) + + await reupMount() + return back +} + +async function unmountStorage(mountPath: string) { + await rclone_api_post('/mount/unmount', { + mountPoint: mountPath, + }) + + await reupMount() +} + +async function getAvailableDriveLetter(): Promise { + return await invoke('get_available_drive_letter')//Z: +} + + +export { reupMount, mountStorage, unmountStorage, addMountStorage, delMountStorage, editMountStorage, getMountStorage, isMounted,getAvailableDriveLetter } \ No newline at end of file diff --git a/src/controller/storage/mount/parameters/defaults.ts b/src/controller/storage/mount/parameters/defaults.ts new file mode 100644 index 0000000..c13c9bf --- /dev/null +++ b/src/controller/storage/mount/parameters/defaults.ts @@ -0,0 +1,67 @@ +import { MountOptions, VfsOptions } from "../../../../type/rclone/storage/mount/parameters"; + + +// 示例:初始化VfsOptions和MountOptions的默认值 +const defaultVfsConfig: VfsOptions = { + ReadOnly: false, + CacheMaxAge: 3600000000000, + CacheMaxSize: -1, + CacheMode: { + select: 'full', + values: [ + 'off', + 'minimal', + 'writes', + 'full', + ] + }, + CachePollInterval: 60000000000, + CaseInsensitive: false, + ChunkSize: 67108864, + ChunkSizeLimit: -1, + DirCacheTime: 300000000000, + DirPerms: 511, + FilePerms: 511, + NoChecksum: false, + NoModTime: false, + NoSeek: false, + PollInterval: 60000000000, + ReadAhead: 0, + ReadWait: 20000000, + WriteBack: 5000000000, + WriteWait: 1000000000, + Refresh: false, + BlockNormDupes: false, + UsedIsSize: false, + FastFingerprint: false, + DiskSpaceTotalSize: -1, + UID: 4294967295, + GID: 4294967295, + Umask: 0, + +}; + +const defaultMountConfig: MountOptions = { + VolumeName: '', + AllowNonEmpty: false, + AllowOther: false, + AllowRoot: false, + AsyncRead: true, + AttrTimeout: 1000000000, + Daemon: false, + DaemonTimeout: 0, + DebugFUSE: false, + DefaultPermissions: true, + ExtraFlags: [], + ExtraOptions: [], + MaxReadAhead: 1048576, + NoAppleDouble: true, + NoAppleXattr: false, + WritebackCache: false, + DaemonWait: 0, + DeviceName: '', + NetworkMode: false, //挂载为网络驱动器 + //CaseInsensitive: null, +}; + +export { defaultVfsConfig, defaultMountConfig } \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/alias.ts b/src/controller/storage/parameters/defaults/alias.ts new file mode 100644 index 0000000..b2ef921 --- /dev/null +++ b/src/controller/storage/parameters/defaults/alias.ts @@ -0,0 +1,21 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface AliasParamsStandard { + remote: string; +} + +const standard: AliasParamsStandard = { + remote: "", +} + +const advanced = { + +} + +const aliasDefaults: DefaultParams = { + "name": "alias", + "standard": standard, + "advanced": advanced, + "required": ['remote'] +} +export{aliasDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/alist.ts b/src/controller/storage/parameters/defaults/alist.ts new file mode 100644 index 0000000..04ccbcb --- /dev/null +++ b/src/controller/storage/parameters/defaults/alist.ts @@ -0,0 +1,24 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; +import { webdavDefaults } from "./webdav"; + +const alistDefaults: DefaultParams = { + ...webdavDefaults, + standard: { + ...webdavDefaults.standard, + "url": "http://localhost:5244/dav", + "user": "admin", + }, + advanced: { + ...webdavDefaults.advanced, + "vendor":webdavDefaults.standard.vendor + }, + required: [ + ...webdavDefaults.required, + 'user', 'pass' + ] +} + +//Reflect.deleteProperty(alistDefaults.standard, "vendor"); +delete alistDefaults.standard.vendor; + +export { alistDefaults } \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/box.ts b/src/controller/storage/parameters/defaults/box.ts new file mode 100644 index 0000000..887599a --- /dev/null +++ b/src/controller/storage/parameters/defaults/box.ts @@ -0,0 +1,30 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface BoxParamsStandard { + client_id?: string; + client_secret?: string; +} + +interface BoxParamsAdvanced { + upload_cutoff?: number; + commit_retries?: number; +} + +const standard: BoxParamsStandard = { + client_id: "", + client_secret: "" +} + +const advanced: BoxParamsAdvanced = { + upload_cutoff: 52428800, + commit_retries: 100 +} + +const boxDefaults: DefaultParams = { + "name": "Box", + "standard": standard, + "advanced": advanced, + required:['client_id','client_secret'] +} + +export{boxDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/crypt.ts b/src/controller/storage/parameters/defaults/crypt.ts new file mode 100644 index 0000000..606a00d --- /dev/null +++ b/src/controller/storage/parameters/defaults/crypt.ts @@ -0,0 +1,35 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; +import { CryptParamsAdvanced, CryptParamsStandard } from "../../../../type/rclone/storage/parameters/crypt"; + +const standard: CryptParamsStandard = { + remote: "", + filename_encryption: { + select:"standard", + values:["standard","obfuscate","off"] + }, + directory_name_encryption: true, + password: "", + password2: "" +} + +const advanced: CryptParamsAdvanced = { + server_side_across_configs: false, + show_mapping: false, + no_data_encryption: false, + pass_bad_blocks: false, + strict_names: false, + filename_encoding: { + select:'base32', + values:['base32','base64','base32768'] + }, + suffix: ".bin" +} + +const cryptDefaults: DefaultParams = { + "name": "New_Storage", + "standard": standard, + "advanced": advanced, + "required": ['remote', 'password'] +} + +export{cryptDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/dropbox.ts b/src/controller/storage/parameters/defaults/dropbox.ts new file mode 100644 index 0000000..3c1b261 --- /dev/null +++ b/src/controller/storage/parameters/defaults/dropbox.ts @@ -0,0 +1,30 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface DropboxParamsStandard { + client_id?: string; + client_secret?: string; + } + + interface DropboxParamsAdvanced { + chunk_size?: number; + impersonate?: string; + } + + const standard: DropboxParamsStandard = { + client_id: "", + client_secret: "", + } + + const advanced: DropboxParamsAdvanced = { + chunk_size: 50331648, // 48MB + impersonate: "", + } + + const dropboxDefaults: DefaultParams = { + "name": "Dropbox", + "standard": standard, + "advanced": advanced, + required:["client_id", "client_secret"] + } + + export{dropboxDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/ftp.ts b/src/controller/storage/parameters/defaults/ftp.ts new file mode 100644 index 0000000..1da2426 --- /dev/null +++ b/src/controller/storage/parameters/defaults/ftp.ts @@ -0,0 +1,40 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults" +import { FtpParamsAdvanced, FtpParamsStandard } from "../../../../type/rclone/storage/parameters/ftp" + +const standard: FtpParamsStandard = { + host: "", + user: "", + port: 21, + pass: "", + tls: false, + explicit_tls: false, +} + +const advanced: FtpParamsAdvanced = { + concurrency: 4, + no_check_certificate: false, + disable_epsv: false, + disable_mlsd: false, + disable_utf8: false, + writing_mdtm: false, + force_list_hidden: false, + idle_timeout: "1m", + close_timeout: "1m", + tls_cache_size: 16, + disable_tls13: false, + shut_timeout: "1m", + ask_password: false, + socks_proxy: "", + encoding: "Slash,Del,Ctl,RightSpace,Dot", // rclone的编码设置根据主要的FTP服务器进行选择,如ProFTPd, PureFTPd, VsFTPd等 + description: "", +} + +const ftpDefaults: DefaultParams = { + "name":"New_Storage", + "standard": standard, + "advanced": advanced, + "required": ['host','port'] +} + + + export {ftpDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/googleCloudStorage.ts b/src/controller/storage/parameters/defaults/googleCloudStorage.ts new file mode 100644 index 0000000..f9ae582 --- /dev/null +++ b/src/controller/storage/parameters/defaults/googleCloudStorage.ts @@ -0,0 +1,76 @@ +import { DefaultParams, ParamsSelectType } from "../../../../type/rclone/storage/defaults"; + + +interface GoogleCloudStorageParamsStandard { + client_id?: string; + client_secret?: string; + project_number?: string; + user_project?: string; + service_account_file?: string; + service_account_credentials?: string; + anonymous?: boolean; + object_acl?: ParamsSelectType; + bucket_acl?: ParamsSelectType; + bucket_policy_only?: boolean; + location?: string; + storage_class?: ParamsSelectType; + env_auth?: boolean; +} + +interface GoogleCloudStorageParamsAdvanced { + token?: string; + auth_url?: string; + token_url?: string; + directory_markers?: boolean; + no_check_bucket?: boolean; + decompress?: boolean; + endpoint?: string; + encoding?: string; + description?: string; +} + +const standard: GoogleCloudStorageParamsStandard = { + client_id: "", + client_secret: "", + project_number: "", + user_project: "", + service_account_file: "", + service_account_credentials: "", + anonymous: false, + object_acl: { + select: 'private', + values: ['authenticatedRead', 'bucketOwnerFullControl', 'bucketOwnerRead', 'private', 'projectPrivate', 'publicRead'] + }, + bucket_acl: { + select: 'private', + values: ['authenticatedRead', 'private', 'projectPrivate', 'publicRead', 'publicReadWrite'] + }, + bucket_policy_only: false, + location: "", + storage_class: { + select: 'STANDARD', + values: ['MULTI_REGIONAL', 'REGIONAL', 'NEARLINE', 'COLDLINE', 'ARCHIVE', 'DURABLE_REDUCED_AVAILABILITY', 'STANDARD'] + }, + env_auth: false, +} + +const advanced: GoogleCloudStorageParamsAdvanced = { + token: "", + auth_url: "", + token_url: "", + directory_markers: false, + no_check_bucket: false, + decompress: false, + endpoint: "", + encoding: "Slash,CrLf,InvalidUtf8,Dot", + description: "" +} + +const googleCloudStorageDefaults: DefaultParams = { + "name": "GoogleCloudStorage", + "standard": standard, + "advanced": advanced, + "required": [] +} + +export{googleCloudStorageDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/googledrive.ts b/src/controller/storage/parameters/defaults/googledrive.ts new file mode 100644 index 0000000..994419d --- /dev/null +++ b/src/controller/storage/parameters/defaults/googledrive.ts @@ -0,0 +1,74 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface GoogleDriveParamsStandard { + client_id?: string; + client_secret?: string; + scope?: string; + root_folder_id?: string; + service_account_file?: string; +} + +interface GoogleDriveParamsAdvanced { + service_account_credentials?: string; + team_drive?: string; + auth_owner_only?: boolean; + use_trash?: boolean; + skip_gdocs?: boolean; + shared_with_me?: boolean; + trashed_only?: boolean; + export_formats?: string; + import_formats?: string; + allow_import_name_change?: boolean; + use_created_date?: boolean; + list_chunk?: number; + impersonate?: string; + alternate_export?: boolean; + upload_cutoff?: number; + chunk_size?: number; + acknowledge_abuse?: boolean; + keep_revision_forever?: boolean; + v2_download_min_size?: number; + pacer_min_sleep?: number; + pacer_burst?: number; +} + +const standard: GoogleDriveParamsStandard = { + client_id: "", + client_secret: "", + scope: "", + root_folder_id: "", + service_account_file: "" +} + +const advanced: GoogleDriveParamsAdvanced = { + service_account_credentials: "", + team_drive: "", + auth_owner_only: false, + use_trash: true, + skip_gdocs: false, + shared_with_me: false, + trashed_only: false, + export_formats: "docx,xlsx,pptx,svg", + import_formats: "", + allow_import_name_change: false, + use_created_date: false, + list_chunk: 1000, + impersonate: "", + alternate_export: false, + upload_cutoff: 8388608, + chunk_size: 8388608, + acknowledge_abuse: false, + keep_revision_forever: false, + v2_download_min_size: -1, + pacer_min_sleep: 100000000, + pacer_burst: 100 +} + +const googleDriveDefaults: DefaultParams = { + "name": "Google Drive", + "standard": standard, + "advanced": advanced, + required:["client_id", "client_secret"] +} + +export{googleDriveDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/http.ts b/src/controller/storage/parameters/defaults/http.ts new file mode 100644 index 0000000..7ddfe12 --- /dev/null +++ b/src/controller/storage/parameters/defaults/http.ts @@ -0,0 +1,33 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + + +interface HTTPParamsStandard { + url: string; +} + +interface HTTPParamsAdvanced { + headers?: string; // Assuming this to be a string for simplification + no_slash?: boolean; + no_head?: boolean; + description?: string; +} + +const standard: HTTPParamsStandard = { + url: "", // This should be provided by the user since it's required +} + +const advanced: HTTPParamsAdvanced = { + headers: "", + no_slash: false, + no_head: false, + description: "" +} + +const httpDefaults: DefaultParams = { + "name": "http", + "standard": standard, + "advanced": advanced, + "required": ['url'] +} + +export{httpDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/jottacloud.ts b/src/controller/storage/parameters/defaults/jottacloud.ts new file mode 100644 index 0000000..fa7899c --- /dev/null +++ b/src/controller/storage/parameters/defaults/jottacloud.ts @@ -0,0 +1,46 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface JottacloudParamsStandard { + client_id?: string; + client_secret?: string; +} + +interface JottacloudParamsAdvanced { + token?: string; + auth_url?: string; + token_url?: string; + md5_memory_limit?: string; + trashed_only?: boolean; + hard_delete?: boolean; + upload_resume_limit?: string; + no_versions?: boolean; + encoding?: string; + description?: string; +} + +const standard: JottacloudParamsStandard = { + client_id: "", + client_secret: "", +} + +const advanced: JottacloudParamsAdvanced = { + token: "", + auth_url: "", + token_url: "", + md5_memory_limit: "10Mi", + trashed_only: false, + hard_delete: false, + upload_resume_limit: "10Mi", + no_versions: false, + encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,Del,Ctl,InvalidUtf8,Dot", + description: "", +} + +const jottacloudDefaults: DefaultParams = { + "name": "Jottacloud", + "standard": standard, + "advanced": advanced, + "required": [] // 根据实际情况调整必须的参数 +}; + +export{jottacloudDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/local.ts b/src/controller/storage/parameters/defaults/local.ts new file mode 100644 index 0000000..cbadd77 --- /dev/null +++ b/src/controller/storage/parameters/defaults/local.ts @@ -0,0 +1,33 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; +import { LocalParamsAdvanced, LocalParamsStandard } from "../../../../type/rclone/storage/parameters/local"; + +const standard: LocalParamsStandard = { + // 对于本地文件系统,标准参数通常不需要 +} + +const advanced: LocalParamsAdvanced = { + nounc: false, + copy_links: false, + links: false, + skip_links: false, + zero_size_links: false, + unicode_normalization: false, + no_check_updated: false, + one_file_system: false, + case_sensitive: false, + case_insensitive: false, + no_preallocate: false, + no_sparse: false, + no_set_modtime: false, + encoding: "Slash,Dot", + description: "", +} + +const localDefaults: DefaultParams = { + "name": "New_Storage", + "standard": standard, + "advanced": advanced, + "required": [] +} + +export { localDefaults }; \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/mega.ts b/src/controller/storage/parameters/defaults/mega.ts new file mode 100644 index 0000000..237b520 --- /dev/null +++ b/src/controller/storage/parameters/defaults/mega.ts @@ -0,0 +1,37 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface MegaParamsStandard { + user?: string; + pass?: string; +} + +interface MegaParamsAdvanced { + debug?: boolean; + hard_delete?: boolean; + use_https?: boolean; + encoding?: string; + description?: string; +} + +const standard: MegaParamsStandard = { + user: "", + pass: "" +}; + +const advanced: MegaParamsAdvanced = { + debug: false, + hard_delete: false, + use_https: false, + encoding: "Slash,InvalidUtf8,Dot", + description: "" +}; + + +const megaDefaults: DefaultParams = { + "name": "Mega", + "standard": standard, + "advanced": advanced, + "required": ['user', 'pass'] +}; + +export{megaDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/onedrive.ts b/src/controller/storage/parameters/defaults/onedrive.ts new file mode 100644 index 0000000..324e88e --- /dev/null +++ b/src/controller/storage/parameters/defaults/onedrive.ts @@ -0,0 +1,47 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults" +import { OneDriveParamsAdvanced, OneDriveParamsStandard } from "../../../../type/rclone/storage/parameters/onedrive" + + +const standard: OneDriveParamsStandard = { + client_id: "", + client_secret: "", + region: { + select: 'global', + values: ['global', 'us', 'de', 'cn'] + }, +} + +const advanced: OneDriveParamsAdvanced = { + drive_type: { select: 'personal', values: ['personal', 'business','documentLibrary'] }, + token: "", + auth_url: "", + token_url: "", + chunk_size: "10Mi", + drive_id: "", + root_folder_id: "", + access_scopes: "Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access", + disable_site_permission: false, + expose_onenote_files: false, + server_side_across_configs: false, + list_chunk: 1000, + no_versions: false, + link_scope: "anonymous", + link_type: "view", + link_password: "", + hash_type: "auto", + av_override: false, + delta: false, + metadata_permissions: "off", + encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,Del,Ctl,LeftSpace,LeftTilde,RightSpace,RightPeriod,InvalidUtf8,Dot", + description: "" +} + +const onedriveDefaults: DefaultParams = { + "name": "New_Storage", + "standard": standard, + "advanced": advanced, + "required": ['client_id', 'client_secret'] +} + + +export { onedriveDefaults } \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/opendrive.ts b/src/controller/storage/parameters/defaults/opendrive.ts new file mode 100644 index 0000000..08ae346 --- /dev/null +++ b/src/controller/storage/parameters/defaults/opendrive.ts @@ -0,0 +1,33 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface OpenDriveParamsStandard { + username?: string; + password?: string; +} + +interface OpenDriveParamsAdvanced { + encoding?: string; + chunk_size?: string; + description?: string; +} + +const standard: OpenDriveParamsStandard = { + username: "", + password: "" +}; + +const advanced: OpenDriveParamsAdvanced = { + encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,LeftSpace,LeftCrLfHtVt,RightSpace,RightCrLfHtVt,InvalidUtf8,Dot", + chunk_size: "10Mi", + description: "" +}; + + +const opendriveDefaults: DefaultParams = { + "name": "OpenDrive", + "standard": standard, + "advanced": advanced, + "required": ['username', 'password'] +}; + +export{opendriveDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/pcloud.ts b/src/controller/storage/parameters/defaults/pcloud.ts new file mode 100644 index 0000000..970036c --- /dev/null +++ b/src/controller/storage/parameters/defaults/pcloud.ts @@ -0,0 +1,45 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface PcloudParamsStandard { + client_id?: string; + client_secret?: string; +} + +interface PcloudParamsAdvanced { + token?: string; + auth_url?: string; + token_url?: string; + encoding?: string; + root_folder_id?: string; + hostname?: string; + username?: string; + password?: string; + description?: string; +} + +const standard: PcloudParamsStandard = { + client_id: '', + client_secret: '' +}; + +const advanced: PcloudParamsAdvanced = { + token: '', + auth_url: '', + token_url: '', + encoding: "Slash,BackSlash,Del,Ctl,InvalidUtf8,Dot", + root_folder_id: "d0", + hostname: "api.pcloud.com", + username: '', + password: '', + description: '' +}; + + +const pcloudDefaults: DefaultParams = { + "name": "Pcloud", + "standard": standard, + "advanced": advanced, + required: ["client_id", "client_secret"] +}; + +export { pcloudDefaults }; \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/qingstor.ts b/src/controller/storage/parameters/defaults/qingstor.ts new file mode 100644 index 0000000..ef608d4 --- /dev/null +++ b/src/controller/storage/parameters/defaults/qingstor.ts @@ -0,0 +1,45 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface QingStorParamsStandard { + env_auth?: boolean; + access_key_id?: string; + secret_access_key?: string; + endpoint?: string; + zone?: string; +} + +interface QingStorParamsAdvanced { + connection_retries?: number; + upload_cutoff?: string; // 假设 SizeSuffix 可以被表示为 string + chunk_size?: string; // 假设 SizeSuffix 可以被表示为 string + upload_concurrency?: number; + encoding?: string; + description?: string; +} + +const standard: QingStorParamsStandard = { + env_auth: false, + access_key_id: '', + secret_access_key: '', + endpoint: "https://qingstor.com:443", + zone: "pek3a" +}; + +const advanced: QingStorParamsAdvanced = { + connection_retries: 3, + upload_cutoff: "200Mi", + chunk_size: "4Mi", + upload_concurrency: 1, + encoding: "Slash,Ctl,InvalidUtf8", + description: '' +}; + + +const qingstorDefaults: DefaultParams = { + "name": "QingStor", + "standard": standard, + "advanced": advanced, + required: ["access_key_id", "secret_access_key"] +}; + +export { qingstorDefaults } \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/s3.ts b/src/controller/storage/parameters/defaults/s3.ts new file mode 100644 index 0000000..b81ade7 --- /dev/null +++ b/src/controller/storage/parameters/defaults/s3.ts @@ -0,0 +1,80 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults" +import { S3ParamsAdvanced, S3ParamsStandard } from "../../../../type/rclone/storage/parameters/s3" + + +const standard: S3ParamsStandard = { + provider: { + select: 'Alibaba', + values: [ + "AWS", "Alibaba", "ArvanCloud", "Ceph", "ChinaMobile", "Cloudflare", + "DigitalOcean", "Dreamhost", "GCS", "HuaweiOBS", "IBMCOS", "IDrive", + "IONOS", "LyveCloud", "Leviia", "Liara", "Linode", "Minio", "Netease", + "Petabox", "RackCorp", "Rclone", "Scaleway", "SeaweedFS", "StackPath", + "Storj", "Synology", "TencentCOS", "Wasabi", "Qiniu", "Other" + ] + }, + env_auth: false, + access_key_id: "", + secret_access_key: "", + region: "", + endpoint: "", + location_constraint: "", + acl: "private", + server_side_encryption: "", + sse_kms_key_id: "", + storage_class: "STANDARD", +} + +const advanced: S3ParamsAdvanced = { + bucket_acl: "private", + requester_pays: false, + sse_customer_algorithm: "", + sse_customer_key: "", + sse_customer_key_base64: "", + sse_customer_key_md5: "", + upload_cutoff: "200Mi", + chunk_size: "5Mi", + max_upload_parts: 10000, + copy_cutoff: "4.656Gi", + disable_checksum: false, + shared_credentials_file: "", + profile: "", + session_token: "", + upload_concurrency: 4, + force_path_style: true, + v2_auth: false, + use_dual_stack: false, + use_accelerate_endpoint: false, + leave_parts_on_error: false, + list_chunk: 1000, + list_version: 0, + list_url_encode: { select: 'unset', values: ['true', 'false', 'unset'] }, + no_check_bucket: false, + no_head: false, + no_head_object: false, + encoding: "Slash,InvalidUtf8,Dot", + disable_http2: false, + download_url: "", + directory_markers: false, + use_multipart_etag: { select: 'unset', values: ['true', 'false', 'unset'] }, + use_presigned_request: false, + versions: false, + version_at: "", + version_deleted: false, + decompress: false, + might_gzip: { select: 'unset', values: ['true', 'false', 'unset'] }, + use_accept_encoding_gzip: { select: 'unset', values: ['true', 'false', 'unset'] }, + no_system_metadata: false, + sts_endpoint: "", + use_already_exists: { select: 'unset', values: ['true', 'false', 'unset'] }, + use_multipart_uploads: { select: 'unset', values: ['true', 'false', 'unset'] }, + description: "", +} + +const s3Defaults: DefaultParams = { + "name": "New_Storage", + "standard": standard, + "advanced": advanced, + "required": ['provider','access_key_id','secret_access_key'] +} +export{s3Defaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/sftp.ts b/src/controller/storage/parameters/defaults/sftp.ts new file mode 100644 index 0000000..a42675e --- /dev/null +++ b/src/controller/storage/parameters/defaults/sftp.ts @@ -0,0 +1,89 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface SFTPParamsStandard { + host: string; + user?: string; + port?: number; + pass?: string; + key_pem?: string; + key_file?: string; + key_file_pass?: string; + pubkey_file?: string; + key_use_agent?: boolean; + use_insecure_cipher?: boolean; + disable_hashcheck?: boolean; + ssh?: string; +} + +interface SFTPParamsAdvanced { + known_hosts_file?: string; + ask_password?: boolean; + path_override?: string; + set_modtime?: boolean; + shell_type?: string; + md5sum_command?: string; + sha1sum_command?: string; + skip_links?: boolean; + subsystem?: string; + server_command?: string; + use_fstat?: boolean; + disable_concurrent_reads?: boolean; + disable_concurrent_writes?: boolean; + idle_timeout?: string; // 假设 Duration 可以被表示为 string + chunk_size?: string; // 假设 SizeSuffix 可以被表示为 string + concurrency?: number; + set_env?: string; // SpaceSepList 可以被表示为 string + ciphers?: string; // SpaceSepList 可以被表示为 string + key_exchange?: string; // SpaceSepList 可以被表示为 string + macs?: string; // SpaceSepList 可以被表示为 string + host_key_algorithms?: string; // SpaceSepList 可以被表示为 string + socks_proxy?: string; + copy_is_hardlink?: boolean; + description?: string; +} + +const standard: SFTPParamsStandard = { + host: "", + user: "", + port: 22, + key_use_agent: false, + use_insecure_cipher: false, + disable_hashcheck: false +}; + +const advanced: SFTPParamsAdvanced = { + known_hosts_file: '', + ask_password: false, + path_override: '', + set_modtime: true, + shell_type: '', + md5sum_command: '', + sha1sum_command: '', + skip_links: false, + subsystem: "sftp", + server_command: '', + use_fstat: false, + disable_concurrent_reads: false, + disable_concurrent_writes: false, + idle_timeout: "1m0s", + chunk_size: "32Ki", + concurrency: 64, + set_env: '', + ciphers: '', + key_exchange: '', + macs: '', + host_key_algorithms: '', + socks_proxy: '', + copy_is_hardlink: false, + description: '' +}; + + +const sftpDefaults: DefaultParams = { + "name": "SFTP", + "standard": standard, + "advanced": advanced, + "required": ['host'] +}; + +export{sftpDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/swift.ts b/src/controller/storage/parameters/defaults/swift.ts new file mode 100644 index 0000000..e6417a7 --- /dev/null +++ b/src/controller/storage/parameters/defaults/swift.ts @@ -0,0 +1,70 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface SwiftParamsStandard { + env_auth?: boolean; + user?: string; + key?: string; + auth?: string; + user_id?: string; + domain?: string; + tenant?: string; + tenant_id?: string; + tenant_domain?: string; + region?: string; + storage_url?: string; + auth_token?: string; + application_credential_id?: string; + application_credential_name?: string; + application_credential_secret?: string; + auth_version?: number; + endpoint_type?: string; + storage_policy?: string; +} + +interface SwiftParamsAdvanced { + leave_parts_on_error?: boolean; + chunk_size?: string; + no_chunk?: boolean; + no_large_objects?: boolean; + encoding?: string; + description?: string; +} + +const standard: SwiftParamsStandard = { + env_auth: false, + user: "", + key: "", + auth: "", + user_id: "", + domain: "", + tenant: "", + tenant_id: "", + tenant_domain: "", + region: "", + storage_url: "", + auth_token: "", + application_credential_id: "", + application_credential_name: "", + application_credential_secret: "", + auth_version: 0, + endpoint_type: "public", + storage_policy: "", +}; + +const advanced: SwiftParamsAdvanced = { + leave_parts_on_error: false, + chunk_size: "5Gi", + no_chunk: false, + no_large_objects: false, + encoding: "Slash,InvalidUtf8", + description: "", +}; + +const swiftDefaults: DefaultParams= { + "name": "Swift", + "standard": standard, + "advanced": advanced, + "required": [] // 假设 'auth' 是必需的,具体根据实际情况调整 +}; + +export{swiftDefaults} \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/webdav.ts b/src/controller/storage/parameters/defaults/webdav.ts new file mode 100644 index 0000000..dd258e4 --- /dev/null +++ b/src/controller/storage/parameters/defaults/webdav.ts @@ -0,0 +1,32 @@ + +import { DefaultParams, ParamsSelectType } from "../../../../type/rclone/storage/defaults"; +import { WebdavParamsAdvanced, WebdavParamsStandard } from "../../../../type/rclone/storage/parameters/webdav"; + +const standard: WebdavParamsStandard = { + url: "", + vendor: { + select: "other", + values: ["other", "fastmail", "nextcloud", "owncloud", "sharepoint", "sharepoint-ntlm", "rclone"] + }, + user: "", + pass: "", +} + +const advanced: WebdavParamsAdvanced = { + bearer_token_command: "", + encoding: "", + headers: [], + pacer_min_sleep: "", + nextcloud_chunk_size: "", + owncloud_exclude_shares: false, + description: "", +} + +const webdavDefaults: DefaultParams = { + "name":"New_Storage", + "standard": standard, + "advanced": advanced, + "required": ['url'] +} + +export { webdavDefaults } \ No newline at end of file diff --git a/src/controller/storage/parameters/defaults/yandex.ts b/src/controller/storage/parameters/defaults/yandex.ts new file mode 100644 index 0000000..9667fea --- /dev/null +++ b/src/controller/storage/parameters/defaults/yandex.ts @@ -0,0 +1,42 @@ +import { DefaultParams } from "../../../../type/rclone/storage/defaults"; + +interface YandexParamsStandard { + client_id?: string; + client_secret?: string; +} + +interface YandexParamsAdvanced { + token?: string; + auth_url?: string; + token_url?: string; + hard_delete?: boolean; + encoding?: string; + description?: string; +} + +const standard: YandexParamsStandard = { + client_id: '', + client_secret: '' +}; + +const advanced: YandexParamsAdvanced = { + token: '', + auth_url: '', + token_url: '', + hard_delete: false, + encoding: "Slash,Del,Ctl,InvalidUtf8,Dot", + description: '' +}; + + +const yandexDefaults: DefaultParams = { + "name": "Yandex", + "standard": standard, + "advanced": advanced, + required: [ + "client_id", + "client_secret" + ], +}; + +export{yandexDefaults} \ No newline at end of file diff --git a/src/controller/storage/storage.ts b/src/controller/storage/storage.ts new file mode 100644 index 0000000..3ee2748 --- /dev/null +++ b/src/controller/storage/storage.ts @@ -0,0 +1,165 @@ +import { invoke } from "@tauri-apps/api" +import { hooks } from "../../services/hook" +import { rcloneInfo } from "../../services/rclone" +import { FileInfo } from "../../type/rclone/rcloneInfo" +import { ParametersType } from "../../type/rclone/storage/defaults" +import { rclone_api_post } from "../../utils/rclone/request" + +//列举存储 +async function reupStorage() { + const dump = await rclone_api_post( + '/config/dump', + ) + rcloneInfo.storageList = [] + for (const storageName in dump) { + rcloneInfo.storageList.push({ + name: storageName, + type: dump[storageName].type, + }) + } + hooks.upStorage() +} + + +//删除存储 +async function delStorage(name: string) { + const del = await rclone_api_post( + '/config/delete', { + name: name + }) + console.log(del); + reupStorage() +} + +//获取存储 +async function getStorageParams(name: string): Promise { + const get = await rclone_api_post( + '/config/get', { + name: name + }) + return get +} + + +//获取文件列表 +async function getFileList(storageName: string, path: string): Promise { + + const fileList = await rclone_api_post( + '/operations/list', { + fs: storageName + ':', + remote: formatPathRclone(path, false) + }) + return fileList.list +} + +//删除存储 +async function delFile(storageName: string, path: string, refreshCallback?: Function) { + if (path.substring(0, 1) == '/') { + path = path.substring(1, path.length) + } + const backData = await rclone_api_post( + '/operations/deletefile', { + fs: storageName + ':', + remote: formatPathRclone(path, false) + }) + if (refreshCallback) { + refreshCallback() + } +} + +async function delDir(storageName: string, path: string, refreshCallback?: Function) { + + const backData = await rclone_api_post( + '/operations/purge', { + fs: storageName + ':', + remote: formatPathRclone(path, true) + }) + if (refreshCallback) { + refreshCallback() + } +} + +//创建目录 +async function mkDir(storageName: string, path: string, refreshCallback?: Function) { + + + const backData = await rclone_api_post( + '/operations/mkdir', { + fs: storageName + ':', + remote: formatPathRclone(path, true) + }) + if (refreshCallback) { + refreshCallback() + } +} + +function formatPathRclone(path: string, isDir?: boolean): string { + if (path.substring(0, 1) == '/') { + path = path.substring(1, path.length) + } + if (isDir) { + if (path.substring(path.length - 1, path.length) == '/') { + path = path.substring(0, path.length - 1) + } else { + path = path + '/' + } + } + + path = path.replace(/\/+/g, '/'); + return path; +} + +//copyFile +async function copyFile(storageName: string, path: string, destStoragename: string, destPath: string, pathF2f: boolean = false) {//pathF2f:destPath为文件时需要设置为true。(默认false时为文件夹,文件名来自srcPath) + const backData = await rclone_api_post( + '/operations/copyfile', { + srcFs: storageName + ':', + srcRemote: formatPathRclone(path), + dstFs: destStoragename + ':', + dstRemote: formatPathRclone(destPath, !pathF2f) + (!pathF2f && getFileName(path)) + }, true) +} + +async function moveFile(storageName: string, path: string, destStoragename: string, destPath: string, newNmae?: string,pathF2f: boolean = false) { + + const backData = await rclone_api_post( + '/operations/movefile', { + srcFs: storageName + ':', + srcRemote: formatPathRclone(path), + dstFs: destStoragename + ':', + dstRemote: formatPathRclone(destPath, !pathF2f) + (!pathF2f && newNmae ? newNmae : getFileName(path)) + }, true) +} + +function getFileName(path: string): string { + const pathArr = path.split('/') + return pathArr[pathArr.length - 1] +} + +//copyDir +async function copyDir(storageName: string, path: string, destStoragename: string, destPath: string) { + const backData = await rclone_api_post( + '/sync/copy', { + srcFs: storageName + ':' + formatPathRclone(path, true), + dstFs: destStoragename + ':' + formatPathRclone(destPath, true) + getFileName(path) + }, true) +} + +async function moveDir(storageName: string, path: string, destStoragename: string, destPath: string, newNmae?: string) { + const backData = await rclone_api_post( + '/sync/move', { + srcFs: storageName + ':' + formatPathRclone(path, true), + dstFs: destStoragename + ':' + formatPathRclone(destPath, true) + (newNmae ? newNmae : getFileName(path)) + }, true) +} + +//sync,需完整path(pathF2f) +async function sync(storageName: string, path: string, destStoragename: string, destPath: string, bisync?: boolean) {//bisync:双向同步 + const backData = await rclone_api_post( + !bisync?'/sync/sync':'/sync/bisync', { + srcFs: storageName + ':' + formatPathRclone(path, true), + dstFs: destStoragename + ':' + formatPathRclone(destPath, true) + }, true) +} + +export { reupStorage, delStorage, getStorageParams, getFileList, delFile, delDir, mkDir, formatPathRclone, copyFile, copyDir, moveFile, moveDir,sync } \ No newline at end of file diff --git a/src/controller/task/autoMount.ts b/src/controller/task/autoMount.ts new file mode 100644 index 0000000..0e2553b --- /dev/null +++ b/src/controller/task/autoMount.ts @@ -0,0 +1,11 @@ +import { nmConfig } from "../../services/config"; +import { mountStorage } from "../storage/mount/mount"; + +async function autoMount() { + nmConfig.mount.lists.forEach(async (item) => { + item.autoMount && await mountStorage(item) + }) +} + +export{autoMount} + \ No newline at end of file diff --git a/src/controller/task/runner.ts b/src/controller/task/runner.ts new file mode 100644 index 0000000..ffa8f41 --- /dev/null +++ b/src/controller/task/runner.ts @@ -0,0 +1,92 @@ +import { TaskListItem } from "../../type/config"; +import { copyDir, copyFile, delDir, delFile, moveDir, moveFile, sync } from "../storage/storage"; + +async function runTask(task: TaskListItem): Promise { + let taskMsg = '' + + const executeTask = (task: TaskListItem) => { + console.log(`Executing ${task.taskType} task: ${task.name}`); + + const srcIsDir = task.source.path.endsWith('/'); + const targetIsDir = task.target.path.endsWith('/'); + + /* if (item.isMove) { + if (item.isDir) { + moveDir(item.storageName, item.path, storageName!, path!); + } else { + moveFile(item.storageName, item.path, storageName!, path!); + } + } else { + if (item.isDir) { + copyDir(item.storageName, item.path, storageName!, path!); + } else { + copyFile(item.storageName, item.path, storageName!, path!) + } + } */ + + switch (task.taskType) { + case 'copy': {//复制 + if (srcIsDir && targetIsDir) {//复制目录 + copyDir(task.source.storageName, task.source.path, task.target.storageName, task.target.path) + } else if (!srcIsDir && !targetIsDir) {//复制文件 + copyFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path, true) + } else if (!srcIsDir && targetIsDir) {//复制文件到目录 + copyFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path) + } else { + throw new Error('The directory cannot be copied/moved to a file'); + } + break; + }; + case 'move': {//移动 + if (srcIsDir && targetIsDir) {//移动目录 + moveDir(task.source.storageName, task.source.path, task.target.storageName, task.target.path) + } else if (!srcIsDir && targetIsDir) {//移动文件到目录 + moveFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path) + } else if (!srcIsDir && !targetIsDir) {//移动文件 + moveFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path, undefined, true) + } else { + throw new Error('The directory cannot be copied/moved to a file'); + } + break; + }; + case 'delete': {//删除 + if (srcIsDir) {//删除目录 + delDir(task.source.storageName, task.source.path) + } else {//删除文件 + delFile(task.source.storageName, task.source.path) + } + break; + }; + case 'sync': {//同步 + sync(task.source.storageName, task.source.path, task.target.storageName, task.target.path) + break; + }; + case 'bisync': {//双向同步 + sync(task.source.storageName, task.source.path, task.target.storageName, task.target.path, true) + break; + } + default: { + throw new Error('Invalid task type'); + } + } + + //一次性任务,执行完毕后禁用 + if (task.run.mode === 'disposable') { + task.enable = false; + } + }; + + try { + if (task.enable) { + executeTask(task); + task.runInfo = { ...task.runInfo, error: false, msg: taskMsg }; + } + } catch (error) { + console.error(`Error executing task ${task.name}:`, error); + task.runInfo = { ...task.runInfo, error: true, msg: taskMsg + (error instanceof Error ? error.message : String(error)) }; + } + + return task; +} + +export { runTask } \ No newline at end of file diff --git a/src/controller/task/scheduler.ts b/src/controller/task/scheduler.ts new file mode 100644 index 0000000..b4e36b8 --- /dev/null +++ b/src/controller/task/scheduler.ts @@ -0,0 +1,77 @@ +import { TaskListItem } from "../../type/config"; +import { runTask } from "./runner"; +import { delTask } from "./task"; + +class TaskScheduler { + tasks: TaskListItem[]; + + constructor() { + this.tasks = []; + } + + public async addTask(task: TaskListItem) { + if (task.enable) { + this.tasks.push(task); + this.scheduleTask(task); + } + } + + private async scheduleTask(task: TaskListItem) { + switch (task.run.mode) { + case 'start': + await this.executeTask(task); + this.cancelTask(task.name); + break; + case 'disposable': + await this.executeTask(task); + this.cancelTask(task.name); + delTask(task.name); + break; + case 'time': + const executeTaskInterval = () => { + const now = new Date(); + const scheduledTime = new Date(now); + scheduledTime.setDate(scheduledTime.getDate() + task.run.time.intervalDays); + scheduledTime.setHours(task.run.time.h, task.run.time.m, task.run.time.s); + + // 如果设置的时间比现在的时间早,则表示下一次执行是明天 + if (scheduledTime < now) { + scheduledTime.setDate(scheduledTime.getDate() + 1); + } + + const timeout = scheduledTime.getTime() - now.getTime(); + if (timeout >= 0) { + task.run.runId = window.setTimeout(async () => { + await this.executeTask(task); + // 完成执行后,重新计划下一次执行 + executeTaskInterval(); + }, timeout); + } + }; + executeTaskInterval(); + break; + case 'interval': + task.run.runId = window.setInterval(async () => await this.executeTask(task), task.run.interval); + break; + default: + console.error('Invalid task mode:', task.run.mode); + } + } + + public async executeTask(task: TaskListItem) { + const updatedTask = await runTask(task) + this.tasks = this.tasks.map(t => t.name === updatedTask.name ? updatedTask : t); + } + + cancelTask(taskName: string) { + const task = this.tasks.find(t => t.name === taskName); + if (task && task.run.runId !== undefined) { + window.clearInterval(task.run.runId); + window.clearTimeout(task.run.runId); + task.run.runId = undefined + console.log(`${taskName} task cancelled.`); + } + } +} + +export { TaskScheduler } \ No newline at end of file diff --git a/src/controller/task/task.ts b/src/controller/task/task.ts new file mode 100644 index 0000000..e223c4c --- /dev/null +++ b/src/controller/task/task.ts @@ -0,0 +1,39 @@ +import { nmConfig } from "../../services/config"; +import { TaskListItem } from "../../type/config"; +import { TaskScheduler } from "./scheduler"; + +const taskScheduler = new TaskScheduler() + +function saveTask(taskInfo: TaskListItem) { + const existingTaskIndex = nmConfig.task.findIndex( + (task) => task.name === taskInfo.name + ); + + if (existingTaskIndex !== -1) { + // 存在同名任务,更新已有任务 + if(taskInfo.run.runId){taskScheduler.cancelTask(taskInfo.name)} + nmConfig.task[existingTaskIndex] = taskInfo; + } else { + // 不存在同名任务,直接添加新任务 + nmConfig.task.push(taskInfo); + } + if(taskInfo.run.mode!=='start'){ + taskScheduler.addTask(taskInfo) + } + + return true +} + +function delTask(taskName: string) { + taskScheduler.cancelTask(taskName) + nmConfig.task = nmConfig.task.filter((task) => task.name !== taskName); + return true; +} + +async function startTaskScheduler() { + for (let task of nmConfig.task) { + await taskScheduler.addTask(task) + } +} + +export { saveTask, delTask, taskScheduler, startTaskScheduler } \ No newline at end of file diff --git a/src/controller/test.ts b/src/controller/test.ts new file mode 100644 index 0000000..137c516 --- /dev/null +++ b/src/controller/test.ts @@ -0,0 +1,62 @@ +import { rclone_api_post } from "../utils/rclone/request"; +import { createStorage } from "./storage/create"; +import { getFileList, reupStorage } from "./storage/storage"; +import { invoke } from '@tauri-apps/api'; + +import { appWindow } from "@tauri-apps/api/window"; +import { app } from "@tauri-apps/api"; +import { nmConfig, osInfo } from "../services/config"; +import { Aria2 } from "../utils/aria2/aria2"; +import { checkUpdate } from "./update/update"; +import { getWinFspInstallState, installWinFsp } from "../utils/utils"; +import { t } from "i18next"; + +export async function Test() { + + + console.log(await rclone_api_post('/options/get')); + + console.log(nmConfig); + + + /* let data = await invoke('read_config_file') as any; + console.log(data); + console.log(await invoke('write_config_file', { + configData: data + })); */ + + + /* let taskids = (await rclone_api_post('/job/list')).jobids as Array + taskids.forEach(async (taskid) => { + console.log(await rclone_api_post('/job/status', { + jobid: taskid + })); + }) */ + /* console.log(await rclone_api_post('/operations/copyurl',{ + remote:'/hpm-od/QQNT_VirtualHotBar_9.9.7.21453_QQNT.HPM', + fs:'Webdav:' + })); */ + + + + + + +/* const aria2Test = new Aria2('https://down.hotpe.top/d/Package/HotPE-V2.7.240201.exe', + 'F:/', + 'test1.7z', + 8, (back) => { + console.log(back); + }) + + + + aria2Test.start() */ + //console.log(await runCmd('curl', [url,'-o', path])); + + console.log(osInfo); + + //await installWinFsp() + //console.log(await getWinFspInstallState()); + +} \ No newline at end of file diff --git a/src/controller/update/notice.ts b/src/controller/update/notice.ts new file mode 100644 index 0000000..c6183a5 --- /dev/null +++ b/src/controller/update/notice.ts @@ -0,0 +1,14 @@ +import { nmConfig } from "../../services/config"; +import { Notice } from "../../type/controller/update"; + + +async function checkNotice() { + const notice: Notice = (await (await fetch(nmConfig.api.url + '/GetNotice/?lang=' + nmConfig.settings.language)).json()) + if (notice.state === 'success') { + if (nmConfig.notice === undefined || (nmConfig.notice && notice.data.content !== nmConfig.notice.data.content)) { + nmConfig.notice = notice + } + } +} + +export { checkNotice } \ No newline at end of file diff --git a/src/controller/update/update.ts b/src/controller/update/update.ts new file mode 100644 index 0000000..f538563 --- /dev/null +++ b/src/controller/update/update.ts @@ -0,0 +1,23 @@ +import { fs } from "@tauri-apps/api"; +import { downloadFile, takeRightStr } from "../../utils/utils"; +import { ResItem, ResList } from "../../type/controller/update"; +import { nmConfig, osInfo } from "../../services/config"; +import { getVersion } from "@tauri-apps/api/app"; +import { Modal } from "@arco-design/web-react"; + + +async function checkUpdate(updateCall: (resList: ResItem,localUpdateId: number) => void) { + const localUpdateId = await getUpdateId() + + const resList: ResItem = (await (await fetch(nmConfig.api.url + '/GetUpdate/?arch=' + osInfo.arch + '&osType=' + osInfo.osType)).json()).data + + if (resList.id && Number(resList.id) < localUpdateId) { + updateCall(resList,localUpdateId) + } +} + +async function getUpdateId() { + return Number(takeRightStr(await getVersion(), '-')) +} + +export { checkUpdate } \ No newline at end of file diff --git a/src/controller/window.ts b/src/controller/window.ts new file mode 100644 index 0000000..71a605b --- /dev/null +++ b/src/controller/window.ts @@ -0,0 +1,38 @@ +import { listen } from "@tauri-apps/api/event"; +import { appWindow } from "@tauri-apps/api/window" +import { exit } from "./main"; + +function listenWindow() { + appWindow.listen('tauri://close-requested', () => { + windowsHide() + }) + + // 阻止F5或Ctrl+R(Windows/Linux)和Command+R(Mac)刷新页面 + document.addEventListener('keydown', function (event) { + if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { + event.preventDefault(); + } + }); + + //禁止右键 + document.oncontextmenu = () => { + return false; + } + + +} + +function windowsHide() { + appWindow.hide() +} + +function windowsMini() { + appWindow.minimize() +} + +listen('exit_app', async () => { + await exit() +}); + +export { listenWindow, windowsHide, windowsMini } + diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..3c35ae1 --- /dev/null +++ b/src/index.css @@ -0,0 +1,60 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +p,h1,h2,h3,h4,h5,h6 { + color: var(--color-text-1); +} + +.singe-line { + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + white-space: nowrap; + } + +/* 字体 */ +@font-face { + font-family: "Inter"; + src: url("./assets/font/HarmonyOS_Sans_Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Inter-Bold"; + src: url("./assets/font/HarmonyOS_Sans_Bold.ttf") format("truetype"); +} + +@font-face { + font-family: 'emoji'; + src: url('./assets/font/seguiemj.woff2') format('woff2'); +} + + +/* 美化滚动条 */ +::-webkit-scrollbar { + width: 6px; + height: 10px; +} + +::-webkit-scrollbar-track { + width: 6px; + background: rgba(#101F1C, 0.1); + -webkit-border-radius: 2em; + -moz-border-radius: 2em; + border-radius: 2em; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(144, 147, 153, .5); + background-clip: padding-box; + min-height: 28px; + -webkit-border-radius: 2em; + -moz-border-radius: 2em; + border-radius: 2em; + transition: background-color .3s; + cursor: pointer; +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(144, 147, 153, .3); +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..6dfaf7d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next'; +import './services/i18n' +import './index.css' +import { App } from './app' +import { BrowserRouter } from 'react-router-dom' +import { init } from './controller/main'; +import ReactDOM from 'react-dom/client'; +import { ConfigProvider, Spin } from '@arco-design/web-react'; +import { hooks } from './services/hook'; +import './controller/errorHandling' + + +function StartPage() { + const { t } = useTranslation() + const [startStr, setStartStr] = useState('loading') + + useEffect(() => { + appStart(setStartStr) + }) + + return
+

+ +
+ {t('starting') + ':' + startStr}

+
+} + +const reactRoot = ReactDOM.createRoot(document.getElementById('root')!) +reactRoot.render( + +) + +let appStarting = false +async function appStart(setStartStr: Function) { + if (appStarting) { return }//避免重新执行 + appStarting = true +1 + await init(setStartStr)//初始化功能 + + reactRoot.render( + + + + )//React.StrictMode:严格模式检查组件副作用 +} \ No newline at end of file diff --git a/src/page/home/home.tsx b/src/page/home/home.tsx new file mode 100644 index 0000000..df14f5c --- /dev/null +++ b/src/page/home/home.tsx @@ -0,0 +1,207 @@ +import React, { useEffect, useReducer, useState } from 'react' + +import { Alert, Avatar, Button, Card, Descriptions, Grid, Link, Modal, Notification, Space, Typography } from "@arco-design/web-react" +import { Test } from "../../controller/test" +import { rcloneInfo } from '../../services/rclone' +import { hooks } from '../../services/hook'; +import { checkUpdate } from '../../controller/update/update'; +import { getVersion } from '@tauri-apps/api/app'; +import { shell } from '@tauri-apps/api'; +import { formatETA, formatSize } from '../../utils/utils'; +import { useTranslation } from 'react-i18next'; +import { nmConfig } from '../../services/config'; +import { IconCloud, IconList, IconSelectAll, IconStorage, IconSwap } from '@arco-design/web-react/icon'; +const Row = Grid.Row; +const Col = Grid.Col; +const { Meta } = Card; + +let checkedUpdate: boolean = false; + +//checkedUpdate = true; + +function Home_page() { + const { t } = useTranslation() + const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件 + const [modal, contextHolder] = Modal.useModal(); + const [notification, noticeContextHolder] = Notification.useNotification(); + + useEffect(() => { + hooks.upStats = forceUpdate; + console.log(nmConfig.notice); + + if (nmConfig.notice && !nmConfig.notice.displayed && nmConfig.notice.data.content) { + notification.info!({ + ...(nmConfig.notice.data.title && { title: nmConfig.notice.data.title }), + content: nmConfig.notice.data.content, + ...{ duration: nmConfig.notice.manual_close ? 1000*60*60*24*365 : 3000 }, + }) + nmConfig.notice.displayed = true + } + + if (!checkedUpdate) { + checkUpdate(async (info) => { + modal.confirm!({ + title: t('update_available'), + content: <> + {`${t('current_version')}:${await getVersion()} , ${t('latest_version')}:${info.name}`} +
+ {t('goto_the_website_get_latest_version_ask')} + , + onOk: () => { + shell.open(info.website!) + }, + }) + }) + checkedUpdate = true; + } + + }, []) + + return ( +
+ {contextHolder}{noticeContextHolder} + + {/*

欢迎使用,统一管理和挂载云存储设施。

*/} +
+

NetMount

+ {t('netmount_slogan')} +
+ {/* + + + 🧐 +

初次使用,请点击下方按钮进行配置

+
+ +
*/} + {/* + 运行时间:{formatETA(rcloneInfo.stats.elapsedTime)} + */} +
+ {rcloneInfo.storageList && !(rcloneInfo.storageList.length > 0) && +
+ + + {t('please_add_storage_tip')} + + + { hooks.navigate('/storage/manage/add') }}> {t('add')} + + + } /> +
+ } +
+ + + {t('storage')}({rcloneInfo.storageList.length})
+
+ + + + +
+
+ + {t('mount')}({rcloneInfo.mountList.length}) +
+ + + + +
+
+ + {t('task')}({nmConfig.task.length}) +
+ + + + +
+
+
+
+

+ {/* + 存储和挂载概览 +
+ 存储数: +
+ 挂载数:{nmConfig.mount.lists.length} +
+ 已挂载:{rcloneInfo.mountList.length} +
*/} +
+ + + + + + + {t('transmission_overview')} + + + + + + + + + + 0 ? [ + { + label: t('used_time'), + value: formatETA(rcloneInfo.stats.transferTime) + } + ] : []), + ...(Number(rcloneInfo.stats.eta) > 0 ? [ + { + label: t('eta'), + value: formatETA(rcloneInfo.stats.eta!) + } + ] : []), + ...(rcloneInfo.stats.transferring && Number(rcloneInfo.stats.transferring.length) > 0 ? [ + { + label: t('transferring'), + value: rcloneInfo.stats.transferring.length + } + ] : []), + ...(Number(rcloneInfo.stats.totalTransfers) > 0 ? [ + { + label: t('transferred'), + value: rcloneInfo.stats.totalTransfers + } + ] : []), + + ]} /> + +
+ +
+ + ) +} + +/* 软件名称:NetMount +软件功能:挂载云存储到本地 + +主菜单(位于左边):首页(待实现),存储(添加存储,编辑存储,浏览和管理存储内文件),挂载存储(挂载为本地路径或盘符),传输(当前在传输的文件信息、速度、剩余时间等),任务(定时或间隔,可执行存储的文件同步、文件复制、文件删除、挂载等) + +软件整体布局为左:主菜单,右:对应页面 + +现在就还有软件首页没有写了,请你为我的软件设计一个首页 */ + +export { Home_page } \ No newline at end of file diff --git a/src/page/mount/add.tsx b/src/page/mount/add.tsx new file mode 100644 index 0000000..6b5d7f2 --- /dev/null +++ b/src/page/mount/add.tsx @@ -0,0 +1,145 @@ +import { Button, Checkbox, Collapse, Form, Input, Notification, Select, Space, Switch } from '@arco-design/web-react' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom'; +import { ParametersType } from '../../type/rclone/storage/defaults'; +import { getProperties, getURLSearchParam } from '../../utils/utils'; +import { defaultMountConfig, defaultVfsConfig } from '../../controller/storage/mount/parameters/defaults'; +import { InputItem_module } from '../other/inputItem'; +import { rcloneInfo } from '../../services/rclone'; +import { addMountStorage, getAvailableDriveLetter, getMountStorage, mountStorage } from '../../controller/storage/mount/mount'; + +const FormItem = Form.Item; + +const CollapseItem = Collapse.Item; + +export default function AddMount_page() { + const { t } = useTranslation() + const navigate = useNavigate(); + const [storageName, setStorageName] = useState() + const [showAllOptions, setShowAllOptions] = useState(false) + const [mountPath, setMountPath] = useState('') + const [autoMount, setAutoMount] = useState(true) + //const [autoMountPath, setAutoMountPath] = useState(true)//自动分配盘符 + + const isWindows = rcloneInfo.version.os.toLowerCase().includes('windows'); + + + let parameters: ParametersType = { mountOpt: {}, vfsOpt: {} } + + const setMountParams = (key: string, value: any) => { + parameters.mountOpt[key] = value; + }; + + const setVfsParams = (key: string, value: any) => { + parameters.vfsOpt[key] = value; + }; + + useEffect(() => { + if (getURLSearchParam('name')) { + setStorageName(getURLSearchParam('name')) + } else if (!storageName && rcloneInfo.storageList.length > 0) { + setStorageName(rcloneInfo.storageList[0].name) + } + + + if (isWindows) { + setMountPath('*') + } else { + setMountPath('/netmount/' + storageName) + } + }, []) + + return ( +
+

{t('add_mount')}

+
+ + + + + + { + isWindows ? + <> + {mountPath != '*' && setMountPath(value)} style={{ width: '12rem' }} placeholder={t('please_input')} />} + { checked ? setMountPath('*') : setMountPath('Z:') }} >{t('auto_drive_letter')} + : <> + setMountPath(value)} placeholder={t('please_input')} /> + + } + + + {!showAllOptions && + + {isWindows && { setMountParams('NetworkMode', !checked) }} >{t('simulate_hard_drive')}} + { setVfsParams('ReadOnly', checked) }} >{t('read_only')} + + } + + { +
+ { + getProperties(defaultMountConfig).map((item) => { + return ( + + ) + }) + } + { + getProperties(defaultVfsConfig).map((item) => { + return ( + + ) + }) + } +
+ } + + {/* 按钮 */} +
+ + { setAutoMount(checked) }} >{t('auto_mount')} + {!showAllOptions && } + + + +
+
+
+ ) +} diff --git a/src/page/mount/mount.tsx b/src/page/mount/mount.tsx new file mode 100644 index 0000000..9ec0aea --- /dev/null +++ b/src/page/mount/mount.tsx @@ -0,0 +1,103 @@ +import { Alert, Button, Grid, Message, Space, Table, TableColumnProps, Typography } from '@arco-design/web-react' +import React, { useEffect, useReducer, useState } from 'react' +import { rcloneInfo } from '../../services/rclone' +import { delMountStorage, isMounted, mountStorage, reupMount, unmountStorage } from '../../controller/storage/mount/mount' +import { useTranslation } from 'react-i18next' +import { hooks } from '../../services/hook' +import { useNavigate } from 'react-router-dom' +import { nmConfig, osInfo } from '../../services/config' +import { NoData_module } from '../other/noData' +import { getWinFspInstallState, installWinFsp } from '../../utils/utils' +const Row = Grid.Row; +const Col = Grid.Col; + +function Mount_page() { + const { t } = useTranslation() + const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件 + const navigate = useNavigate(); + const [winFspInstallState, setWinFspInstallState] = useState(); + const [winFspInstalling, setWinFspInstalling] = useState(); + + const columns: TableColumnProps[] = [ + { + title: t('storage_name'), + dataIndex: 'storageName', + }, + { + title: t('mount_path'), + dataIndex: 'mountPath', + }, + { + title: t('mount_status'), + dataIndex: 'mounted', + }, + { + title: t('actions'), + dataIndex: 'actions', + align: 'right' + } + ] + + const getWinFspState = async () => { + console.log(await getWinFspInstallState()); + + setWinFspInstallState(await getWinFspInstallState()) + } + + useEffect(() => { + hooks.upMount = forceUpdate + if (osInfo.osType === 'Windows_NT' && rcloneInfo.endpoint.isLocal && winFspInstallState === undefined) { + getWinFspState() + } + }, [ignored]) + + return ( +
+
+ + + + +
+
+
+ { + winFspInstallState !== undefined && !winFspInstallState && <> + + + }/> +
+ + } + } columns={columns} pagination={false} data={ + nmConfig.mount.lists.map((item) => { + const mounted = isMounted(item.mountPath) + return { + ...item, + mounted: mounted ? t('mounted') : t('unmounted'), + actions: + { + mounted ? <> + + : + <> + + + } + + } + })} /> + + + ) +} +export { Mount_page } \ No newline at end of file diff --git a/src/page/other/devTips.tsx b/src/page/other/devTips.tsx new file mode 100644 index 0000000..3ba4619 --- /dev/null +++ b/src/page/other/devTips.tsx @@ -0,0 +1,16 @@ +import { Result } from '@arco-design/web-react' +import { IconCodeBlock } from '@arco-design/web-react/icon' +import React from 'react' +import { useTranslation } from 'react-i18next' + +function DevTips_module() { + const { t } = useTranslation() + + return ( + } + title={t('dev_tips')} + > + ) +} +export { DevTips_module } \ No newline at end of file diff --git a/src/page/other/inputItem.tsx b/src/page/other/inputItem.tsx new file mode 100644 index 0000000..83d67d1 --- /dev/null +++ b/src/page/other/inputItem.tsx @@ -0,0 +1,112 @@ +import { CSSProperties, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, Checkbox, Form, Input, InputNumber, InputTag, Link, Message, Notification, Select, Space, Switch, Typography } from "@arco-design/web-react"; +import { rcloneInfo } from "../../services/rclone"; + +const FormItem = Form.Item; + +interface InputItemProps { + data: { key: any, value: any }; + setParams: (key: any, value: any) => void; +} + +function InputItem_module(props: InputItemProps) { + const { t } = useTranslation() + + let valueType: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' = typeof props.data.value; + + if (valueType === 'string' && (props.data.value === 'true' || props.data.value === 'false')) { + valueType = 'boolean'; + } else if (props.data.value == '[]' || (valueType === 'object' && props.data.value instanceof Array)) { + valueType = 'array'; + } + + const setParams = (value: any) => { + console.log(value); + + if (valueType == 'array' && props.data.key == 'headers') { + props.setParams(props.data.key, value.join(',')) + } else { + props.setParams(props.data.key, value) + } + } + + if (valueType == 'object' && props.data.value.select) { + setParams(props.data.value.select) + } else { + setParams(props.data.value) + } + + const style: CSSProperties = { + width: '85%' + } + + return {t(props.data.key)}} title={props.data.key}> + + {/* 输入框,string */ + valueType === 'string' && + <>{props.data.key.includes('pass') ?//密码 + setParams(value)} placeholder={t('please_input')} /> + : props.data.key === 'remote' ?//选择存储(remote:) + + : setParams(value)} placeholder={t('please_input')} /> + } + } + + {valueType === 'object' && props.data.value.select != null &&/* 选择器 */ + + } + + {valueType === 'number' &&/* 数字输入框 */ + setParams(value)} + style={style} + />} + + {valueType === 'boolean' &&/* 开关 */ + setParams(value)} /> + } + + {valueType === 'array' && ( + { + setParams(value); + }} + style={style} + /> + )} + +
+ +
+} + +export { InputItem_module } \ No newline at end of file diff --git a/src/page/other/noData.tsx b/src/page/other/noData.tsx new file mode 100644 index 0000000..4801cf8 --- /dev/null +++ b/src/page/other/noData.tsx @@ -0,0 +1,18 @@ +import { Result } from '@arco-design/web-react' +import { IconCodeBlock } from '@arco-design/web-react/icon' +import React from 'react' +import { useTranslation } from 'react-i18next' +interface NoDataProps { + tips?: string +} + +function NoData_module(props:NoDataProps) { + const { t } = useTranslation() + + return ( + + ) +} +export { NoData_module } \ No newline at end of file diff --git a/src/page/setting/setting.tsx b/src/page/setting/setting.tsx new file mode 100644 index 0000000..9eb58b3 --- /dev/null +++ b/src/page/setting/setting.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useReducer, useState } from 'react' +import { DevTips_module } from '../other/devTips' +import { Button, Card, Collapse, Divider, Form, Grid, Link, Modal, Select, Space, Switch, Typography } from '@arco-design/web-react' +import { Test } from '../../controller/test' +import { nmConfig, roConfig } from '../../services/config'; +import { getAutostartState, setAutostartState, setThemeMode } from '../../controller/setting/setting'; +import { useTranslation } from 'react-i18next'; +import { getVersion } from '@tauri-apps/api/app'; +import { shell } from '@tauri-apps/api'; +import { rcloneInfo } from '../../services/rclone'; +import { setLocalized } from '../../controller/language/localized'; +const CollapseItem = Collapse.Item; +const FormItem = Form.Item; +const Row = Grid.Row; +const Col = Grid.Col; + +export default function Setting_page() { + const { t } = useTranslation() + const [autostart, setAutostart] = useState() + const [modal, contextHolder] = Modal.useModal(); + const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件 + + const getAutostart = async () => { + setAutostart(await getAutostartState()); + } + + const showLog = (log: string) => { + modal.info!({ + + title: t('log'), + content:
+ {log} +
+ }) + + } + + useEffect(() => { + getAutostart() + }, []) + + + + return ( +
+ {contextHolder} + + +
+ + + + + + + + { + await setAutostartState(value); + setAutostart(value) + }} /> + + + + { + nmConfig.settings.startHide = value + forceUpdate() + }} /> + +
+ + +
+ {t('about_text')} +
+ {t('technology_stack')}:Tauri,TypeScript,Vite,React,Arco Design,Rust +
+ Copyright © 2024-Present VirtualHotBar + + + { shell.open(roConfig.url.website) }}> NetMount +
+ { open(roConfig.url.website + 'page/license') }}> {t('licence')} +
+ + + + + { shell.open(roConfig.url.rclone) }}>Rclone( { + rcloneInfo.process.log && showLog(rcloneInfo.process.log) + }}>{t('log')}): {rcloneInfo.version.version} +
+
+ + + + + + ) +} diff --git a/src/page/storage/add.tsx b/src/page/storage/add.tsx new file mode 100644 index 0000000..74a8882 --- /dev/null +++ b/src/page/storage/add.tsx @@ -0,0 +1,197 @@ +import { Button, Checkbox, Form, Input, InputNumber, InputTag, Link, Message, Notification, Select, Space, Switch, Typography } from "@arco-design/web-react"; +import { DefaultParams, ParametersType } from "../../type/rclone/storage/defaults"; +import { useTranslation } from "react-i18next"; +import { searchStorage, storageListAll } from "../../controller/storage/listAll"; +import { CSSProperties, useEffect, useState } from "react"; +import { checkParams, createStorage } from "../../controller/storage/create"; +import { useNavigate, useParams } from "react-router-dom"; +import { getProperties, getURLSearchParam } from "../../utils/utils"; +import { getStorageParams } from "../../controller/storage/storage"; +import { InputItem_module } from "../other/inputItem"; +import { rcloneInfo } from "../../services/rclone"; +const FormItem = Form.Item; + + + +function AddStorage_page() { + const { t } = useTranslation() + const navigate = useNavigate(); + + //const [selectStorage, setSelectStorage] = useState() + const [storageTypeName, setStorageTypeName] = useState() + const [defaultParams, setDefaultParams] = useState() + const [step, setStep] = useState(0)//0:选择类型,1:填写参数 + const [showAdvanced, setShowAdvanced] = useState(false) + + const [storageName, setStorageName] = useState('')//存储名称 + const [isEditMode, setIsEditMode] = useState(false) + let parameters: ParametersType = {}; + + const setParams = (key: string, value: any) => { + parameters[key] = value; + }; + + const editMode = async () => { + const type = getURLSearchParam('type') + const name = getURLSearchParam('name') + const storage = await getStorageParams(name) + + //setSelectStorage(type) + setStorageTypeName(searchStorage(type).name) + setStorageName(name) + + let defaultParamsEdit = searchStorage(type).defaultParams + + + const overwriteParams = (params: ParametersType) => { + getProperties(params).forEach((paramsItem) => { + if (storage[paramsItem.key]) { + const valueType = typeof params[paramsItem.key] + if (valueType === 'object' && !(params[paramsItem.key] instanceof Array)) { + if (params[paramsItem.key].values.includes(storage[paramsItem.key])) { + params[paramsItem.key].select = storage[paramsItem.key] + } + } else { + params[paramsItem.key] = storage[paramsItem.key]; + } + } + }) + } + + overwriteParams(defaultParamsEdit.standard) + overwriteParams(defaultParamsEdit.advanced) + + setDefaultParams(defaultParamsEdit) + + setStep(1) + } + + useEffect(() => { + setIsEditMode(Boolean(getURLSearchParam('edit'))) + if (isEditMode) { + editMode() + } + }, []) + + + return <> +

{t('add_storage')}

+ {step == 0 ?/* 选择类型 */ +
+
+ + + + + + {/* 存储介绍 */} + {storageTypeName ? + {t(searchStorage(storageTypeName).introduce!)} + : ''} + +
+ + + {/* 按钮 */} +
+ + + + +
+ +
+ : step == 1 ?/* 填写参数 */ +
+
+ { key && setStorageName(value) }} /> + + { + getProperties(defaultParams!.standard).map((paramsItem) => { + return ( + + ) + }) + } + + + +
+ + {//高级选项 + getProperties(defaultParams!.advanced).map((paramsItem) => { + return ( + + ) + })} + +
+ +
+
+ + { + //高级选项 + !showAdvanced && + + } + + + +
+
+ : '' + } +} + + + +export { AddStorage_page } \ No newline at end of file diff --git a/src/page/storage/explorer.tsx b/src/page/storage/explorer.tsx new file mode 100644 index 0000000..06f2f85 --- /dev/null +++ b/src/page/storage/explorer.tsx @@ -0,0 +1,370 @@ +import React, { CSSProperties, useEffect, useReducer, useState } from 'react' +import { BackTop, Badge, Button, Divider, Dropdown, Grid, Input, Link, List, Menu, Message, Modal, Notification, Popconfirm, Select, Space, Spin, Table, TableColumnProps, Tabs, Tooltip, Typography, Upload } from '@arco-design/web-react'; +import { IconCopy, IconDelete, IconEdit, IconFolderAdd, IconLeft, IconMore, IconPaste, IconRefresh, IconScissor, IconUpCircle, IconUpload } from '@arco-design/web-react/icon'; +import { rcloneInfo } from '../../services/rclone'; +import { useTranslation } from 'react-i18next'; +import { copyDir, copyFile, delDir, delFile, formatPathRclone, getFileList, mkDir, moveDir, moveFile } from '../../controller/storage/storage'; +import { FileInfo } from '../../type/rclone/rcloneInfo'; +import { formatSize, getURLSearchParam } from '../../utils/utils'; +import { rcloneApiHeaders } from '../../utils/rclone/request'; +import { RequestOptions } from '@arco-design/web-react/es/Upload'; +import { NoData_module } from '../other/noData'; +import { clipListItem } from '../../type/page/storage/explorer'; +import { searchStorage } from '../../controller/storage/listAll'; +const Row = Grid.Row; +const Col = Grid.Col; +const TabPane = Tabs.TabPane; +const tipsStyle: CSSProperties = { + textAlign: 'center', + paddingTop: '6rem', + fontSize: '1rem' +}; + +function Explorer_page() { + return ( + <> + {/* + + + + */} + + + ) +} + +// 规范路径 +const sanitizePath = (newPath: string): string => { + if (!newPath.startsWith('/')) { + newPath = '/' + newPath; + } + + // 确保路径不以 / 结尾,如果不是根路径 + if (newPath !== '/' && newPath.endsWith('/')) { + newPath = newPath.slice(0, -1); + } + + return newPath; +}; + +//取父目录 +const getParentPath = (currentPath: string): string => { + // 如果路径为空或者只有一个"/",则无上级目录 + if (currentPath === '/' || currentPath === '') { + return currentPath; + } + + // 找到最后一个"/"出现的位置 + const lastSlashIndex = currentPath.lastIndexOf('/'); + + // 返回截取到倒数第二个"/"之前的路径作为上级目录 + return currentPath.substring(0, lastSlashIndex); +}; + + +function ExplorerItem() { + const { t } = useTranslation() + + const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件 + const [modal, contextHolder] = Modal.useModal(); + const [storageName, setStorageName] = useState() + const [path, setPath] = useState() + const [pathTemp, setPathTemp] = useState('') + //const [selectedRowKeys, setSelectedRowKeys] = useState>([]); + + const [fileList, setFileInfo] = useState>() + + const [loading, setLoading] = useState(false) + + const [clipList, setClipList] = useState>([]) + + + const columns: TableColumnProps[] = [ + { + title: t('name'), + dataIndex: 'fileName', + ellipsis: true, + }, + { + title: t('modified_time'), + dataIndex: 'fileModTime', + ellipsis: true, + width: '10.5rem', + }, + { + title: t('size'), + dataIndex: 'fileSize', + ellipsis: true, + width: '7rem', + }, + { + title: t('actions'), + dataIndex: 'actions', + align: 'right', + width: '10rem', + + } + ] + + + + //刷新文件列表 + async function fileInfo() { + setLoading(true) + const l = await getFileList(storageName!, path!) + setLoading(false) + setFileInfo(l) + } + + // 创建一个自定义函数用于更新路径,确保路径始终符合规范 + const updatePath = (newPath: string) => { + const sanitizedPath = sanitizePath(newPath); + setPath(sanitizedPath); + setPathTemp(sanitizedPath) + }; + + //剪贴板去重 + const isClipHave = (storageName_: string, path: string): boolean => { + return clipList.findIndex(v => v.storageName === storageName_ && v.path === path) !== -1; + }; + const addCilp = (clip: clipListItem) => { + if (isClipHave(clip.storageName, clip.path)) return; + setClipList([...clipList, { isMove: clip.isMove, storageName: clip.storageName, path: clip.path, isDir: clip.isDir }]) + } + + useEffect(() => { + //页面加载时,从URL中获取存储名称和路径 + if (getURLSearchParam('name')) { + setStorageName(getURLSearchParam('name')) +/* if (getURLSearchParam('path')) { + setPath(getURLSearchParam('path')) + } */ + } + + + if (!storageName && !getURLSearchParam('name')&& rcloneInfo.storageList.length > 0) { + setStorageName(rcloneInfo.storageList[0].name) + } + }, []); + + useEffect(() => { + if (storageName && path) { + fileInfo(); + } + }, [path]); + + useEffect(() => { + if (storageName && !path) { + updatePath('/') + setPathTemp('/') + } + + if (storageName && path) { + fileInfo(); + } + }, [storageName]) + + + useEffect(() => { + }, [clipList]) + + function MakeDir() { + let dirNameTemp = '' + if (storageName && path) { + modal.info!({ + title: t('create_directory'), + icon: null, + content: dirNameTemp = value} />, + onOk: async () => { + dirNameTemp ? await mkDir(storageName, path + '/' + dirNameTemp, fileInfo) : Message.error(t('dir_name_cannot_empty')) + }, + }) + } + } + + function UploadFile() { + + const customRequest = (option: RequestOptions) => { + const { onProgress, onError, onSuccess, file } = option; + + const formData = new FormData(); + formData.append('file', file); + + const xhr = new XMLHttpRequest(); + + xhr.upload.onprogress = ({ lengthComputable, loaded, total }) => { + if (lengthComputable) { + console.log(Math.round(loaded / total * 100)); + onProgress(Math.round(loaded / total * 100)); + } + }; + + xhr.onload = () => { + xhr.status === 200 ? onSuccess() : onError(xhr); + }; + + xhr.onerror = () => onError(xhr); + + xhr.open('POST', `${rcloneInfo.endpoint.url}/operations/uploadfile?fs=${storageName}:&remote=${formatPathRclone(path!, false)}`, true); + xhr.setRequestHeader('Authorization', `Bearer ${rcloneApiHeaders.Authorization}`); + xhr.send(formData); + }; + + if (storageName && path) { + modal.info!({ + title: t('upload_file'), + icon: null, + content: <> + , + onOk: fileInfo, + onCancel: fileInfo + }) + } + } + + function fileRename(filePath: string, isDir: boolean) { + console.log(getParentPath(filePath)); + + let nameTemp = filePath.split('/').pop()!; + modal.info!({ + title: t('rename'), + icon: null, + content: nameTemp = value} />, + onOk: async () => { + if (nameTemp) { + isDir ? await moveDir(storageName!, filePath, storageName!, getParentPath(filePath), nameTemp) : + await moveFile(storageName!, filePath, storageName!, getParentPath(filePath), nameTemp); + fileInfo(); + } else { + Message.error(t('name_cannot_empty')) + } + }, + }) + } + + return ( +
+
+ {contextHolder} + +
+ + + + + + { return path! }} onChange={(value) => { setPathTemp(value) }} onPressEnter={() => { updatePath(pathTemp) }} /> + + + + + + + + { + clipList.forEach((item) => { + if (item.isMove) { + if (item.isDir) { + moveDir(item.storageName, item.path, storageName!, path!); + } else { + moveFile(item.storageName, item.path, storageName!, path!); + } + } else { + if (item.isDir) { + copyDir(item.storageName, item.path, storageName!, path!); + } else { + copyFile(item.storageName, item.path, storageName!, path!) + } + } + }) + setClipList([]) + Notification.success({ + title: t('success'), + content: t('transm_task_created'), + }) + }} key='p' disabled={!storageName && !path}>{t('paste')}({clipList.length}) + setClipList([])} key='q'>{t('empty_the_clipboard')} + } position='bl'> +
} + data={ + fileList.map((item) => { + return { + ...item, fileName: { item.IsDir && updatePath(item.Path) }}>{item.Name}, + fileSize: (item.Size != -1 ? formatSize(item.Size) : t('dir')), + fileModTime: (new Date(item.ModTime)).toLocaleString(), + actions: + + + + + +
+
+
} columns={columns} pagination={false} data={ + rcloneInfo.storageList.map((item) => { + return { + ...item, + type: searchStorage(item.type).name, + actions: + { + delStorage(item.name) + }} + > + + + + + + + + + } + })} /> + + + ) +} + + + + + + + + + +export { Storage_page } \ No newline at end of file diff --git a/src/page/task/add.tsx b/src/page/task/add.tsx new file mode 100644 index 0000000..db0037c --- /dev/null +++ b/src/page/task/add.tsx @@ -0,0 +1,309 @@ +import { Button, Divider, Form, Grid, Input, InputNumber, Notification, Select, Space, Tooltip } from '@arco-design/web-react'; +import React, { useReducer, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { nmConfig, roConfig } from '../../services/config'; +import { TaskListItem } from '../../type/config'; +import { rcloneInfo } from '../../services/rclone'; +import { IconQuestionCircle } from '@arco-design/web-react/icon'; +import { formatPathRclone } from '../../controller/storage/storage'; +import { useNavigate } from 'react-router-dom'; +import { saveTask } from '../../controller/task/task'; +const Row = Grid.Row; +const Col = Grid.Col; + +const formatPath = (path: string) => { + path = path.replace(/\\/g, '/'); + path = path.replace(/\/+/g, '/'); + + if (path.substring(0, 1) != '/') { + path = '/' + path; + } + + return path +} + +// 定义状态和 action 类型 +type TaskInfoState = TaskListItem; + +type Action = + | { type: 'setName'; payload: string } + | { type: 'setRunTypeMode'; payload: string } + | { type: 'setTaskType'; payload: string } + | { type: 'setSourceStorageName'; payload: string } + | { type: 'setSourcePath'; payload: string } + | { type: 'setTargetStorageName'; payload: string } + | { type: 'setTargetPath'; payload: string } + | { type: 'setIntervalDays'; payload: number } + | { type: 'setRunTime'; payload: { h: number, m: number, s: number } }; + +// 定义 reducer 函数 +const reducer = (state: TaskInfoState, action: Action): TaskInfoState => { + switch (action.type) { + case 'setName': + return { ...state, name: action.payload }; + case 'setRunTypeMode': + return { ...state, run: { ...state.run, mode: action.payload } }; + case 'setTaskType': + return { ...state, taskType: action.payload }; + case 'setSourceStorageName': + return { ...state, source: { ...state.source, storageName: action.payload } }; + case 'setSourcePath': + return { ...state, source: { ...state.source, path: formatPath(action.payload) } }; + case 'setTargetStorageName': + return { ...state, target: { ...state.target, storageName: action.payload } }; + case 'setTargetPath': + return { ...state, target: { ...state.target, path: formatPath(action.payload) } }; + case 'setIntervalDays': + return { ...state, run: { ...state.run, time: { ...state.run.time, intervalDays: action.payload } } }; + case 'setRunTime': + return { ...state, run: { ...state.run, time: { ...state.run.time, ...action.payload } } }; + default: + throw new Error('Invalid action'); + } +}; + +function AddTask_page() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [taskInfo, dispatch] = useReducer(reducer, { + name: 'task_' + (nmConfig.task ? nmConfig.task.length + 1 : 1), + taskType: roConfig.options.task.taskType.select[roConfig.options.task.taskType.defIndex], + source: { + storageName: + rcloneInfo.storageList && rcloneInfo.storageList.length > 0 + ? rcloneInfo.storageList[0].name + : '', + path: '/', + }, + target: { + storageName: + rcloneInfo.storageList && rcloneInfo.storageList.length > 0 + ? (rcloneInfo.storageList.length > 1 + ? rcloneInfo.storageList[1].name + : rcloneInfo.storageList[0].name) + : '', + path: '/', + }, + run: { + mode: roConfig.options.task.runMode.select[roConfig.options.task.runMode.defIndex], time: { + intervalDays: 1, + h: 10, + m: 30, + s: 0, + } + }, + enable: true, + }); + + const [timeMultiplier, setTimeMultiplier] = useState({ + ...roConfig.options.task.dateMultiplier.select[roConfig.options.task.dateMultiplier.defIndex], + multiplicand: 1 + }) + + useEffect(() => { + if (taskInfo.run.mode === 'time') { + setTimeMultiplier({ ...roConfig.options.task.dateMultiplier.select[roConfig.options.task.dateMultiplier.defIndex], multiplicand: 1 }) + } else if (taskInfo.run.mode === 'interval') { + setTimeMultiplier({ ...roConfig.options.task.intervalMultiplier.select[roConfig.options.task.intervalMultiplier.defIndex], multiplicand: 1 }) + } + }, [taskInfo.run.mode]) + + + return ( +
+

{t('add_task')}

+
+ + dispatch({ type: 'setName', payload: value })} + placeholder={t('please_input')} + /> + + + + + {taskInfo.run.mode != 'start' && taskInfo.run.mode !== 'disposable' && + <> + + +
+ + + + { + setTimeMultiplier({ ...timeMultiplier, multiplicand: value }); + } + } /> + + + + + { + taskInfo.run.mode === 'time' && <> + + + + dispatch({ type: 'setRunTime', payload: { ...taskInfo.run.time, h: value } })} + /> + + + dispatch({ type: 'setRunTime', payload: { ...taskInfo.run.time, m: value } })} + /> + + + dispatch({ type: 'setRunTime', payload: { ...taskInfo.run.time, s: value } })} + /> + + + + + } + + } + + + + + + + + + + + + dispatch({ type: 'setSourcePath', payload: value })} + disabled={!taskInfo.source.storageName} + /> + + + + + + + + + + {taskInfo.taskType !== 'delete' && ( + + + + + + + dispatch({ type: 'setTargetPath', payload: value })} + disabled={!taskInfo.target.storageName} + /> + + + + + + + + + )} + +
+ + + + +
+ + ); +} + + + +export { AddTask_page }; \ No newline at end of file diff --git a/src/page/task/task.tsx b/src/page/task/task.tsx new file mode 100644 index 0000000..c33d829 --- /dev/null +++ b/src/page/task/task.tsx @@ -0,0 +1,64 @@ +import React, { useReducer } from 'react' +import { DevTips_module } from '../other/devTips' +import { Button, Space, Table, TableColumnProps } from '@arco-design/web-react' +import { useTranslation } from 'react-i18next' +import { nmConfig } from '../../services/config' +import { useNavigate } from 'react-router-dom' +import { delTask } from '../../controller/task/task' +import { NoData_module } from '../other/noData' + +function Task_page() { + const { t } = useTranslation() + const navigate = useNavigate(); + const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件 + + const columns: TableColumnProps[] = [ + { + title: t('task_name'), + dataIndex: 'name', + }, + { + title: t('state'), + dataIndex: 'state', + }, { + title: t('cycle'), + dataIndex: 'cycle', + } + , { + title: t('run_info'), + dataIndex: 'runInfo', + }, + { + title: t('actions'), + dataIndex: 'actions', + align:'right' + } + ]; + console.log(nmConfig.task); + + return ( +
+
+ + + + +
+
+
} data={nmConfig.task.map((taskItem) => { + return { + ...taskItem, + state: taskItem.enable ? t('enabled') : t('disabled'), + cycle: t('task_run_mode_' + taskItem.run.mode), + runInfo:taskItem.runInfo?.msg, + actions: <> + + + } + })} /> + + + ) +} + +export { Task_page } \ No newline at end of file diff --git a/src/page/transmit/transmit.tsx b/src/page/transmit/transmit.tsx new file mode 100644 index 0000000..67cb677 --- /dev/null +++ b/src/page/transmit/transmit.tsx @@ -0,0 +1,135 @@ +import React, { useEffect, useState } from 'react' +import { rcloneInfo, rcloneStatsHistory } from '../../services/rclone' +import { hooks } from '../../services/hook' +import { RcloneTransferItem } from '../../type/rclone/stats' +import { Card, Descriptions, List, Progress, Space, Statistic, Grid, Typography } from '@arco-design/web-react' +import { formatETA, formatSize } from '../../utils/utils' +import { Area } from '@ant-design/charts' +import { NoData_module } from '../other/noData' +import { useTranslation } from 'react-i18next' +const Row = Grid.Row; +const Col = Grid.Col; + +function Transmit_page() { + const { t } = useTranslation() + const [transmitList, setTransmitList] = useState([]) + + useEffect(() => { + hooks.upStats = () => { + if (rcloneInfo.stats.transferring) { + setTransmitList(rcloneInfo.stats.transferring) + } else { + setTransmitList([]) + } + } + hooks.upStats() + }, []) + + return ( +
+ + + + {rcloneInfo.stats.bytes > 0 && } + 0 ? [ + { + label:t('used_time'), + value: formatETA(rcloneInfo.stats.transferTime) + } + ] : []), + ...(Number(rcloneInfo.stats.eta) > 0 ? [ + { + label: t('eta'), + value: formatETA(rcloneInfo.stats.eta!) + } + ] : []), + ...(Number(rcloneInfo.stats.totalTransfers) > 0 ? [ + { + label: t('transferred'), + value: rcloneInfo.stats.totalTransfers + } + ] : []), + + ]} /> + + + + + {/* */} + + + + }> + { + transmitList.map((item, index) => { + return + +
+ + + + {item.name} + 0 ? [ + { + label: t('eta'), + value: formatETA(item.eta!) + } + ] : []), + ...(item.dstFs ? [ + { + label: t('target'), + value: item.dstFs + } + ] : []), + ]} /> + + + + }) + } + + + + + ) +} + +export { Transmit_page } \ No newline at end of file diff --git a/src/services/config.ts b/src/services/config.ts new file mode 100644 index 0000000..d5f8818 --- /dev/null +++ b/src/services/config.ts @@ -0,0 +1,94 @@ +import { invoke } from "@tauri-apps/api" +import { NMConfig, OSInfo } from "../type/config" + +const roConfig = { + url: { + website: 'https://www.netmount.cn/', + rclone: 'https://github.com/rclone/rclone', + }, + options: { + task: { + runMode: { + defIndex: 0, + select: ['start', 'time', 'interval', 'disposable'] + }, + taskType: { + defIndex: 3, + select: ['copy', 'move', 'delete', 'sync', 'bisync'] + }, + dateMultiplier: { + defIndex: 0, + select: [{ name: 'day', value: 1 }, { name: 'week', value: 7 }, { name: 'month', value: 30 }] + }, + intervalMultiplier: { + defIndex: 0, + select: [{ name: 'hour', value: 60 * 60 }, { name: 'minute', value: 60 }, { name: 'second', value: 1 }] + }, + }, + setting: { + themeMode: { + defIndex: 0, + select: ['auto', 'light', 'dark'] + }, language: { + defIndex: 0, + select: [ + { name: '简体中文', value: 'cn', langCode: 'zh-cn' }, + { name: 'English', value: 'en', langCode: 'en-us' }, + /* { name: '繁體中文(臺灣)', value: 'cht', langCode: 'zh-tw' }, + { name: '繁體中文(香港)', value: 'cht', langCode: 'zh-hk' }, + { name: 'Русский язык', value: 'ru', langCode: 'ru-RU' }, */ + ] + } + } + } +} + +let nmConfig: NMConfig = { + mount: { + lists: [], + }, + task: [], + api: { + url: 'https://api.hotpe.top/test/NetMount', + }, + settings: { + themeMode: roConfig.options.setting.themeMode.select[roConfig.options.setting.themeMode.defIndex], + startHide: false, + }, +} + +const setNmConfig = (config: NMConfig) => { + nmConfig = config +} + +const readNmConfig = async () => { + await invoke('read_config_file').then(configData => { + setNmConfig(configData as NMConfig) + }).catch(err => { + console.log(err); + }) +} +const saveNmConfig = async () => { + await invoke('write_config_file', { + configData: nmConfig + }); +} + + + +let osInfo: OSInfo = { + arch: 'unknown', + osType: 'unknown', + platform: 'unknown', + tempDir: '', + osVersion: '' +} + +const setOsInfo = (osinfo: OSInfo) => { + osInfo = osinfo +} + + + + +export { nmConfig, setNmConfig, osInfo, setOsInfo, roConfig, readNmConfig, saveNmConfig } \ No newline at end of file diff --git a/src/services/hook.ts b/src/services/hook.ts new file mode 100644 index 0000000..b70aec8 --- /dev/null +++ b/src/services/hook.ts @@ -0,0 +1,12 @@ +import { Hooks } from "../type/hook"; +import i18n from "./i18n"; + +let hooks:Hooks = { + upStats:()=>{}, + upStorage:()=>{}, + upMount:()=>{}, + navigate:()=>{}, + setLocaleStr:()=>{} +} + +export {hooks} \ No newline at end of file diff --git a/src/services/i18n.ts b/src/services/i18n.ts new file mode 100644 index 0000000..f7e20e6 --- /dev/null +++ b/src/services/i18n.ts @@ -0,0 +1,27 @@ +// i18n.js 文件 +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// 引入语言文件 +import cn from '../controller/language/zh-cn.json'; + +// 初始化资源文件,即各种语言的json文件 +const resources = { + cn: { + translation: cn + } +}; + +i18n + // 连接react-i18next与i18next的插件配置 + .use(initReactI18next) + .init({ + resources, +/*lng: "cn", // 初始语言 */ + keySeparator: false, // 是否允许keys使用点分隔符 + interpolation: { + escapeValue: false // 转义字符 + } + }); + +export default i18n; diff --git a/src/services/rclone.ts b/src/services/rclone.ts new file mode 100644 index 0000000..83f9bee --- /dev/null +++ b/src/services/rclone.ts @@ -0,0 +1,61 @@ + +import { t } from "i18next" +import { RcloneInfo } from "../type/rclone/rcloneInfo" +import { RcloneStats } from "../type/rclone/stats" + +let rcloneInfo: RcloneInfo = { + process:{ + + }, + endpoint: { + url: '', + isLocal: true, + auth: { + user: '', + pass: '', + }, + localhost: { + port: 5572, + } + }, + storageList: [], + mountList: [], + version: { + arch: "", + decomposed: [], + goTags: "", + goVersion: "", + isBeta: false, + isGit: false, + linking: "", + os: "", + version: "" + }, + stats: { + bytes: 0, + checks: 0, + deletedDirs: 0, + deletes: 0, + elapsedTime: 0, + errors: 0, + eta: null, + fatalError: false, + renames: 0, + retryError: false, + serverSideCopies: 0, + serverSideCopyBytes: 0, + serverSideMoveBytes: 0, + serverSideMoves: 0, + speed: 0, + totalBytes: 0, + totalChecks: 0, + totalTransfers: 0, + transferTime: 0, + lastError: '', + transferring: [] + } +} + +let rcloneStatsHistory: RcloneStats[] = [] + +export { rcloneInfo, rcloneStatsHistory } \ No newline at end of file diff --git a/src/type/config.d.ts b/src/type/config.d.ts new file mode 100644 index 0000000..1aa07c8 --- /dev/null +++ b/src/type/config.d.ts @@ -0,0 +1,67 @@ +import { Arch, OsType, Platform } from "@tauri-apps/api/os" +import { ParametersType } from "./rclone/storage/defaults" +import { Notice } from "./controller/update" + +interface NMConfig { + mount: { + lists: MountListItem[] + }, + task: TaskListItem[], + api: { + url: string + }, + settings: { + themeMode: 'dark' | 'light' | 'auto' | string, + startHide:boolean, + language?:string + }, + notice?:Notice +} + + +interface MountListItem { + storageName: string, + mountPath: string, + parameters: ParametersType,//挂载配置 + autoMount: boolean,//软件启动自动挂载 +} + +interface TaskListItem { + name: string, + taskType: 'copy' | 'move' | 'delete' | 'sync' |'bisync'| string, + source: { + storageName: string, + path: string, + }, + target: { + storageName: string, + path: string, + }, + parameters?: ParametersType, + enable: boolean + run: { + runId?: number,//任务id,setTimeout或setInterval的返回值 + mode: 'time' | 'interval' | 'start' |'disposable'| string,//start:软件启动时执行,time:定时执行,interval:间隔执行 , disposable:一次性执行(执行后删除任务) + time: { + intervalDays: number,//间隔天数 + h: number,//小时 + m: number,//分钟 + s: number,//秒 + }, + interval?: number,//周期执行,单位ms + }, + runInfo?: { + error:boolean + msg: string, + } +} + +interface OSInfo { + arch: Arch | 'unknown', + osType: OsType | 'unknown', + platform: Platform | 'unknown', + tempDir: string, + osVersion: string +} + +export { NMConfig, MountListItem, TaskListItem, OSInfo } \ No newline at end of file diff --git a/src/type/controller/update.d.ts b/src/type/controller/update.d.ts new file mode 100644 index 0000000..adee664 --- /dev/null +++ b/src/type/controller/update.d.ts @@ -0,0 +1,30 @@ +export interface ResList { + [key: string]: ResItem; +} + +export interface ResItem { + id: string; + name: string; + pushTime: string; + body?: string; + assets: ResAsset[]; + website?: string; + download_url?: string; +} + +export interface ResAsset { + name: string; + size: number; + download_url: string; +} + +export interface Notice { + state: 'success' | string; // 假设状态还有 'failure' 等其他可能值,这里仅作示例 + data: { + title: string; + content: string; + }; + manual_close: boolean; + language: string; // 添加更多可能的语言选项,此处仅为示例 + displayed:boolean +} \ No newline at end of file diff --git a/src/type/hook.d.ts b/src/type/hook.d.ts new file mode 100644 index 0000000..c76ae0d --- /dev/null +++ b/src/type/hook.d.ts @@ -0,0 +1,9 @@ +interface Hooks{ + upStats:Function; + upStorage:Function; + upMount:Function; + navigate:(path:string)=>void; + setLocaleStr:(localeStr:string)=>void|string; +} + +export {Hooks} \ No newline at end of file diff --git a/src/type/page/storage/explorer.d.ts b/src/type/page/storage/explorer.d.ts new file mode 100644 index 0000000..a8da823 --- /dev/null +++ b/src/type/page/storage/explorer.d.ts @@ -0,0 +1,8 @@ +interface clipListItem{ + isMove:boolean; + storageName:string; + path:string; + isDir:boolean +} + +export {clipListItem} \ No newline at end of file diff --git a/src/type/page/storage/pageMark.d.ts b/src/type/page/storage/pageMark.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/type/rclone/rcloneInfo.d.ts b/src/type/rclone/rcloneInfo.d.ts new file mode 100644 index 0000000..0b94e6d --- /dev/null +++ b/src/type/rclone/rcloneInfo.d.ts @@ -0,0 +1,60 @@ +import { Child, Command } from "@tauri-apps/api/shell"; +import { RcloneStats } from "./stats"; + +interface RcloneInfo { + process:{ + command?:Command, + child?:Child, + log?:string + }, + endpoint: { + url: string, + isLocal: boolean,// 是否为本地地址 + auth: { + user: string, + pass: string, + }, + localhost: { + port: number, + } + }, + version: RcloneVersion, + stats: RcloneStats, + storageList: Array, + mountList: Array +} + +// 定义 RcloneVersion 接口 +interface RcloneVersion { + arch: string; // CPU 架构,例如:"386" + decomposed: number[]; // 版本号数组,例如:[1, 66, 0] + goTags: string; // Go 语言标签,例如:"cmount" + goVersion: string; // Go 语言版本,例如:"go1.22.1" + isBeta: boolean; // 是否为 Beta 版本,例如:false + isGit: boolean; // 是否为 Git 构建版本,例如:false + linking: string; // 链接类型,例如:"static" + os: string; // 目标操作系统,例如:"windows" + version: string; // Rclone 版本字符串,例如:"v1.66.0" +} + +interface StorageList { + name: string, + type: 'webdav' | string +} + +interface MountList { + storageName: string, + mountPath: string, + mountedTime: Date, +} + +interface FileInfo { + Path: string; + Name: string; + Size: number; + MimeType: string; + ModTime: Date; + IsDir: boolean; +} + +export { RcloneInfo, FileInfo } \ No newline at end of file diff --git a/src/type/rclone/stats.d.ts b/src/type/rclone/stats.d.ts new file mode 100644 index 0000000..c2ca80c --- /dev/null +++ b/src/type/rclone/stats.d.ts @@ -0,0 +1,62 @@ +/* { + "bytes": 0, + "checks": 0, + "deletedDirs": 0, + "deletes": 0, + "elapsedTime": 9.6440363, + "errors": 0, + "eta": null, + "fatalError": false, + "renames": 0, + "retryError": false, + "serverSideCopies": 0, + "serverSideCopyBytes": 0, + "serverSideMoveBytes": 0, + "serverSideMoves": 0, + "speed": 0, + "totalBytes": 0, + "totalChecks": 0, + "totalTransfers": 0, + "transferTime": 0, + "transfers": 0 +} */ + +interface RcloneTransferItem { + bytes: number; // 已处理的字节数 + eta: null | number; // 预计剩余时间(如果可用,否则为null) + group: string; // 通常是"global_stats" + name: string; // 文件或目录名称 + percentage: number; // 完成百分比 + size: number; // 文件大小(字节) + speed: number; // 当前速度(字节/秒) + speedAvg: number; // 平均速度(字节/秒) + srcFs: string; // 源文件系统的标识符(如"Webdav:") + dstFs?: string; // 目标文件系统的标识符(如"Webdav:") +} + +interface RcloneStats { + bytes: number; // 已处理的字节数 + checks: number; // 完成的校验数 + deletedDirs: number; // 删除的目录数量 + deletes: number; // 删除的文件数量 + elapsedTime: number; // 运行总时间(秒) + errors: number; // 出现的错误总数 + eta: null | number; // 预计剩余时间(如果可用,否则为null) + fatalError: boolean; // 是否出现致命错误 + lastError: string; // 最近发生的错误信息 + renames: number; // 重命名的数量 + retryError: boolean; // 是否有重试错误发生 + serverSideCopies: number; // 服务器端复制操作的数量 + serverSideCopyBytes: number; // 通过服务器端复制传输的字节数 + serverSideMoveBytes: number; // 通过服务器端移动传输的字节数 + serverSideMoves: number; // 服务器端移动操作的数量 + speed: number; // 当前速度(字节/秒) + realSpeed?: number; // 实时速度(字节/秒) + totalBytes: number; // 总共处理的字节数 + totalChecks: number; // 总共完成的校验数 + totalTransfers: number; // 总共完成的传输操作数 + transferTime: number; // 执行传输操作所花费的时间(秒) + transferring: RcloneTransferItem[]; // 正在进行的传输项列表 +} + +export { RcloneStats, RcloneTransferItem } \ No newline at end of file diff --git a/src/type/rclone/storage/defaults.d.ts b/src/type/rclone/storage/defaults.d.ts new file mode 100644 index 0000000..93c8bb5 --- /dev/null +++ b/src/type/rclone/storage/defaults.d.ts @@ -0,0 +1,17 @@ +interface ParametersType { + [key: string]: any +} + +interface DefaultParams { + "name": string,//存储名称 + "standard": ParametersType, + "advanced": ParametersType, + "required": Array +} + +interface ParamsSelectType { + select: string | number, + values: Array | { [key: string | number]: string | number } +} + +export { DefaultParams, ParametersType, ParamsSelectType } \ No newline at end of file diff --git a/src/type/rclone/storage/mount/parameters.d.ts b/src/type/rclone/storage/mount/parameters.d.ts new file mode 100644 index 0000000..2d863e7 --- /dev/null +++ b/src/type/rclone/storage/mount/parameters.d.ts @@ -0,0 +1,71 @@ +//https://rclone.org/rc/#data-types +//https://rclone.org/rc/#mount-mount +//https://p0.hotpe.top/i/p/1/66080cb6e9a4c.png + +//https://github.com/rclone/rclone-webui-react/blob/master/src/utils/MountOptions.js +//http://localhost:5572/options/get +//https://rclone.org/commands/rclone_mount/#options-opt +import { ParametersType, ParamsSelectType } from "../defaults"; + +interface VfsOptions { + CacheMaxAge: number; + CacheMaxSize: number; + CacheMode: ParamsSelectType; + CachePollInterval: number; + CaseInsensitive: boolean; + ChunkSize: number; + ChunkSizeLimit: number; + DirCacheTime: number; + DirPerms: number; + FilePerms: number; + GID: number; + NoChecksum: boolean; + NoModTime: boolean; + NoSeek: boolean; + PollInterval: number; + ReadAhead: number; + ReadOnly: boolean; + ReadWait: number; + UID: number; + Umask: number; + WriteBack: number; + WriteWait: number; + Refresh?: boolean; + BlockNormDupes?: boolean; + UsedIsSize?: boolean; + FastFingerprint?: boolean; + DiskSpaceTotalSize?: number; + UID?: number; + GID?: number; + Umask?: number; +} + +interface MountOptions { + AllowNonEmpty: boolean; + AllowOther: boolean; + AllowRoot: boolean; + AsyncRead: boolean; + AttrTimeout: number; + Daemon: boolean; + DaemonTimeout: number; + DebugFUSE: boolean; + DefaultPermissions: boolean; + ExtraFlags: string[]; + ExtraOptions: string[]; + MaxReadAhead: number; + NoAppleDouble: boolean; + NoAppleXattr: boolean; + VolumeName: string; + WritebackCache: boolean; + DaemonWait?: number; + DeviceName?: string; + NetworkMode?: boolean; + CaseInsensitive?: boolean | null; +} + + + +export { VfsOptions, MountOptions } + + + diff --git a/src/type/rclone/storage/parameters/crypt.d.ts b/src/type/rclone/storage/parameters/crypt.d.ts new file mode 100644 index 0000000..46144f8 --- /dev/null +++ b/src/type/rclone/storage/parameters/crypt.d.ts @@ -0,0 +1,22 @@ +import { ParamsSelectType } from "../defaults"; + + +interface CryptParamsStandard { + remote?: string; + filename_encryption?: ParamsSelectType; + directory_name_encryption?: boolean; + password?: string; + password2?: string; +} + +interface CryptParamsAdvanced { + server_side_across_configs?: boolean; + show_mapping?: boolean; + no_data_encryption?: boolean; + pass_bad_blocks?: boolean; + strict_names?: boolean; + filename_encoding?: ParamsSelectType; + suffix?: string; +} + +export{CryptParamsStandard, CryptParamsAdvanced} \ No newline at end of file diff --git a/src/type/rclone/storage/parameters/ftp.d.ts b/src/type/rclone/storage/parameters/ftp.d.ts new file mode 100644 index 0000000..0afc284 --- /dev/null +++ b/src/type/rclone/storage/parameters/ftp.d.ts @@ -0,0 +1,31 @@ +interface FtpParamsStandard { + // 标准选项 + host: string; + user?: string; + port?: number; + pass?: string; + tls?: boolean; + explicit_tls?: boolean; +} + +interface FtpParamsAdvanced{ + // 高级选项 + concurrency?: number; + no_check_certificate?: boolean; + disable_epsv?: boolean; + disable_mlsd?: boolean; + disable_utf8?: boolean; + writing_mdtm?: boolean; + force_list_hidden?: boolean; + idle_timeout?: string; // Duration 类型,此处简化为字符串 + close_timeout?: string; // Duration 类型,此处简化为字符串 + tls_cache_size?: number; + disable_tls13?: boolean; + shut_timeout?: string; // Duration 类型,此处简化为字符串 + ask_password?: boolean; + socks_proxy?: string; + encoding?: string; // 在 TypeScript 中,可能需要定义一个枚举类型来表示支持的编码格式 + description?: string; +} + +export {FtpParamsStandard,FtpParamsAdvanced} \ No newline at end of file diff --git a/src/type/rclone/storage/parameters/local.d.ts b/src/type/rclone/storage/parameters/local.d.ts new file mode 100644 index 0000000..6af6b9e --- /dev/null +++ b/src/type/rclone/storage/parameters/local.d.ts @@ -0,0 +1,25 @@ +// 定义 LocalParamsStandard 类型,这里暂无具体参数 +interface LocalParamsStandard { + // 这里可以添加未来可能会有的标准参数 +} + +// 定义 LocalParamsAdvanced 类型,包含你给出的所有高级选项 +interface LocalParamsAdvanced { + nounc: boolean, + copy_links: boolean, + links: boolean, + skip_links: boolean, + zero_size_links: boolean, + unicode_normalization: boolean, + no_check_updated: boolean, + one_file_system: boolean, + case_sensitive: boolean, + case_insensitive: boolean, + no_preallocate: boolean, + no_sparse: boolean, + no_set_modtime: boolean, + encoding: string, + description: string +} + +export{LocalParamsStandard, LocalParamsAdvanced} \ No newline at end of file diff --git a/src/type/rclone/storage/parameters/onedrive.d.ts b/src/type/rclone/storage/parameters/onedrive.d.ts new file mode 100644 index 0000000..1992172 --- /dev/null +++ b/src/type/rclone/storage/parameters/onedrive.d.ts @@ -0,0 +1,35 @@ +import { ParamsSelectType } from "../defaults"; + +interface OneDriveParamsStandard { + client_id?: string; + client_secret?: string; + region?: ParamsSelectType; +} + + +interface OneDriveParamsAdvanced { + token?: string; + auth_url?: string; + token_url?: string; + chunk_size?: string; // Assuming this to be a string for simplification + drive_id?: string; + drive_type?: ParamsSelectType; + root_folder_id?: string; + access_scopes?: string; + disable_site_permission?: boolean; + expose_onenote_files?: boolean; + server_side_across_configs?: boolean; + list_chunk?: number; + no_versions?: boolean; + link_scope?: string; + link_type?: string; + link_password?: string; + hash_type?: string; + av_override?: boolean; + delta?: boolean; + metadata_permissions?: string; + encoding?: string; + description?: string; +} + +export{OneDriveParamsStandard, OneDriveParamsAdvanced} \ No newline at end of file diff --git a/src/type/rclone/storage/parameters/s3.d.ts b/src/type/rclone/storage/parameters/s3.d.ts new file mode 100644 index 0000000..50f0ca0 --- /dev/null +++ b/src/type/rclone/storage/parameters/s3.d.ts @@ -0,0 +1,63 @@ +import { ParamsSelectType } from "../defaults"; + +interface S3ParamsStandard { + provider?: ParamsSelectType; + env_auth?: boolean; + access_key_id?: string; + secret_access_key?: string; + region?: string; + endpoint?: string; + location_constraint?: string; + acl?: string; + server_side_encryption?: string; + sse_kms_key_id?: string; + storage_class?: string; +} + +interface S3ParamsAdvanced { + bucket_acl?: string; + requester_pays?: boolean; + sse_customer_algorithm?: string; + sse_customer_key?: string; + sse_customer_key_base64?: string; + sse_customer_key_md5?: string; + upload_cutoff?: string; // Assuming this to be a string for simplification + chunk_size?: string; // Assuming this to be a string for simplification + max_upload_parts?: number; + copy_cutoff?: string; // Assuming this to be a string for simplification + disable_checksum?: boolean; + shared_credentials_file?: string; + profile?: string; + session_token?: string; + upload_concurrency?: number; + force_path_style?: boolean; + v2_auth?: boolean; + use_dual_stack?: boolean; + use_accelerate_endpoint?: boolean; + leave_parts_on_error?: boolean; + list_chunk?: number; + list_version?: number; + list_url_encode?: ParamsSelectType; + no_check_bucket?: boolean; + no_head?: boolean; + no_head_object?: boolean; + encoding?: string; + disable_http2?: boolean; + download_url?: string; + directory_markers?: boolean; + use_multipart_etag?: ParamsSelectType; + use_presigned_request?: boolean; + versions?: boolean; + version_at?: string; + version_deleted?: boolean; + decompress?: boolean; + might_gzip?: ParamsSelectType; + use_accept_encoding_gzip?: ParamsSelectType; + no_system_metadata?: boolean; + sts_endpoint?: string; + use_already_exists?: ParamsSelectType; + use_multipart_uploads?: ParamsSelectType; + description?: string; +} + +export{S3ParamsStandard, S3ParamsAdvanced} \ No newline at end of file diff --git a/src/type/rclone/storage/parameters/webdav.d.ts b/src/type/rclone/storage/parameters/webdav.d.ts new file mode 100644 index 0000000..89ed8c0 --- /dev/null +++ b/src/type/rclone/storage/parameters/webdav.d.ts @@ -0,0 +1,24 @@ +import { ParamsSelectType } from "../defaults"; + +interface WebdavParamsStandard { + // 标准选项 + url: string; + vendor?: ParamsSelectType; + user?: string; + pass?: string; + bearer_token?: string; +} + + +interface WebdavParamsAdvanced { + // 高级选项 + bearer_token_command?: string; + encoding?: string; + headers?: Array<{ key: string, value: string }>; // 也可以使用 Map 类型 + pacer_min_sleep?: string; // 使用 Duration 类型,但此处为了简化表示,我们暂时将其作为字符串 + nextcloud_chunk_size?: string; // 使用 SizeSuffix 类型,此处同样简化为字符串 + owncloud_exclude_shares?: boolean; + description?: string; +} + +export { WebdavParamsStandard, WebdavParamsAdvanced } \ No newline at end of file diff --git a/src/type/rclone/storage/storageListAll.d.ts b/src/type/rclone/storage/storageListAll.d.ts new file mode 100644 index 0000000..39b384c --- /dev/null +++ b/src/type/rclone/storage/storageListAll.d.ts @@ -0,0 +1,10 @@ +import { DefaultParams } from "./defaults"; + +interface StorageListAll{ + name: string; + type: string; + introduce?: string + defaultParams:DefaultParams +} + +export {StorageListAll} \ No newline at end of file diff --git a/src/type/routers.d.ts b/src/type/routers.d.ts new file mode 100644 index 0000000..2463283 --- /dev/null +++ b/src/type/routers.d.ts @@ -0,0 +1,11 @@ +interface Routers { + title: string | ReactNode; + path: string;//菜单:key未定义时,key=path + key?:string;//菜单:在隐藏子项的情况下,父子设置相同key,则可选择实现选择父项 + hide?: boolean; + hideChildren?:boolean; + children?: Routers[]; + component?: JSX.Element; +} + +export { Routers } \ No newline at end of file diff --git a/src/type/utils/aria2.d.ts b/src/type/utils/aria2.d.ts new file mode 100644 index 0000000..ed4d06b --- /dev/null +++ b/src/type/utils/aria2.d.ts @@ -0,0 +1,13 @@ +// 定义Aria2的属性 +interface Aria2Attrib { + state: 'doing' | 'done' | 'request' | 'error',//状态,错误:error,发送请求:request,下载中:doing,完成:done + speed: string,//速度 + percentage: number,//进度百分比 + eta: string,//剩余时间 + size: string,//总大小 + newSize: string,//已下载大小 + message: string,//当前的Aria2返回 + +} + +export {Aria2Attrib} \ No newline at end of file diff --git a/src/utils/aria2/aria2.ts b/src/utils/aria2/aria2.ts new file mode 100644 index 0000000..a10fcbe --- /dev/null +++ b/src/utils/aria2/aria2.ts @@ -0,0 +1,108 @@ +import { Child, Command } from '@tauri-apps/api/shell'; +import { takeMidStr, takeRightStr } from '../utils'; +import { Aria2Attrib } from '../../type/utils/aria2'; + +class Aria2 { + private filePath: string = ''; + private command: Command; + private process: Child | null = null; + + constructor(url: string, saveDir: string, saveName: string, thread: number = 8, callback: (attrib: Aria2Attrib) => void) { + this.filePath = saveDir + saveName; + const args = [ + '-d', saveDir, + '-o', saveName, + '-s', thread.toString(), + '-x', thread.toString(), + '--file-allocation=none', + '-c', + '--check-certificate=false', + '--force-save=false', + url + ] + + this.command = new Command('ria2c', args); + + this.command.stdout.on('data', (data) => { + const output = data.toString(); + if (output.includes('NOTICE') || output.includes('#')) { + callback(this.parseOutput(output)); + } + }); + + this.command.on('close', (data) => { + this.process = null; + const attrib: Aria2Attrib = this.parseOutput(''); + if (data.code === 0) { + attrib.state = 'done'; + } else { + attrib.state = 'error'; + } + callback(attrib); + }); + } + + // 解析aria2的命令行输出 + private parseOutput(output: string): Aria2Attrib { + let tempAria2Attrib: Aria2Attrib = { + state: 'request', + speed: '', + percentage: 0, + eta: '', + size: '', + newSize: '', + message: output + } + + if (output.includes('DL:')) {//正在下载,[#46fea8 210MiB/583MiB(36%) CN:4 DL:10MiB ETA:35s] + tempAria2Attrib.state = 'doing'; + + //速度speed,str.substring(str.indexOf("DL:") + 3, str.indexOf("iB ETA")) + 'B/S' + + + //进度百分比,Number(str.substring(str.indexOf("B(") + 2, str.indexOf("%)")))) + tempAria2Attrib.percentage = Number(takeMidStr(output, 'B(', '%)')) + + + if (output.includes('ETA')) { + tempAria2Attrib.speed = takeMidStr(output, 'DL:', 'iB ETA') + 'B/s' + //剩余时间 eta + tempAria2Attrib.eta = takeMidStr(output, 'ETA:', ']') + } else { + tempAria2Attrib.speed = takeMidStr(output, 'DL:', 'iB]') + 'B/s' + } + + //总大小,size + tempAria2Attrib.size = takeMidStr(output, '/', 'iB(') + 'B' + + //已下载大小,newSize + tempAria2Attrib.newSize = takeRightStr(takeMidStr(output, '[#', 'iB/'), ' ') + 'B' + } else { + tempAria2Attrib.state = 'request' + } + return tempAria2Attrib + } + + // 启动aria2下载 + async start(): Promise { + this.process = await this.command.spawn() + } + + // 停止aria2下载 + async stop(): Promise { + if (this.process) { + try { + await this.process.kill(); + this.process = null; + return true; + } catch (error) { + return false; + } + } else { + return false; + } + } +} + + +export { Aria2 }; \ No newline at end of file diff --git a/src/utils/rclone/process.ts b/src/utils/rclone/process.ts new file mode 100644 index 0000000..479e0fe --- /dev/null +++ b/src/utils/rclone/process.ts @@ -0,0 +1,47 @@ +import { invoke } from "@tauri-apps/api"; +import { Command } from "@tauri-apps/api/shell"; +import { rcloneInfo } from "../../services/rclone"; +import { rclone_api_post } from "./request"; + + +async function startRclone() { + if (rcloneInfo.process.child) { + await stopRclone() + } + + //rcloneInfo.endpoint.auth.user = randomString(32) + //rcloneInfo.endpoint.auth.pass = randomString(128) + + rcloneInfo.endpoint.url = 'http://localhost:' + rcloneInfo.endpoint.localhost.port.toString() + + const args = [ + 'rcd', + `--rc-addr=:${rcloneInfo.endpoint.localhost.port.toString()}`, + `--rc-user=${rcloneInfo.endpoint.auth.user}`, + `--rc-pass=${rcloneInfo.endpoint.auth.pass}`, + '--rc-allow-origin=*', + '--rc-no-auth' + ]; + + rcloneInfo.process.command = new Command('rclone', args) + + rcloneInfo.process.log='' + const addLog= (data:string) => { rcloneInfo.process.log += data ; + console.log(data); + } + + rcloneInfo.process.command.stdout.on('data',(data) => addLog(data)) + rcloneInfo.process.command.stderr.on('data', (data) => addLog(data)) + + rcloneInfo.process.child = await rcloneInfo.process.command.spawn() + +} + +async function stopRclone() { + await rclone_api_post('/core/quit') + if (rcloneInfo.process.child) { + await rcloneInfo.process.child.kill() + } +} + +export { startRclone, stopRclone } \ No newline at end of file diff --git a/src/utils/rclone/request.ts b/src/utils/rclone/request.ts new file mode 100644 index 0000000..85728f1 --- /dev/null +++ b/src/utils/rclone/request.ts @@ -0,0 +1,66 @@ +import { Message } from "@arco-design/web-react"; +import { rcloneInfo } from "../../services/rclone"; + +let rcloneApiHeaders = { + Authorization: `Basic ${btoa(`${rcloneInfo.endpoint.auth.user}:${rcloneInfo.endpoint.auth.pass}`)}`, + 'Content-Type': 'application/json' +}; + + +function rclone_api_post(path: string, data?: object, ignoreError?: boolean) { + + + if (!data) data = {} + + // 以 base64 编码的方式来设置账密字符串 + const base64Credentials = btoa(`${rcloneInfo.endpoint.auth.user}:${rcloneInfo.endpoint.auth.pass}`); + + // 定义请求头部,包括授权头部 + rcloneApiHeaders = { + Authorization: `Basic ${base64Credentials}`, + 'Content-Type': 'application/json' + }; + + return fetch(rcloneInfo.endpoint.url + path, { + method: 'POST', + headers: rcloneApiHeaders, + body: JSON.stringify(data) + }).then((response) => { + if (!response.ok && !ignoreError) { + printError(response); + } + return response.json(); + }).then((jsonResponse) => { + return jsonResponse; + }).catch((error) => { + if (ignoreError) { return } + printError(error); + }); +} + +async function printError(error: Response) { + console.log(error); + + let str = '' + + if (error.status) { + str += `HTTP ${error.status} - ${error.statusText}\n` + } + if (error.body) { + str += "\n" + (await error.json()).error; + } + if (str) { + Message.error('Error:' + str); + } +} + +/* export function rclone_api_get(path:string){ + return fetch(rcloneApiEndpoint + path,{ + method: 'GET', + headers +}).then((res)=>{ + return res.json() + }) +} */ + +export { rclone_api_post, rcloneApiHeaders } \ No newline at end of file diff --git a/src/utils/tauri/cmd.ts b/src/utils/tauri/cmd.ts new file mode 100644 index 0000000..db0020e --- /dev/null +++ b/src/utils/tauri/cmd.ts @@ -0,0 +1,30 @@ +import { Command } from '@tauri-apps/api/shell'; + +async function runCmd(cmd: string, args: string[]): Promise { + // Create a new Command instance with the provided command and arguments. + const commandInstance = new Command(cmd, args); + + try { + // Execute the command and wait for its completion. + const result = await commandInstance.execute(); + console.log(result); + + // Ensure the command execution was successful. + if (result.code === 0) { + // Return the command's output as a string. + return result.stdout; + } else { + throw new Error(`Command failed with exit code ${result.code}: ${cmd} ${args.join(' ')}\nError: ${result.stderr}`); + } + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Failed to execute command: ${cmd} ${args.join(' ')}\n${error.message}`); + } else { + throw new Error(`Unknown error occurred while executing command: ${cmd} ${args.join(' ')}`); + } + } +} + +export default runCmd; + +export { runCmd } \ No newline at end of file diff --git a/src/utils/tauri/osInfo.ts b/src/utils/tauri/osInfo.ts new file mode 100644 index 0000000..6945c15 --- /dev/null +++ b/src/utils/tauri/osInfo.ts @@ -0,0 +1,15 @@ +import { os } from "@tauri-apps/api"; +import { OSInfo } from "../../type/config"; +import { setOsInfo } from "../../services/config"; + +async function getOsInfo() { + setOsInfo({ + arch: await os.arch(), + osType: await os.type(), + platform: await os.platform(), + tempDir: await os.tempdir(), + osVersion: await os.version(), + }) +} + +export { getOsInfo } \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..3f5e0cf --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,105 @@ +import { fs, invoke } from "@tauri-apps/api"; +import { runCmd } from "./tauri/cmd"; + +export function isEmptyObject(back: any): boolean { + return Object.keys(back).length === 0 && back.constructor === Object; +} + +export function getURLSearchParam(name: string): string { + const searchParams = new URLSearchParams(window.location.search); + return searchParams.get(name) || ''; +} + +export function getProperties(obj: Record) { + + let result: Array<{ key: any, value: any }> = [] + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + result.push({ key: key, value: obj[key] }) + } + } + + return result +} + +export function formatSize(v: number) { + let UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'ZB']; + let prev = 0, i = 0; + while (Math.floor(v) > 0 && i < UNITS.length) { + prev = v; + v /= 1024; + i += 1; + } + + if (i > 0 && i < UNITS.length) { + v = prev; + i -= 1; + } + return Math.round(v * 100) / 100 + ' ' + UNITS[i]; +} + +//格式化剩余时间 +export function formatETA(etaInSeconds: number): string { + if (isNaN(etaInSeconds) || etaInSeconds <= 0) { + return '未知'; + } + + const hours = Math.floor(etaInSeconds / 3600); + const minutes = Math.floor((etaInSeconds % 3600) / 60); + const seconds = Math.floor(etaInSeconds % 60); + + let formattedETA = ''; + + if (hours > 0) { + formattedETA += `${hours.toString().padStart(2, '0')}h `; + } + if (minutes > 0) { + formattedETA += `${minutes.toString().padStart(2, '0')}m `; + } + + formattedETA += `${seconds.toString().padStart(2, '0')}s`; + + return formattedETA; +} + +export function randomString(length: number): string { + const alphanumericChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + //const specialChars = '!@#$%^&*()_+~`|}{[]:;?><,./-='; + const specialChars = ''; + const getRandomChar = (chars: string): string => chars[Math.floor(Math.random() * chars.length)]; + + const randomString = Array.from({ length }, () => + Math.random() < 0.8 ? getRandomChar(alphanumericChars) : getRandomChar(specialChars) + ).join(''); + + return randomString; +} + +export function takeMidStr(input: string, startMarker: string, endMarker: string): string { + const startIndex = input.indexOf(startMarker) + startMarker.length; + const endIndex = input.indexOf(endMarker, startIndex); + return input.substring(startIndex, endIndex); +} + +//取字符串右边 +export function takeRightStr(str: string, taggedStr: string) { + return str.substring(str.indexOf(taggedStr) + taggedStr.length, str.length) +} + +//下载文件 +export async function downloadFile(url: string, path: string) { + await invoke('download_file', { + url: url, + outPath: path + }) + return await fs.exists(path) +} + +export async function getWinFspInstallState() { + return await invoke('get_winfsp_install_state') as boolean +} + +export async function installWinFsp() { + await runCmd('msiexec',['/i', 'res\\bin\\winfsp.msi','/qn']) +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +///