diff --git a/api/modules/remote/service.ts b/api/modules/remote/service.ts index 6251c0b..b507c60 100644 --- a/api/modules/remote/service.ts +++ b/api/modules/remote/service.ts @@ -73,6 +73,7 @@ export class RemoteService { serverHost: deviceConfig.serverHost || '', desktopPort: deviceConfig.desktopPort || 3000, gitPort: deviceConfig.gitPort || 3001, + password: deviceConfig.password || '', } } catch { return { @@ -81,6 +82,7 @@ export class RemoteService { serverHost: '', desktopPort: 3000, gitPort: 3001, + password: '', } } }) @@ -114,6 +116,7 @@ export class RemoteService { serverHost: device.serverHost, desktopPort: device.desktopPort, gitPort: device.gitPort, + password: device.password || '', } await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), 'utf-8') } diff --git a/remote/src/routes/files.js b/remote/src/routes/files.js index 11a89ee..cee51c7 100644 --- a/remote/src/routes/files.js +++ b/remote/src/routes/files.js @@ -17,14 +17,25 @@ router.get('/', (req, res) => { } }); +router.get('/drives', (req, res) => { + try { + const drives = fileService.getDrives(); + res.json({ items: drives }); + } catch (error) { + logger.error('Failed to get drives', { error: error.message }); + res.status(500).json({ error: 'Failed to get drives' }); + } +}); + router.get('/browse', (req, res) => { try { - const path = req.query.path || ''; - const result = fileService.browseDirectory(path); + const filePath = req.query.path || ''; + const allowSystem = req.query.allowSystem === 'true'; + const result = fileService.browseDirectory(filePath, allowSystem); res.json(result); } catch (error) { - logger.error('Failed to browse directory', { error: error.message }); - res.status(500).json({ error: 'Failed to browse directory' }); + logger.error('Failed to browse directory', { error: error.message, stack: error.stack }); + res.status(500).json({ error: error.message || 'Failed to browse directory' }); } }); diff --git a/remote/src/services/file/FileService.js b/remote/src/services/file/FileService.js index 0ebdd7f..81e982b 100644 --- a/remote/src/services/file/FileService.js +++ b/remote/src/services/file/FileService.js @@ -136,48 +136,78 @@ class FileService { } } - browseDirectory(relativePath = '') { - try { + getDrives() { + const drives = []; + const letters = 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + for (const letter of letters) { + const drivePath = `${letter}:\\`; + try { + fs.accessSync(drivePath); + drives.push({ name: `${letter}:`, isDirectory: true, size: 0 }); + } catch {} + } + return drives; + } + + browseDirectory(relativePath = '', allowSystem = false) { + let targetDir; + let currentPath; + + if (allowSystem) { + currentPath = path.normalize(relativePath || '').replace(/^(\.\.(\/|\\|$))+/, ''); + if (!currentPath) { + currentPath = ''; + } + targetDir = currentPath || 'C:\\'; + } else { const safePath = path.normalize(relativePath || '').replace(/^(\.\.(\/|\\|$))+/, ''); - const targetDir = path.join(this.uploadDir, safePath); - + targetDir = path.join(this.uploadDir, safePath); + currentPath = safePath; + if (!targetDir.startsWith(this.uploadDir)) { return { error: 'Access denied', items: [], currentPath: '' }; } - - if (!fs.existsSync(targetDir)) { - return { error: 'Directory not found', items: [], currentPath: safePath }; - } - - const items = fs.readdirSync(targetDir).map(name => { - const itemPath = path.join(targetDir, name); - const stat = fs.statSync(itemPath); - const isDirectory = stat.isDirectory(); - - return { - name, - isDirectory, - size: isDirectory ? 0 : stat.size, - modified: stat.mtime, - type: isDirectory ? 'directory' : path.extname(name) - }; - }); - - items.sort((a, b) => { - if (a.isDirectory && !b.isDirectory) return -1; - if (!a.isDirectory && b.isDirectory) return 1; - return a.name.localeCompare(b.name); - }); - - return { - items, - currentPath: safePath, - parentPath: safePath ? path.dirname(safePath) : null - }; - } catch (error) { - logger.error('Failed to browse directory', { error: error.message }); - return { error: error.message, items: [], currentPath: relativePath }; } + + const items = []; + + try { + const files = fs.readdirSync(targetDir); + + for (const name of files) { + try { + const itemPath = path.join(targetDir, name); + const stat = fs.statSync(itemPath); + const isDirectory = stat.isDirectory(); + items.push({ + name, + isDirectory, + size: isDirectory ? 0 : stat.size, + modified: stat.mtime, + type: isDirectory ? 'directory' : path.extname(name) + }); + } catch (err) { + logger.debug('Skipped inaccessible file', { name, error: err.message }); + } + } + } catch (err) { + logger.warn('Failed to read directory', { targetDir, error: err.message }); + return { items: [], currentPath: currentPath, parentPath: path.dirname(currentPath) || null }; + } + + items.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + const parentPath = currentPath ? path.dirname(currentPath) : null; + + return { + items, + currentPath: currentPath, + parentPath: parentPath === currentPath ? null : parentPath + }; } } diff --git a/src/modules/remote/api.ts b/src/modules/remote/api.ts index 4a7d830..e72de0b 100644 --- a/src/modules/remote/api.ts +++ b/src/modules/remote/api.ts @@ -72,9 +72,10 @@ export const fetchRemoteFiles = async ( serverHost: string, port: number, path: string, - password?: string + password?: string, + allowSystem: boolean = true ): Promise => { - let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(path)}` + let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(path)}&allowSystem=${allowSystem}` if (password) { url += `&password=${encodeURIComponent(password)}` } @@ -93,6 +94,30 @@ export const fetchRemoteFiles = async ( })) } +export const fetchRemoteDrives = async ( + serverHost: string, + port: number, + password?: string +): Promise => { + let url = `http://${serverHost}:${port}/api/files/drives` + if (password) { + url += `?password=${encodeURIComponent(password)}` + } + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to fetch remote drives: ${response.statusText}`) + } + const data = await response.json() + const items = data.items || [] + return items.map((item: { name: string; isDirectory: boolean; size: number }) => ({ + name: item.name, + path: item.name, + type: item.isDirectory ? 'dir' : 'file', + size: item.size, + modified: '', + })) +} + export const uploadFileToRemote = async ( serverHost: string, port: number, diff --git a/src/modules/remote/components/file-transfer/RemoteFilePanel.tsx b/src/modules/remote/components/file-transfer/RemoteFilePanel.tsx index 667ec24..6ae1a03 100644 --- a/src/modules/remote/components/file-transfer/RemoteFilePanel.tsx +++ b/src/modules/remote/components/file-transfer/RemoteFilePanel.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react' -import { Folder, FileText, ChevronLeft, RefreshCw } from 'lucide-react' +import { Folder, FileText, ChevronLeft, RefreshCw, HardDrive } from 'lucide-react' import { clsx } from 'clsx' -import { fetchRemoteFiles, type RemoteFileItem } from '../../api' +import { fetchRemoteFiles, fetchRemoteDrives, type RemoteFileItem } from '../../api' interface RemoteFilePanelProps { serverHost: string @@ -26,12 +26,28 @@ export const RemoteFilePanel: React.FC = ({ const [files, setFiles] = useState([]) const [loading, setLoading] = useState(false) const [pathHistory, setPathHistory] = useState(['']) + const [showDrives, setShowDrives] = useState(true) + + const loadDrives = useCallback(async () => { + setLoading(true) + try { + const drives = await fetchRemoteDrives(serverHost, port, password) + setFiles(drives) + setShowDrives(true) + } catch (err) { + console.error('Failed to load remote drives:', err) + setFiles([]) + } finally { + setLoading(false) + } + }, [serverHost, port, password]) const loadFiles = useCallback(async (path: string) => { setLoading(true) try { - const items = await fetchRemoteFiles(serverHost, port, path, password) + const items = await fetchRemoteFiles(serverHost, port, path, password, true) setFiles(items) + setShowDrives(false) } catch (err) { console.error('Failed to load remote files:', err) setFiles([]) @@ -41,10 +57,13 @@ export const RemoteFilePanel: React.FC = ({ }, [serverHost, port, password]) useEffect(() => { - loadFiles(currentPath) - }, [currentPath, loadFiles]) + loadDrives() + }, [loadDrives]) const handleGoBack = () => { + if (showDrives) { + return + } if (pathHistory.length > 1) { const newHistory = [...pathHistory] newHistory.pop() @@ -52,24 +71,45 @@ export const RemoteFilePanel: React.FC = ({ setPathHistory(newHistory) setCurrentPath(prevPath) onSelect(null) + } else { + loadDrives() } } const handleGoInto = (file: RemoteFileItem) => { - const newPath = currentPath ? `${currentPath}/${file.name}` : file.name - setPathHistory([...pathHistory, newPath]) - setCurrentPath(newPath) + if (showDrives) { + const newPath = file.path + '\\' + setPathHistory(['', newPath]) + setCurrentPath(newPath) + loadFiles(newPath) + } else { + const newPath = currentPath ? `${currentPath}\\${file.name}` : file.name + setPathHistory([...pathHistory, newPath]) + setCurrentPath(newPath) + loadFiles(newPath) + } onSelect(null) } const handleRefresh = () => { - loadFiles(currentPath) + if (showDrives) { + loadDrives() + } else { + loadFiles(currentPath) + } + } + + const handleGoToRoot = () => { + setPathHistory(['']) + setCurrentPath('') + loadDrives() + onSelect(null) } const getDisplayPath = () => { + if (showDrives) return '选择驱动器' if (!currentPath) return '远程文件' - const parts = currentPath.split('/') - return parts[parts.length - 1] || currentPath + return currentPath } return ( @@ -78,13 +118,20 @@ export const RemoteFilePanel: React.FC = ({
- + + {getDisplayPath()}
@@ -113,28 +160,33 @@ export const RemoteFilePanel: React.FC = ({ ) : (
- {files.map((file) => ( -
onSelect(selectedFile?.path === file.path ? null : file)} - onDoubleClick={() => file.type === 'dir' && handleGoInto(file)} - className={clsx( - 'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer transition-colors text-sm', - selectedFile?.path === file.path - ? 'bg-gray-100 text-gray-800 dark:bg-gray-700/60 dark:text-gray-200' - : 'hover:bg-gray-100 dark:hover:bg-gray-700/50 text-gray-700 dark:text-gray-200' - )} - > - {file.type === 'dir' ? ( - - ) : ( - - )} - - {file.name} - -
- ))} + {files.map((file) => { + const isDrive = showDrives && file.name.length === 2 && file.name.endsWith(':') + return ( +
onSelect(selectedFile?.path === file.path ? null : file)} + onDoubleClick={() => (file.type === 'dir' || isDrive) && handleGoInto(file)} + className={clsx( + 'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer transition-colors text-sm', + selectedFile?.path === file.path + ? 'bg-gray-100 text-gray-800 dark:bg-gray-700/60 dark:text-gray-200' + : 'hover:bg-gray-100 dark:hover:bg-gray-700/50 text-gray-700 dark:text-gray-200' + )} + > + {isDrive ? ( + + ) : file.type === 'dir' ? ( + + ) : ( + + )} + + {file.name} + +
+ ) + })}
)}