diff --git a/backend/src/routes/files.js b/backend/src/routes/files.js
new file mode 100644
index 0000000..0866e9b
--- /dev/null
+++ b/backend/src/routes/files.js
@@ -0,0 +1,169 @@
+const express = require('express');
+const multer = require('multer');
+const path = require('path');
+const fs = require('fs');
+const crypto = require('crypto');
+const db = require('../db');
+
+const router = express.Router();
+
+// Configure multer for file upload
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ const uploadDir = path.join(__dirname, '../../uploads');
+ if (!fs.existsSync(uploadDir)) {
+ fs.mkdirSync(uploadDir, { recursive: true });
+ }
+ cb(null, uploadDir);
+ },
+ filename: (req, file, cb) => {
+ const uniqueSuffix = Date.now() + '-' + crypto.randomBytes(6).toString('hex');
+ cb(null, uniqueSuffix + path.extname(file.originalname));
+ }
+});
+
+const upload = multer({
+ storage,
+ limits: { fileSize: 100 * 1024 * 1024 } // 100MB limit
+});
+
+// Get file list
+router.get('/', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+ const parentId = req.query.parentId || null;
+
+ const files = db.query(
+ 'SELECT * FROM files WHERE user_id = ? AND parent_id IS ? AND deleted_at IS NULL ORDER BY is_folder DESC, name ASC',
+ [decoded.userId, parentId]
+ );
+
+ res.json({ files });
+ } catch (error) {
+ res.status(401).json({ error: 'Invalid token' });
+ }
+});
+
+// Upload file
+router.post('/upload', upload.single('file'), (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ if (!req.file) {
+ return res.status(400).json({ error: 'No file uploaded' });
+ }
+
+ // Calculate file hash (simplified)
+ const hash = crypto.createHash('md5').update(req.file.filename).digest('hex');
+
+ // Check if file already exists (for deduplication)
+ const existing = db.query(
+ 'SELECT id FROM files WHERE user_id = ? AND hash = ? AND name = ?',
+ [decoded.userId, hash, req.file.originalname]
+ );
+
+ let fileId;
+ if (existing.length > 0) {
+ // File exists, update
+ fileId = existing[0].id;
+ db.run(
+ 'UPDATE files SET size = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
+ [req.file.size, fileId]
+ );
+ } else {
+ // Insert new file
+ const result = db.run(
+ 'INSERT INTO files (user_id, name, type, size, path, hash, is_folder) VALUES (?, ?, ?, ?, ?, ?, ?)',
+ [decoded.userId, req.file.originalname, path.extname(req.file.originalname).slice(1), req.file.size, req.file.path, hash, 0]
+ );
+ fileId = result.lastInsertRowid;
+ }
+
+ // Update user storage
+ db.run(
+ 'UPDATE users SET storage_used = storage_used + ? WHERE id =',
+ [req.file.size, decoded.userId]
+ );
+
+ res.json({ success: true, fileId, filename: req.file.originalname });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// Download file
+router.get('/:id/download', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ const file = db.query(
+ 'SELECT * FROM files WHERE id = ? AND user_id = ? AND is_folder = 0',
+ [req.params.id, decoded.userId]
+ );
+
+ if (file.length === 0) {
+ return res.status(404).json({ error: 'File not found' });
+ }
+
+ res.download(file[0].path, file[0].name);
+ } catch (error) {
+ res.status(401).json({ error: 'Invalid token' });
+ }
+});
+
+// Delete file
+router.delete('/:id', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ // Soft delete
+ db.run(
+ 'UPDATE files SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
+ [req.params.id, decoded.userId]
+ );
+
+ res.json({ success: true });
+ } catch (error) {
+ res.status(401).json({ error: 'Invalid token' });
+ }
+});
+
+// Create folder
+router.post('/folder', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ const { name, parentId } = req.body;
+
+ const result = db.run(
+ 'INSERT INTO files (user_id, name, parent_id, is_folder) VALUES (?, ?, ?, ?)',
+ [decoded.userId, name, parentId || null, 1]
+ );
+
+ res.json({ success: true, folderId: result.lastInsertRowid });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+module.exports = router;
diff --git a/frontend/src/main/index.js b/frontend/src/main/index.js
new file mode 100644
index 0000000..60f3af7
--- /dev/null
+++ b/frontend/src/main/index.js
@@ -0,0 +1,66 @@
+const { app, BrowserWindow, ipcMain, dialog } = require('electron');
+const path = require('path');
+
+let mainWindow;
+
+function createWindow() {
+ mainWindow = new BrowserWindow({
+ width: 1200,
+ height: 800,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, 'preload.js')
+ }
+ });
+
+ // Load the app
+ if (process.env.NODE_ENV === 'development') {
+ mainWindow.loadURL('http://localhost:3000');
+ mainWindow.webContents.openDevTools();
+ } else {
+ mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
+ }
+
+ mainWindow.on('closed', () => {
+ mainWindow = null;
+ });
+}
+
+app.whenReady().then(createWindow);
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
+});
+
+// IPC handlers
+ipcMain.handle('select-file', async () => {
+ const result = await dialog.showOpenDialog(mainWindow, {
+ properties: ['openFile', 'multiSelections']
+ });
+ return result.filePaths;
+});
+
+ipcMain.handle('select-folder', async () => {
+ const result = await dialog.showOpenDialog(mainWindow, {
+ properties: ['openDirectory']
+ });
+ return result.filePaths[0];
+});
+
+ipcMain.handle('show-message', async (event, { title, message, type }) => {
+ const { dialog } = require('electron');
+ return dialog.showMessageBox(mainWindow, {
+ type: type || 'info',
+ title: title || 'CloudDisk',
+ message: message
+ });
+});
diff --git a/frontend/src/main/preload.js b/frontend/src/main/preload.js
new file mode 100644
index 0000000..9be8f53
--- /dev/null
+++ b/frontend/src/main/preload.js
@@ -0,0 +1,18 @@
+const { contextBridge, ipcRenderer } = require('electron');
+
+contextBridge.exposeInMainWorld('electron', {
+ selectFile: () => ipcRenderer.invoke('select-file'),
+ selectFolder: () => ipcRenderer.invoke('select-folder'),
+ showMessage: (options) => ipcRenderer.invoke('show-message', options),
+
+ // File operations
+ uploadFile: (filePath) => {
+ const formData = new FormData();
+ formData.append('file', require('fs').createReadStream(filePath));
+ return fetch('/api/files/upload', {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` },
+ body: formData
+ });
+ }
+});
diff --git a/frontend/src/renderer/App.jsx b/frontend/src/renderer/App.jsx
new file mode 100644
index 0000000..0c08ca2
--- /dev/null
+++ b/frontend/src/renderer/App.jsx
@@ -0,0 +1,178 @@
+import React, { useState, useEffect } from 'react';
+import { ConfigProvider, Layout, Menu, Button, Upload, Table, Breadcrumb, Input } from 'antd';
+import {
+ UploadOutlined,
+ FolderOutlined,
+ FileOutlined,
+ HomeOutlined,
+ DeleteOutlined,
+ DownloadOutlined,
+ ShareAltOutlined
+} from '@ant-design/icons';
+
+const { Header, Sider, Content } = Layout;
+
+function App() {
+ const [user, setUser] = useState(null);
+ const [files, setFiles] = useState([]);
+ const [currentPath, setCurrentPath] = useState([]);
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+
+ const login = async (username, password) => {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password })
+ });
+ const data = await response.json();
+ if (data.token) {
+ localStorage.setItem('token', data.token);
+ setUser(data.user);
+ setIsLoggedIn(true);
+ loadFiles();
+ }
+ return data;
+ };
+
+ const loadFiles = async (parentId = null) => {
+ const token = localStorage.getItem('token');
+ const url = parentId ? `/api/files?parentId=${parentId}` : '/api/files';
+ const response = await fetch(url, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ const data = await response.json();
+ setFiles(data.files || []);
+ };
+
+ const handleUpload = async (file) => {
+ const token = localStorage.getItem('token');
+ const formData = new FormData();
+ formData.append('file', file);
+
+ await fetch('/api/files/upload', {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${token}` },
+ body: formData
+ });
+
+ loadFiles();
+ };
+
+ const handleCreateFolder = async () => {
+ const name = prompt('请输入文件夹名称:');
+ if (!name) return;
+
+ const token = localStorage.getItem('token');
+ await fetch('/api/files/folder', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify({ name })
+ });
+
+ loadFiles();
+ };
+
+ const handleDelete = async (fileId) => {
+ const token = localStorage.getItem('token');
+ await fetch(`/api/files/${fileId}`, {
+ method: 'DELETE',
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ loadFiles();
+ };
+
+ const columns = [
+ {
+ title: '名称',
+ dataIndex: 'name',
+ key: 'name',
+ render: (text, record) => (
+
+ {record.is_folder ?
+