70 lines
2.3 KiB
TypeScript
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 });
|
|
}
|
|
};
|