团队工作进展:
- 后端: 同步功能 API (状态/开始同步/文件同步) - 前端: 同步状态组件 - 更新: README 文档
This commit is contained in:
parent
fb16b8544f
commit
2501893ab2
53
README.md
53
README.md
@ -4,6 +4,53 @@
|
||||
|
||||
Electron 跨平台桌面网盘应用
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 前端
|
||||
- Electron 28+ (桌面框架)
|
||||
- React 18 (UI框架)
|
||||
- TypeScript (类型安全)
|
||||
- Zustand (状态管理)
|
||||
- Ant Design 5 (UI组件库)
|
||||
|
||||
### 后端
|
||||
- Node.js 20+ (运行环境)
|
||||
- Express 4 (Web框架)
|
||||
- SQLite3 (本地数据库)
|
||||
- JWT (用户认证)
|
||||
- Multer (文件上传)
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 用户注册/登录
|
||||
- ✅ 文件上传/下载/删除
|
||||
- ✅ 文件夹管理
|
||||
- ✅ 分享功能(密码保护/链接分享)
|
||||
- 🔄 云端同步
|
||||
- ⏳ 离线访问
|
||||
- ⏳ 主题切换
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 前端开发
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 后端开发
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 测试
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## 团队成员
|
||||
|
||||
| 角色 | 账号 |
|
||||
@ -21,7 +68,11 @@ Electron 跨平台桌面网盘应用
|
||||
├── prd/ # 产品需求文档
|
||||
├── ui-design/ # UI设计文件
|
||||
├── architecture/ # 架构设计文档
|
||||
├── frontend/ # 前端代码
|
||||
├── frontend/ # 前端代码
|
||||
├── backend/ # 后端代码
|
||||
└── tests/ # 测试代码
|
||||
```
|
||||
|
||||
## Git 仓库
|
||||
|
||||
https://git.liantu.tech/openclaw_product_manager/clouddisk-project
|
||||
|
||||
@ -3,6 +3,7 @@ const cors = require('cors');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const fileRoutes = require('./routes/files');
|
||||
const shareRoutes = require('./routes/share');
|
||||
const syncRoutes = require('./routes/sync');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@ -14,6 +15,7 @@ app.use(express.json());
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/files', fileRoutes);
|
||||
app.use('/api/share', shareRoutes);
|
||||
app.use('/api/sync', syncRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
|
||||
152
backend/src/routes/sync.js
Normal file
152
backend/src/routes/sync.js
Normal file
@ -0,0 +1,152 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('../db');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get sync status
|
||||
router.get('/status', (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 status = db.query(
|
||||
'SELECT * FROM sync_status WHERE user_id = ?',
|
||||
[decoded.userId]
|
||||
);
|
||||
|
||||
if (status.length === 0) {
|
||||
// Create initial sync status
|
||||
const result = db.run(
|
||||
'INSERT INTO sync_status (user_id) VALUES (?)',
|
||||
[decoded.userId]
|
||||
);
|
||||
return res.json({
|
||||
status: 'idle',
|
||||
totalFiles: 0,
|
||||
syncedFiles: 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: status[0].status,
|
||||
lastSyncTime: status[0].last_sync_time,
|
||||
totalFiles: status[0].total_files,
|
||||
syncedFiles: status[0].synced_files
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Start sync
|
||||
router.post('/start', (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');
|
||||
|
||||
// Update sync status
|
||||
db.run(
|
||||
'UPDATE sync_status SET status = ?, last_sync_time = CURRENT_TIMESTAMP WHERE user_id = ?',
|
||||
['syncing', decoded.userId]
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Sync started' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate file hash for deduplication
|
||||
function calculateFileHash(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('md5');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on('data', data => hash.update(data));
|
||||
stream.on('end', () => resolve(hash.digest('hex')));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Sync single file (for client to call)
|
||||
router.post('/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');
|
||||
|
||||
const { fileId, localHash, localModified } = req.body;
|
||||
|
||||
// Get server file info
|
||||
const serverFile = db.query(
|
||||
'SELECT * FROM files WHERE id = ? AND user_id = ?',
|
||||
[fileId, decoded.userId]
|
||||
);
|
||||
|
||||
if (serverFile.length === 0) {
|
||||
return res.status(404).json({ error: 'File not found' });
|
||||
}
|
||||
|
||||
// Determine sync action
|
||||
let action = 'up-to-date';
|
||||
|
||||
if (!serverFile[0].hash || serverFile[0].hash !== localHash) {
|
||||
// Need to upload
|
||||
action = 'upload';
|
||||
}
|
||||
|
||||
res.json({
|
||||
action,
|
||||
serverModified: serverFile[0].updated_at,
|
||||
serverHash: serverFile[0].hash
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get local files that need syncing
|
||||
router.get('/pending', (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');
|
||||
|
||||
// Get files modified since last sync
|
||||
const lastSync = db.query(
|
||||
'SELECT last_sync_time FROM sync_status WHERE user_id = ?',
|
||||
[decoded.userId]
|
||||
);
|
||||
|
||||
let query;
|
||||
let params;
|
||||
|
||||
if (lastSync.length > 0 && lastSync[0].last_sync_time) {
|
||||
query = 'SELECT * FROM files WHERE user_id = ? AND updated_at > ?';
|
||||
params = [decoded.userId, lastSync[0].last_sync_time];
|
||||
} else {
|
||||
query = 'SELECT * FROM files WHERE user_id = ?';
|
||||
params = [decoded.userId];
|
||||
}
|
||||
|
||||
const files = db.query(query, params);
|
||||
|
||||
res.json({ files });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
114
frontend/src/renderer/components/SyncStatus.jsx
Normal file
114
frontend/src/renderer/components/SyncStatus.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Badge, Button, Progress, Tooltip } from 'antd';
|
||||
import { SyncOutlined, CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
function SyncStatus() {
|
||||
const [status, setStatus] = useState('idle');
|
||||
const [lastSync, setLastSync] = useState(null);
|
||||
const [syncedFiles, setSyncedFiles] = useState(0);
|
||||
const [totalFiles, setTotalFiles] = useState(0);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSyncStatus();
|
||||
// Poll status every 30 seconds
|
||||
const interval = setInterval(fetchSyncStatus, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const fetchSyncStatus = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/sync/status', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const data = await response.json();
|
||||
setStatus(data.status);
|
||||
setLastSync(data.lastSyncTime);
|
||||
setSyncedFiles(data.syncedFiles);
|
||||
setTotalFiles(data.totalFiles);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sync status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await fetch('/api/sync/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
// Start polling for progress
|
||||
const interval = setInterval(async () => {
|
||||
await fetchSyncStatus();
|
||||
if (status === 'idle' || status === 'completed') {
|
||||
setSyncing(false);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to start sync:', error);
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = () => {
|
||||
switch (status) {
|
||||
case 'syncing':
|
||||
return <Badge status="processing" text="同步中" />;
|
||||
case 'completed':
|
||||
return <Badge status="success" text="已同步" />;
|
||||
case 'error':
|
||||
return <Badge status="error" text="同步失败" />;
|
||||
default:
|
||||
return <Badge status="default" text="未同步" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getProgress = () => {
|
||||
if (totalFiles === 0) return 0;
|
||||
return Math.round((syncedFiles / totalFiles) * 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
padding: '8px 16px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
<SyncOutlined spin={syncing} style={{ fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ marginBottom: '4px' }}>{getStatusBadge()}</div>
|
||||
{lastSync && (
|
||||
<div style={{ fontSize: '12px', color: '#888' }}>
|
||||
<ClockCircleOutlined /> 最后同步: {new Date(lastSync).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{totalFiles > 0 && (
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={getProgress()}
|
||||
width={40}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleSync}
|
||||
loading={syncing}
|
||||
>
|
||||
{syncing ? '同步中' : '立即同步'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SyncStatus;
|
||||
Loading…
Reference in New Issue
Block a user