mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-06 18:22:44 +08:00
feat: add WebDAV mapping configuration and UI in System Settings
This commit is contained in:
@@ -20,6 +20,16 @@ from services.virtual_fs import (
|
||||
copy_path,
|
||||
stream_file,
|
||||
)
|
||||
from services.config import ConfigCenter
|
||||
|
||||
|
||||
_WEBDAV_ENABLED_KEY = "WEBDAV_MAPPING_ENABLED"
|
||||
|
||||
|
||||
async def _ensure_webdav_enabled() -> None:
|
||||
enabled = await ConfigCenter.get(_WEBDAV_ENABLED_KEY, "1")
|
||||
if str(enabled).strip().lower() in ("0", "false", "off", "no"):
|
||||
raise HTTPException(503, detail="WebDAV mapping disabled")
|
||||
|
||||
|
||||
router = APIRouter(prefix="/webdav", tags=["webdav"])
|
||||
@@ -140,12 +150,17 @@ def _normalize_fs_path(path: str) -> str:
|
||||
|
||||
|
||||
@router.options("/{path:path}")
|
||||
async def options_root(path: str = ""):
|
||||
async def options_root(path: str = "", _enabled: None = Depends(_ensure_webdav_enabled)):
|
||||
return Response(status_code=200, headers=_dav_headers())
|
||||
|
||||
|
||||
@router.api_route("/{path:path}", methods=["PROPFIND"])
|
||||
async def propfind(request: Request, path: str, user: User = Depends(_get_basic_user)):
|
||||
async def propfind(
|
||||
request: Request,
|
||||
path: str,
|
||||
_enabled: None = Depends(_ensure_webdav_enabled),
|
||||
user: User = Depends(_get_basic_user),
|
||||
):
|
||||
full_path = _normalize_fs_path(path)
|
||||
depth = request.headers.get("Depth", "1").lower()
|
||||
if depth not in ("0", "1", "infinity"):
|
||||
@@ -187,14 +202,23 @@ async def propfind(request: Request, path: str, user: User = Depends(_get_basic_
|
||||
|
||||
|
||||
@router.get("/{path:path}")
|
||||
async def dav_get(path: str, request: Request, user: User = Depends(_get_basic_user)):
|
||||
async def dav_get(
|
||||
path: str,
|
||||
request: Request,
|
||||
_enabled: None = Depends(_ensure_webdav_enabled),
|
||||
user: User = Depends(_get_basic_user),
|
||||
):
|
||||
full_path = _normalize_fs_path(path)
|
||||
range_header = request.headers.get("Range")
|
||||
return await stream_file(full_path, range_header)
|
||||
|
||||
|
||||
@router.head("/{path:path}")
|
||||
async def dav_head(path: str, user: User = Depends(_get_basic_user)):
|
||||
async def dav_head(
|
||||
path: str,
|
||||
_enabled: None = Depends(_ensure_webdav_enabled),
|
||||
user: User = Depends(_get_basic_user),
|
||||
):
|
||||
full_path = _normalize_fs_path(path)
|
||||
try:
|
||||
st = await stat_file(full_path)
|
||||
@@ -216,7 +240,12 @@ async def dav_head(path: str, user: User = Depends(_get_basic_user)):
|
||||
|
||||
|
||||
@router.api_route("/{path:path}", methods=["PUT"])
|
||||
async def dav_put(path: str, request: Request, user: User = Depends(_get_basic_user)):
|
||||
async def dav_put(
|
||||
path: str,
|
||||
request: Request,
|
||||
_enabled: None = Depends(_ensure_webdav_enabled),
|
||||
user: User = Depends(_get_basic_user),
|
||||
):
|
||||
full_path = _normalize_fs_path(path)
|
||||
async def body_iter():
|
||||
async for chunk in request.stream():
|
||||
@@ -227,14 +256,22 @@ async def dav_put(path: str, request: Request, user: User = Depends(_get_basic_u
|
||||
|
||||
|
||||
@router.api_route("/{path:path}", methods=["DELETE"])
|
||||
async def dav_delete(path: str, user: User = Depends(_get_basic_user)):
|
||||
async def dav_delete(
|
||||
path: str,
|
||||
_enabled: None = Depends(_ensure_webdav_enabled),
|
||||
user: User = Depends(_get_basic_user),
|
||||
):
|
||||
full_path = _normalize_fs_path(path)
|
||||
await delete_path(full_path)
|
||||
return Response(status_code=204, headers=_dav_headers())
|
||||
|
||||
|
||||
@router.api_route("/{path:path}", methods=["MKCOL"])
|
||||
async def dav_mkcol(path: str, user: User = Depends(_get_basic_user)):
|
||||
async def dav_mkcol(
|
||||
path: str,
|
||||
_enabled: None = Depends(_ensure_webdav_enabled),
|
||||
user: User = Depends(_get_basic_user),
|
||||
):
|
||||
full_path = _normalize_fs_path(path)
|
||||
await make_dir(full_path)
|
||||
return Response(status_code=201, headers=_dav_headers())
|
||||
@@ -270,4 +307,3 @@ async def dav_copy(path: str, request: Request, user: User = Depends(_get_basic_
|
||||
overwrite = request.headers.get("Overwrite", "T").upper() != "F"
|
||||
await copy_path(full_src, dst, overwrite=overwrite)
|
||||
return Response(status_code=201 if not overwrite else 204, headers=_dav_headers())
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ export const en = {
|
||||
'App Settings': 'App Settings',
|
||||
'Email Settings': 'Email Settings',
|
||||
'AI Settings': 'AI Settings',
|
||||
'Protocol Mappings': 'Protocol Mappings',
|
||||
'Vision Model': 'Vision Model',
|
||||
'Embedding Model': 'Embedding Model',
|
||||
'Embedding Dimension': 'Embedding Dimension',
|
||||
@@ -332,6 +333,14 @@ export const en = {
|
||||
'Favicon URL': 'Favicon URL',
|
||||
'App Domain': 'App Domain',
|
||||
'File Domain': 'File Domain',
|
||||
'WebDAV Mapping': 'WebDAV Mapping',
|
||||
'WebDAV Endpoint': 'WebDAV Endpoint',
|
||||
'Basic (system account password)': 'Basic (system account password)',
|
||||
'Root Path': 'Root Path',
|
||||
'Client Compatibility': 'Client Compatibility',
|
||||
'Supports Finder, Windows network drive, rclone, and other WebDAV clients.': 'Supports Finder, Windows network drive, rclone, and other WebDAV clients.',
|
||||
'Toggle the switch to expose the virtual file system via WebDAV.': 'Toggle the switch to expose the virtual file system via WebDAV.',
|
||||
'S3 Mapping': 'S3 Mapping',
|
||||
'SMTP Settings': 'SMTP Settings',
|
||||
'SMTP Host': 'SMTP Host',
|
||||
'Please input SMTP host': 'Please input SMTP host',
|
||||
|
||||
@@ -307,6 +307,7 @@ export const zh = {
|
||||
'App Settings': '应用设置',
|
||||
'Email Settings': '邮箱设置',
|
||||
'AI Settings': 'AI设置',
|
||||
'Protocol Mappings': '映射协议',
|
||||
'Choose Template': '选择模板',
|
||||
'Configure Provider': '配置提供商',
|
||||
'Back to Templates': '返回选择',
|
||||
@@ -357,6 +358,14 @@ export const zh = {
|
||||
'Favicon URL': 'Favicon 地址',
|
||||
'App Domain': '应用域名',
|
||||
'File Domain': '文件域名',
|
||||
'WebDAV Mapping': 'WebDAV 映射',
|
||||
'WebDAV Endpoint': 'WebDAV 访问地址',
|
||||
'Basic (system account password)': 'Basic(系统账号密码)',
|
||||
'Root Path': '根路径',
|
||||
'Client Compatibility': '客户端兼容性',
|
||||
'Supports Finder, Windows network drive, rclone, and other WebDAV clients.': '兼容 Finder、Windows 网络驱动器、rclone 等 WebDAV 客户端。',
|
||||
'Toggle the switch to expose the virtual file system via WebDAV.': '通过开关控制是否对外暴露虚拟文件系统的 WebDAV 协议。',
|
||||
'S3 Mapping': 'S3 映射',
|
||||
'SMTP Settings': 'SMTP 配置',
|
||||
'SMTP Host': 'SMTP 服务器',
|
||||
'Please input SMTP host': '请输入 SMTP 服务器',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { message, Tabs, Space } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import PageCard from '../../components/PageCard';
|
||||
import { getAllConfig, setConfig } from '../../api/config';
|
||||
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined, MailOutlined, CloudSyncOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import '../../styles/settings-tabs.css';
|
||||
import { useI18n } from '../../i18n';
|
||||
@@ -11,10 +11,11 @@ import AppSettingsTab from './components/AppSettingsTab';
|
||||
import AiSettingsTab from './components/AiSettingsTab';
|
||||
import VectorDbSettingsTab from './components/VectorDbSettingsTab';
|
||||
import EmailSettingsTab from './components/EmailSettingsTab';
|
||||
import ProtocolMappingsTab from './components/ProtocolMappingsTab';
|
||||
|
||||
type TabKey = 'appearance' | 'app' | 'email' | 'ai' | 'vector-db';
|
||||
type TabKey = 'appearance' | 'app' | 'email' | 'ai' | 'vector-db' | 'mappings';
|
||||
|
||||
const TAB_KEYS: TabKey[] = ['appearance', 'app', 'email', 'ai', 'vector-db'];
|
||||
const TAB_KEYS: TabKey[] = ['appearance', 'app', 'email', 'ai', 'vector-db', 'mappings'];
|
||||
const DEFAULT_TAB: TabKey = 'appearance';
|
||||
|
||||
const isValidTab = (key?: string): key is TabKey => !!key && (TAB_KEYS as string[]).includes(key);
|
||||
@@ -191,6 +192,22 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
|
||||
<VectorDbSettingsTab isActive={activeTab === 'vector-db'} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'mappings',
|
||||
label: (
|
||||
<span>
|
||||
<CloudSyncOutlined style={{ marginRight: 8 }} />
|
||||
{t('Protocol Mappings')}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<ProtocolMappingsTab
|
||||
config={config}
|
||||
loading={loading}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Card, Descriptions, Space, Switch, Typography } from 'antd';
|
||||
import { useI18n } from '../../../i18n';
|
||||
|
||||
interface ProtocolMappingsTabProps {
|
||||
config: Record<string, string>;
|
||||
loading: boolean;
|
||||
onSave: (values: Record<string, unknown>) => Promise<void>;
|
||||
}
|
||||
|
||||
const WEBDAV_KEY = 'WEBDAV_MAPPING_ENABLED';
|
||||
|
||||
const truthy = new Set(['1', 'true', 'yes', 'on']);
|
||||
|
||||
export default function ProtocolMappingsTab({ config, loading, onSave }: ProtocolMappingsTabProps) {
|
||||
const { t } = useI18n();
|
||||
const [webdavEnabled, setWebdavEnabled] = useState(() => truthy.has((config[WEBDAV_KEY] ?? '1').toLowerCase()));
|
||||
const [webdavSaving, setWebdavSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setWebdavEnabled(truthy.has((config[WEBDAV_KEY] ?? '1').toLowerCase()));
|
||||
}, [config]);
|
||||
|
||||
const webdavEndpoint = useMemo(() => {
|
||||
const configured = (config.APP_DOMAIN ?? '').trim();
|
||||
if (configured) {
|
||||
const hasProtocol = configured.startsWith('http://') || configured.startsWith('https://');
|
||||
const base = hasProtocol ? configured : `https://${configured}`;
|
||||
return base.replace(/\/$/, '') + '/webdav';
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.origin.replace(/\/$/, '') + '/webdav';
|
||||
}
|
||||
return '/webdav';
|
||||
}, [config.APP_DOMAIN]);
|
||||
|
||||
const handleToggleWebdav = async (checked: boolean) => {
|
||||
setWebdavSaving(true);
|
||||
try {
|
||||
await onSave({ [WEBDAV_KEY]: checked ? '1' : '0' });
|
||||
setWebdavEnabled(checked);
|
||||
} finally {
|
||||
setWebdavSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Card
|
||||
title={t('WebDAV Mapping')}
|
||||
extra={(
|
||||
<Space size={12} align="center">
|
||||
<Switch
|
||||
checked={webdavEnabled}
|
||||
loading={webdavSaving}
|
||||
disabled={loading}
|
||||
onChange={handleToggleWebdav}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
<Descriptions
|
||||
column={1}
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'endpoint',
|
||||
label: t('WebDAV Endpoint'),
|
||||
children: (
|
||||
<Typography.Text copyable={{ text: webdavEndpoint }}>
|
||||
<code>{webdavEndpoint}</code>
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'auth',
|
||||
label: t('Authentication'),
|
||||
children: t('Basic (system account password)'),
|
||||
},
|
||||
{
|
||||
key: 'root',
|
||||
label: t('Root Path'),
|
||||
children: '/webdav',
|
||||
},
|
||||
{
|
||||
key: 'compat',
|
||||
label: t('Client Compatibility'),
|
||||
children: t('Supports Finder, Windows network drive, rclone, and other WebDAV clients.'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Typography.Text type="secondary">
|
||||
{t('Toggle the switch to expose the virtual file system via WebDAV.')}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
|
||||
<Card title={t('S3 Mapping')}>
|
||||
<Typography.Text type="secondary">{t('Coming soon')}</Typography.Text>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user