Files
dockhand-upstream/src/routes/api/system/files/content/+server.ts
T

70 lines
2.3 KiB
TypeScript

import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { readFileSync, existsSync, statSync } from 'node:fs';
import { authorize } from '$lib/server/authorize';
import { isProtectedPath } from '$lib/server/fs-guard';
/**
* GET /api/system/files/content
* Read file content from Dockhand's local filesystem
*
* @openapi
* summary: Read a text file from Dockhand's local filesystem (max 10MB, protected paths denied)
* query: path:string! Absolute path of the file to read
* resp-200: {path:string!, content:string!, size:integer!, mtime:string!}
* resp-200-example: {"path":"/docker/stacks/myapp/compose.yaml","content":"services:\n app:\n image: nginx","size":42,"mtime":"2026-07-01T10:00:00.000Z"}
* resp-400: Path is missing, points to a directory, or the file exceeds 10MB
* resp-403: Permission denied, or the path is protected
* resp-404: File not found
* resp-500: Failed to read file
*/
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
// Loading a stack's own compose/.env for editing needs stacks:edit; host and
// Dockhand secrets are kept unreadable regardless via isProtectedPath below.
if (auth.authEnabled && !await auth.can('stacks', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const path = url.searchParams.get('path');
if (!path) {
return json({ error: 'Path is required' }, { status: 400 });
}
if (isProtectedPath(path)) {
return json({ error: 'Access denied' }, { status: 403 });
}
try {
if (!existsSync(path)) {
return json({ error: `File not found: ${path}` }, { status: 404 });
}
const stat = statSync(path);
if (stat.isDirectory()) {
return json({ error: `Cannot read directory as file: ${path}` }, { status: 400 });
}
// Limit file size to 10MB
const maxSize = 10 * 1024 * 1024;
if (stat.size > maxSize) {
return json({ error: `File too large (max ${maxSize / 1024 / 1024}MB)` }, { status: 400 });
}
const content = readFileSync(path, 'utf-8');
return json({
path,
content,
size: stat.size,
mtime: stat.mtime.toISOString()
});
} catch (error) {
console.error('Error reading file:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
return json({ error: `Failed to read file: ${message}` }, { status: 500 });
}
};