mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-08 05:39:06 +00:00
Initial commit
This commit is contained in:
32
routes/api/containers/[id]/files/+server.ts
Normal file
32
routes/api/containers/[id]/files/+server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { listContainerDirectory } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path') || '/';
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
const simpleLs = url.searchParams.get('simpleLs') === 'true';
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'view', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listContainerDirectory(
|
||||
params.id,
|
||||
path,
|
||||
envIdNum,
|
||||
simpleLs
|
||||
);
|
||||
|
||||
return json(result);
|
||||
} catch (error: any) {
|
||||
console.error('Error listing container directory:', error);
|
||||
return json({ error: error.message || 'Failed to list directory' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
57
routes/api/containers/[id]/files/chmod/+server.ts
Normal file
57
routes/api/containers/[id]/files/chmod/+server.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { chmodContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { path, mode, recursive } = body;
|
||||
|
||||
if (!path || typeof path !== 'string') {
|
||||
return json({ error: 'Path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!mode || typeof mode !== 'string') {
|
||||
return json({ error: 'Mode is required (e.g., "755" or "u+x")' }, { status: 400 });
|
||||
}
|
||||
|
||||
await chmodContainerPath(params.id, path, mode, recursive === true, envIdNum);
|
||||
|
||||
return json({ success: true, path, mode, recursive: recursive === true });
|
||||
} catch (error: any) {
|
||||
console.error('Error changing permissions:', error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('No such file or directory')) {
|
||||
return json({ error: 'Path not found' }, { status: 404 });
|
||||
}
|
||||
if (msg.includes('Invalid chmod mode')) {
|
||||
return json({ error: msg }, { status: 400 });
|
||||
}
|
||||
if (msg.includes('Read-only file system')) {
|
||||
return json({ error: 'File system is read-only' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('Operation not permitted')) {
|
||||
return json({ error: 'Operation not permitted' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('container is not running')) {
|
||||
return json({ error: 'Container is not running' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: `Failed to change permissions: ${msg}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
116
routes/api/containers/[id]/files/content/+server.ts
Normal file
116
routes/api/containers/[id]/files/content/+server.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { readContainerFile, writeContainerFile } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
// Max file size for reading (1MB)
|
||||
const MAX_FILE_SIZE = 1024 * 1024;
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'view', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!path) {
|
||||
return json({ error: 'Path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const content = await readContainerFile(
|
||||
params.id,
|
||||
path,
|
||||
envIdNum
|
||||
);
|
||||
|
||||
// Check if content is too large
|
||||
if (content.length > MAX_FILE_SIZE) {
|
||||
return json({ error: 'File is too large to edit (max 1MB)' }, { status: 413 });
|
||||
}
|
||||
|
||||
return json({ content, path });
|
||||
} catch (error: any) {
|
||||
console.error('Error reading container file:', error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('No such file or directory')) {
|
||||
return json({ error: 'File not found' }, { status: 404 });
|
||||
}
|
||||
if (msg.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied to read this file' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('Is a directory')) {
|
||||
return json({ error: 'Cannot read a directory' }, { status: 400 });
|
||||
}
|
||||
if (msg.includes('container is not running')) {
|
||||
return json({ error: 'Container is not running' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: `Failed to read file: ${msg}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!path) {
|
||||
return json({ error: 'Path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
if (typeof body.content !== 'string') {
|
||||
return json({ error: 'Content is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check content size
|
||||
if (body.content.length > MAX_FILE_SIZE) {
|
||||
return json({ error: 'Content is too large (max 1MB)' }, { status: 413 });
|
||||
}
|
||||
|
||||
await writeContainerFile(
|
||||
params.id,
|
||||
path,
|
||||
body.content,
|
||||
envIdNum
|
||||
);
|
||||
|
||||
return json({ success: true, path });
|
||||
} catch (error: any) {
|
||||
console.error('Error writing container file:', error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied to write this file' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('No such file or directory')) {
|
||||
return json({ error: 'Directory not found' }, { status: 404 });
|
||||
}
|
||||
if (msg.includes('Read-only file system')) {
|
||||
return json({ error: 'File system is read-only' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('No space left on device')) {
|
||||
return json({ error: 'No space left on device' }, { status: 507 });
|
||||
}
|
||||
if (msg.includes('container is not running')) {
|
||||
return json({ error: 'Container is not running' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: `Failed to write file: ${msg}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
55
routes/api/containers/[id]/files/create/+server.ts
Normal file
55
routes/api/containers/[id]/files/create/+server.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { createContainerFile, createContainerDirectory } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { path, type } = body;
|
||||
|
||||
if (!path || typeof path !== 'string') {
|
||||
return json({ error: 'Path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (type !== 'file' && type !== 'directory') {
|
||||
return json({ error: 'Type must be "file" or "directory"' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (type === 'file') {
|
||||
await createContainerFile(params.id, path, envIdNum);
|
||||
} else {
|
||||
await createContainerDirectory(params.id, path, envIdNum);
|
||||
}
|
||||
|
||||
return json({ success: true, path, type });
|
||||
} catch (error: any) {
|
||||
console.error('Error creating path:', error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('File exists')) {
|
||||
return json({ error: 'Path already exists' }, { status: 409 });
|
||||
}
|
||||
if (msg.includes('No such file or directory')) {
|
||||
return json({ error: 'Parent directory not found' }, { status: 404 });
|
||||
}
|
||||
if (msg.includes('container is not running')) {
|
||||
return json({ error: 'Container is not running' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: `Failed to create: ${msg}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
51
routes/api/containers/[id]/files/delete/+server.ts
Normal file
51
routes/api/containers/[id]/files/delete/+server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { deleteContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!path) {
|
||||
return json({ error: 'Path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await deleteContainerPath(params.id, path, envIdNum);
|
||||
|
||||
return json({ success: true, path });
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting path:', error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('No such file or directory')) {
|
||||
return json({ error: 'Path not found' }, { status: 404 });
|
||||
}
|
||||
if (msg.includes('Cannot delete critical')) {
|
||||
return json({ error: msg }, { status: 400 });
|
||||
}
|
||||
if (msg.includes('Read-only file system')) {
|
||||
return json({ error: 'File system is read-only' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('Directory not empty')) {
|
||||
return json({ error: 'Directory is not empty' }, { status: 400 });
|
||||
}
|
||||
if (msg.includes('container is not running')) {
|
||||
return json({ error: 'Container is not running' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: `Failed to delete: ${msg}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
98
routes/api/containers/[id]/files/download/+server.ts
Normal file
98
routes/api/containers/[id]/files/download/+server.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { getContainerArchive, statContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'view', envIdNum)) {
|
||||
return new Response(JSON.stringify({ error: 'Permission denied' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return new Response(JSON.stringify({ error: 'Path is required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Get format from query parameter (defaults to tar)
|
||||
const format = url.searchParams.get('format') || 'tar';
|
||||
|
||||
// Get stat info to determine filename
|
||||
let filename: string;
|
||||
try {
|
||||
const stat = await statContainerPath(params.id, path, envIdNum);
|
||||
filename = stat.name || path.split('/').pop() || 'download';
|
||||
} catch {
|
||||
filename = path.split('/').pop() || 'download';
|
||||
}
|
||||
|
||||
// Get the archive from Docker
|
||||
const response = await getContainerArchive(
|
||||
params.id,
|
||||
path,
|
||||
envIdNum
|
||||
);
|
||||
|
||||
// Prepare response based on format
|
||||
let body: ReadableStream<Uint8Array> | Uint8Array = response.body!;
|
||||
let contentType = 'application/x-tar';
|
||||
let extension = '.tar';
|
||||
|
||||
if (format === 'tar.gz') {
|
||||
// Compress with gzip using Bun's native implementation
|
||||
const tarData = new Uint8Array(await response.arrayBuffer());
|
||||
body = Bun.gzipSync(tarData);
|
||||
contentType = 'application/gzip';
|
||||
extension = '.tar.gz';
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': contentType,
|
||||
'Content-Disposition': `attachment; filename="${filename}${extension}"`
|
||||
};
|
||||
|
||||
// Set content length for compressed data
|
||||
if (body instanceof Uint8Array) {
|
||||
headers['Content-Length'] = body.length.toString();
|
||||
} else {
|
||||
// Pass through content length for streaming tar
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
if (contentLength) {
|
||||
headers['Content-Length'] = contentLength;
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(body, { headers });
|
||||
} catch (error: any) {
|
||||
console.error('Error downloading container file:', error);
|
||||
|
||||
if (error.message?.includes('No such file or directory')) {
|
||||
return new Response(JSON.stringify({ error: 'File not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
if (error.message?.includes('Permission denied')) {
|
||||
return new Response(JSON.stringify({ error: 'Permission denied to access this path' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: 'Failed to download file' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
54
routes/api/containers/[id]/files/rename/+server.ts
Normal file
54
routes/api/containers/[id]/files/rename/+server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { renameContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { oldPath, newPath } = body;
|
||||
|
||||
if (!oldPath || typeof oldPath !== 'string') {
|
||||
return json({ error: 'Old path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!newPath || typeof newPath !== 'string') {
|
||||
return json({ error: 'New path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await renameContainerPath(params.id, oldPath, newPath, envIdNum);
|
||||
|
||||
return json({ success: true, oldPath, newPath });
|
||||
} catch (error: any) {
|
||||
console.error('Error renaming path:', error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('No such file or directory')) {
|
||||
return json({ error: 'Source path not found' }, { status: 404 });
|
||||
}
|
||||
if (msg.includes('File exists') || msg.includes('Directory not empty')) {
|
||||
return json({ error: 'Destination already exists' }, { status: 409 });
|
||||
}
|
||||
if (msg.includes('Read-only file system')) {
|
||||
return json({ error: 'File system is read-only' }, { status: 403 });
|
||||
}
|
||||
if (msg.includes('container is not running')) {
|
||||
return json({ error: 'Container is not running' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: `Failed to rename: ${msg}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
154
routes/api/containers/[id]/files/upload/+server.ts
Normal file
154
routes/api/containers/[id]/files/upload/+server.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { putContainerArchive } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
/**
|
||||
* Create a simple tar archive from a single file
|
||||
* TAR format: 512-byte header followed by file content padded to 512 bytes
|
||||
*/
|
||||
function createTarArchive(filename: string, content: Uint8Array): Uint8Array {
|
||||
// TAR header is 512 bytes
|
||||
const header = new Uint8Array(512);
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// File name (100 bytes)
|
||||
const nameBytes = encoder.encode(filename.slice(0, 99));
|
||||
header.set(nameBytes, 0);
|
||||
|
||||
// File mode (8 bytes) - 0644
|
||||
header.set(encoder.encode('0000644\0'), 100);
|
||||
|
||||
// Owner UID (8 bytes)
|
||||
header.set(encoder.encode('0000000\0'), 108);
|
||||
|
||||
// Owner GID (8 bytes)
|
||||
header.set(encoder.encode('0000000\0'), 116);
|
||||
|
||||
// File size in octal (12 bytes)
|
||||
const sizeOctal = content.length.toString(8).padStart(11, '0');
|
||||
header.set(encoder.encode(sizeOctal + '\0'), 124);
|
||||
|
||||
// Modification time (12 bytes) - current time in octal
|
||||
const mtime = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0');
|
||||
header.set(encoder.encode(mtime + '\0'), 136);
|
||||
|
||||
// Checksum placeholder (8 spaces initially)
|
||||
header.set(encoder.encode(' '), 148);
|
||||
|
||||
// Type flag - '0' for regular file
|
||||
header[156] = 48; // '0'
|
||||
|
||||
// Link name (100 bytes) - empty
|
||||
// Magic (6 bytes) - 'ustar\0'
|
||||
header.set(encoder.encode('ustar\0'), 257);
|
||||
|
||||
// Version (2 bytes) - '00'
|
||||
header.set(encoder.encode('00'), 263);
|
||||
|
||||
// Owner name (32 bytes)
|
||||
header.set(encoder.encode('root'), 265);
|
||||
|
||||
// Group name (32 bytes)
|
||||
header.set(encoder.encode('root'), 297);
|
||||
|
||||
// Calculate checksum
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < 512; i++) {
|
||||
checksum += header[i];
|
||||
}
|
||||
const checksumOctal = checksum.toString(8).padStart(6, '0') + '\0 ';
|
||||
header.set(encoder.encode(checksumOctal), 148);
|
||||
|
||||
// Calculate padding to 512-byte boundary
|
||||
const paddingSize = (512 - (content.length % 512)) % 512;
|
||||
const padding = new Uint8Array(paddingSize);
|
||||
|
||||
// End of archive marker (two 512-byte zero blocks)
|
||||
const endMarker = new Uint8Array(1024);
|
||||
|
||||
// Combine all parts
|
||||
const totalSize = header.length + content.length + paddingSize + endMarker.length;
|
||||
const tar = new Uint8Array(totalSize);
|
||||
|
||||
let offset = 0;
|
||||
tar.set(header, offset);
|
||||
offset += header.length;
|
||||
tar.set(content, offset);
|
||||
offset += content.length;
|
||||
tar.set(padding, offset);
|
||||
offset += paddingSize;
|
||||
tar.set(endMarker, offset);
|
||||
|
||||
return tar;
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return json({ error: 'Target path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const files = formData.getAll('files') as File[];
|
||||
|
||||
if (files.length === 0) {
|
||||
return json({ error: 'No files provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
// For simplicity, we'll upload files one at a time
|
||||
// A more sophisticated implementation could pack multiple files into one tar
|
||||
const uploaded: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = new Uint8Array(await file.arrayBuffer());
|
||||
const tar = createTarArchive(file.name, content);
|
||||
|
||||
await putContainerArchive(
|
||||
params.id,
|
||||
path,
|
||||
tar,
|
||||
envId ? parseInt(envId) : undefined
|
||||
);
|
||||
|
||||
uploaded.push(file.name);
|
||||
} catch (err: any) {
|
||||
errors.push(`${file.name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0 && uploaded.length === 0) {
|
||||
return json({ error: 'Failed to upload files', details: errors }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
uploaded,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error uploading to container:', error);
|
||||
|
||||
if (error.message?.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied to write to this path' }, { status: 403 });
|
||||
}
|
||||
if (error.message?.includes('No such file or directory')) {
|
||||
return json({ error: 'Target directory not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ error: 'Failed to upload files' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user