nas-samba v1.0.2: add tmpfile ephemeral upload/download endpoints

This commit is contained in:
2026-04-26 05:22:17 +00:00
parent 3c67eb75c3
commit 888acdbd7e
3 changed files with 130 additions and 2 deletions

View File

@@ -8,7 +8,7 @@
"dynamic_config": true,
"no_gui": false,
"tipi_version": 2,
"version": "1.0.1",
"version": "1.0.2",
"categories": ["network", "utilities"],
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
"short_desc": "NAS with SMB + web file manager",

View File

@@ -4,7 +4,7 @@
"services": [
{
"name": "nas-samba",
"image": "git.alexzaw.dev/alexz/nas-samba:1.0.1",
"image": "git.alexzaw.dev/alexz/nas-samba:1.0.2",
"isMain": true,
"internalPort": 8080,
"hostname": "nas.alexzaw.dev",

View File

@@ -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}`);