nas-samba v1.0.2: add tmpfile ephemeral upload/download endpoints
This commit is contained in:
@@ -29,6 +29,8 @@ const JWT_ALGORITHM = 'HS256';
|
||||
const JWT_EXPIRATION_HOURS = 24;
|
||||
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'alexz';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || process.env.SMB_PASSWORD;
|
||||
const TMP_DIR = '/tmp/tmpfiles';
|
||||
const TMP_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SQLite setup
|
||||
@@ -74,6 +76,16 @@ function initDatabase() {
|
||||
details TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tmp_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -402,6 +414,22 @@ function getTypeBreakdown(dirPath) {
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tmp file cleanup
|
||||
// ---------------------------------------------------------------------------
|
||||
function cleanupTmpFiles() {
|
||||
const now = new Date().toISOString();
|
||||
const expired = sqlDb.prepare('SELECT * FROM tmp_files WHERE expires_at <= ?').all(now);
|
||||
for (const file of expired) {
|
||||
const filePath = path.join(TMP_DIR, file.id);
|
||||
try { fs.unlinkSync(filePath); } catch (_e) { /* already gone */ }
|
||||
}
|
||||
sqlDb.prepare('DELETE FROM tmp_files WHERE expires_at <= ?').run(now);
|
||||
if (expired.length > 0) {
|
||||
console.log(`Cleaned up ${expired.length} expired tmp files`);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// API ROUTES
|
||||
// =========================================================================
|
||||
@@ -1445,6 +1473,99 @@ app.post('/api/download-zip', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tmp File Routes (no auth, ephemeral storage)
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/api/tmp/upload', upload.single('file'), (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ detail: 'No file provided' });
|
||||
}
|
||||
|
||||
const fileId = crypto.randomUUID();
|
||||
const token = crypto.randomBytes(12).toString('base64url');
|
||||
const expiresAt = new Date(Date.now() + TMP_EXPIRY_MS);
|
||||
const mimeType = req.file.mimetype || 'application/octet-stream';
|
||||
|
||||
fs.mkdirSync(TMP_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(TMP_DIR, fileId), req.file.buffer);
|
||||
|
||||
sqlDb.prepare(
|
||||
'INSERT INTO tmp_files (id, token, original_name, mime_type, size, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(fileId, token, req.file.originalname, mimeType, req.file.size, new Date().toISOString(), expiresAt.toISOString());
|
||||
|
||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||
return res.json({
|
||||
token,
|
||||
url: `${baseUrl}/api/tmp/${token}`,
|
||||
filename: req.file.originalname,
|
||||
size: req.file.size,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tmp upload error:', err.message);
|
||||
return res.status(500).json({ detail: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/tmp/:token', (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
const file = sqlDb.prepare('SELECT * FROM tmp_files WHERE token = ?').get(token);
|
||||
if (!file) {
|
||||
return res.status(404).json({ detail: 'File not found or expired' });
|
||||
}
|
||||
|
||||
if (new Date() > new Date(file.expires_at)) {
|
||||
const filePath = path.join(TMP_DIR, file.id);
|
||||
try { fs.unlinkSync(filePath); } catch (_e) {}
|
||||
sqlDb.prepare('DELETE FROM tmp_files WHERE id = ?').run(file.id);
|
||||
return res.status(410).json({ detail: 'File has expired' });
|
||||
}
|
||||
|
||||
const filePath = path.join(TMP_DIR, file.id);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
sqlDb.prepare('DELETE FROM tmp_files WHERE id = ?').run(file.id);
|
||||
return res.status(404).json({ detail: 'File not found' });
|
||||
}
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
res.setHeader('Content-Type', file.mime_type);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.original_name)}"`);
|
||||
res.setHeader('Content-Length', stat.size);
|
||||
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
} catch (err) {
|
||||
console.error('Tmp download error:', err.message);
|
||||
return res.status(500).json({ detail: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/tmp/:token/info', (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
const file = sqlDb.prepare('SELECT * FROM tmp_files WHERE token = ?').get(token);
|
||||
if (!file) {
|
||||
return res.status(404).json({ detail: 'File not found or expired' });
|
||||
}
|
||||
|
||||
if (new Date() > new Date(file.expires_at)) {
|
||||
return res.status(410).json({ detail: 'File has expired' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
filename: file.original_name,
|
||||
size: file.size,
|
||||
mime_type: file.mime_type,
|
||||
created_at: file.created_at,
|
||||
expires_at: file.expires_at,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tmp info error:', err.message);
|
||||
return res.status(500).json({ detail: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serve React build (SPA fallback)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1490,6 +1611,13 @@ function main() {
|
||||
// Seed admin
|
||||
seedAdmin();
|
||||
|
||||
// Ensure tmp directory exists
|
||||
fs.mkdirSync(TMP_DIR, { recursive: true });
|
||||
|
||||
// Cleanup expired tmp files on startup and every 5 minutes
|
||||
cleanupTmpFiles();
|
||||
setInterval(cleanupTmpFiles, 5 * 60 * 1000);
|
||||
|
||||
// Start listening
|
||||
app.listen(PORT, () => {
|
||||
console.log(`CloudSync server running on port ${PORT}`);
|
||||
|
||||
Reference in New Issue
Block a user