Files
Mymusic3/sync-server/server.js
史悦 ca1026d166 feat(sync): add cloud synchronization for favorites
Add a new sync service and frontend integration to allow syncing
favorites across devices using a token.

- Configure `sync-service` in docker-compose.yml on port 7482
- Add sync token input and manual sync button to SideDrawer
- Implement auto-sync logic to persist favorites to the KV store
- Add logic to merge cloud favorites with local data on initialization
2026-01-06 11:20:06 +08:00

85 lines
2.8 KiB
JavaScript

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const DATA_DIR = process.env.DATA_DIR || './data';
const PORT = process.env.PORT || 3001;
// Ensure data directory exists
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
const server = http.createServer((req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
// Path format: /kv/:key?token=...
const match = pathname.match(/^\/kv\/([a-zA-Z0-9_-]+)$/);
if (!match) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid path' }));
return;
}
const key = match[1];
const token = parsedUrl.query.token;
if (!token) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Token required' }));
return;
}
// Simple file-based storage: data/{token}_{key}.json
// Using token in filename ensures isolation between users
const safeToken = token.replace(/[^a-zA-Z0-9]/g, '');
const filePath = path.join(DATA_DIR, `${safeToken}_${key}.json`);
if (req.method === 'GET') {
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(data);
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(null)); // Key not found returns null
}
} else if (req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
// Validate JSON
JSON.parse(body);
fs.writeFileSync(filePath, body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
} catch (e) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
} else {
res.writeHead(405);
res.end();
}
});
server.listen(PORT, () => {
console.log(`Sync Server running on port ${PORT}`);
console.log(`Data directory: ${DATA_DIR}`);
});