mirror of
https://github.com/VirtualHotBar/NetMount.git
synced 2026-06-09 08:02:20 +08:00
feat: #53 支持本地代理 - 添加HTTP/SOCKS5代理配置和VFS缓存刷新功能
This commit is contained in:
@@ -33,6 +33,21 @@ async function reupMount(noRefreshUI?: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有已挂载存储的 VFS 目录缓存并刷新挂载列表
|
||||
* 解决远程添加文件后本地不显示的问题
|
||||
*/
|
||||
async function refreshMountWithVfsCache(): Promise<void> {
|
||||
try {
|
||||
// 先清除所有 VFS 缓存,强制 rclone 重新读取远程目录
|
||||
await mountRepository.forgetAllVfsCache()
|
||||
// 再刷新挂载列表
|
||||
await mountRepository.refreshMountList()
|
||||
} catch (error) {
|
||||
mountLogger.error('Failed to refresh mount with VFS cache', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取挂载配置
|
||||
*/
|
||||
@@ -136,6 +151,7 @@ async function getAvailableDriveLetter(): Promise<string> {
|
||||
|
||||
export {
|
||||
reupMount,
|
||||
refreshMountWithVfsCache,
|
||||
mountStorage,
|
||||
unmountStorage,
|
||||
addMountStorage,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Advanced Settings Component
|
||||
* 高级设置组件(启动参数)
|
||||
* 高级设置组件(网络代理、启动参数)
|
||||
*/
|
||||
|
||||
import { Button, Collapse, Form, Input, Message, Modal } from '@arco-design/web-react'
|
||||
import { Button, Collapse, Form, Input, InputNumber, Message, Modal, Select } from '@arco-design/web-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { nmConfig, saveNmConfig } from '../../../services/ConfigService'
|
||||
import { useSettingsStore } from '../../../stores/useSettingsStore'
|
||||
@@ -15,9 +15,99 @@ export function AdvancedSettings(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const { increment: incrementSettings } = useSettingsStore()
|
||||
|
||||
const proxy = nmConfig.settings.proxy
|
||||
const proxyType = proxy?.type || 'no_proxy'
|
||||
|
||||
return (
|
||||
<Form autoComplete="off" style={{ paddingRight: '0.8rem' }}>
|
||||
<Collapse bordered={false} style={{ marginBottom: '0.75rem' }}>
|
||||
<Collapse.Item name="proxy_settings" header={t('proxy_settings')}>
|
||||
<FormItem label={t('proxy_type')}>
|
||||
<Select
|
||||
value={proxyType}
|
||||
onChange={value => {
|
||||
if (value === 'no_proxy') {
|
||||
nmConfig.settings.proxy = undefined
|
||||
} else {
|
||||
nmConfig.settings.proxy = {
|
||||
type: value as 'http' | 'socks5',
|
||||
host: proxy?.host || '',
|
||||
port: proxy?.port || (value === 'socks5' ? 1080 : 8080),
|
||||
username: proxy?.username,
|
||||
password: proxy?.password,
|
||||
}
|
||||
}
|
||||
incrementSettings()
|
||||
}}
|
||||
style={{ width: '12rem' }}
|
||||
>
|
||||
<Select.Option value="no_proxy">{t('no_proxy')}</Select.Option>
|
||||
<Select.Option value="http">HTTP</Select.Option>
|
||||
<Select.Option value="socks5">SOCKS5</Select.Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
{proxyType !== 'no_proxy' && (
|
||||
<>
|
||||
<FormItem label={t('proxy_host')}>
|
||||
<Input
|
||||
value={proxy?.host || ''}
|
||||
placeholder={t('proxy_host_placeholder')}
|
||||
onChange={value => {
|
||||
if (nmConfig.settings.proxy) {
|
||||
nmConfig.settings.proxy.host = value
|
||||
incrementSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('proxy_port')}>
|
||||
<InputNumber
|
||||
value={proxy?.port}
|
||||
min={1}
|
||||
max={65535}
|
||||
placeholder={t('proxy_port_placeholder')}
|
||||
onChange={value => {
|
||||
if (nmConfig.settings.proxy) {
|
||||
nmConfig.settings.proxy.port = value || undefined
|
||||
incrementSettings()
|
||||
}
|
||||
}}
|
||||
style={{ width: '12rem' }}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('proxy_username')}>
|
||||
<Input
|
||||
value={proxy?.username || ''}
|
||||
placeholder={t('optional')}
|
||||
onChange={value => {
|
||||
if (nmConfig.settings.proxy) {
|
||||
nmConfig.settings.proxy.username = value || undefined
|
||||
incrementSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('proxy_password')}>
|
||||
<Input.Password
|
||||
value={proxy?.password || ''}
|
||||
placeholder={t('optional')}
|
||||
onChange={value => {
|
||||
if (nmConfig.settings.proxy) {
|
||||
nmConfig.settings.proxy.password = value || undefined
|
||||
incrementSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--color-text-3)', marginTop: '0.25rem', marginBottom: '0.75rem' }}>
|
||||
{t('proxy_settings_hint')}
|
||||
</div>
|
||||
</Collapse.Item>
|
||||
|
||||
<Collapse.Item name="extra_startup_args" header={t('extra_startup_args')}>
|
||||
<FormItem label={'Rclone'}>
|
||||
<Input
|
||||
|
||||
@@ -241,6 +241,28 @@ export async function performMount(mountInfo: MountListItem): Promise<void> {
|
||||
await refreshMountList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有已挂载存储的 VFS 目录缓存
|
||||
* 用于刷新操作,强制 rclone 重新从远程读取目录列表
|
||||
*/
|
||||
export async function forgetAllVfsCache(): Promise<void> {
|
||||
const mounts = rcloneInfo.mountList
|
||||
if (mounts.length === 0) return
|
||||
|
||||
const forgetPromises = mounts.map(async (mount) => {
|
||||
try {
|
||||
await rclone_api_post('/vfs/forget', {
|
||||
fs: convertStoragePath(mount.storageName) || mount.storageName,
|
||||
}, true)
|
||||
} catch {
|
||||
// 忽略 - 非关键操作
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(forgetPromises)
|
||||
mountLogger.debug('VFS cache forgotten for all mounts', { count: mounts.length })
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行卸载操作
|
||||
* 卸载前先清理 VFS 缓存引用,卸载后清理残留临时文件
|
||||
|
||||
7
src/type/config.d.ts
vendored
7
src/type/config.d.ts
vendored
@@ -25,6 +25,13 @@ interface NMConfig {
|
||||
lockOnSleep?: boolean // 休眠时锁定
|
||||
idleTimeoutMinutes?: number // 空闲超时锁定(分钟),0或undefined表示禁用
|
||||
}
|
||||
proxy?: {
|
||||
type: 'no_proxy' | 'http' | 'socks5' // 代理类型
|
||||
host?: string // 代理主机地址
|
||||
port?: number // 代理端口
|
||||
username?: string // 代理用户名(可选)
|
||||
password?: string // 代理密码(可选)
|
||||
}
|
||||
}
|
||||
notice?: Notice
|
||||
framework: {
|
||||
|
||||
@@ -12,6 +12,29 @@ import { netmountLogDir, rcloneConfigFile, rcloneLogFile } from '../netmountPath
|
||||
import { restartSidecar, startSidecarAndWait, stopSidecarGracefully } from '../sidecarService'
|
||||
import { parseExtraCliArgs } from '../cliArgs'
|
||||
|
||||
/**
|
||||
* 构建代理URL
|
||||
* 根据代理配置生成 rclone --http-proxy 参数值
|
||||
* 支持 HTTP 和 SOCKS5 代理,格式:protocol://[user:pass@]host:port
|
||||
*/
|
||||
function buildProxyUrl(proxy: { type: string; host?: string; port?: number; username?: string; password?: string }): string | undefined {
|
||||
if (proxy.type === 'no_proxy' || !proxy.host || !proxy.port) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const protocol = proxy.type === 'socks5' ? 'socks5' : 'http'
|
||||
let auth = ''
|
||||
if (proxy.username) {
|
||||
auth = encodeURIComponent(proxy.username)
|
||||
if (proxy.password) {
|
||||
auth += ':' + encodeURIComponent(proxy.password)
|
||||
}
|
||||
auth += '@'
|
||||
}
|
||||
|
||||
return `${protocol}://${auth}${proxy.host}:${proxy.port}`
|
||||
}
|
||||
|
||||
async function startRclone() {
|
||||
if (rcloneInfo.process.child) {
|
||||
await stopRclone()
|
||||
@@ -67,6 +90,17 @@ async function startRclone() {
|
||||
if (nmConfig.framework.rclone.user === '') {
|
||||
args.push('--rc-no-auth')
|
||||
}
|
||||
|
||||
// 应用代理配置
|
||||
const proxy = nmConfig.settings.proxy
|
||||
if (proxy && proxy.type !== 'no_proxy') {
|
||||
const proxyUrl = buildProxyUrl(proxy)
|
||||
if (proxyUrl) {
|
||||
args.push(`--http-proxy=${proxyUrl}`)
|
||||
logger.info('Rclone proxy configured', 'Rclone', { type: proxy.type, host: proxy.host })
|
||||
}
|
||||
}
|
||||
|
||||
args.push(...parseExtraCliArgs(nmConfig.framework.rclone.extraArgs))
|
||||
|
||||
// 使用 Rust 端启动 sidecar,确保由主进程创建
|
||||
|
||||
Reference in New Issue
Block a user