fix(file-manager): raise the upload route body limit, add a client-side size guard, and write uploaded content to containers in chunks to avoid oversized SSH commands.
This commit is contained in:
1 parent
ad60be0094
commit
4c957c50b4
3 files changed
+92
-5
No files matched your search
@@ -41,6 +41,8 @@ interface ContainerFileManagerModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const FILE_MANAGER_UPLOAD_MAX_BYTES = 6 * 1024 * 1024;
|
||||
|
||||
type PendingConfirmAction = {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -288,6 +290,16 @@ export default function ContainerFileManagerModal({
|
||||
|
||||
const uploadFileObject = useCallback(
|
||||
async (file: File) => {
|
||||
if (file.size > FILE_MANAGER_UPLOAD_MAX_BYTES) {
|
||||
setNotice({
|
||||
tone: "error",
|
||||
message: `File is too large. Maximum upload size is ${formatBytes(
|
||||
FILE_MANAGER_UPLOAD_MAX_BYTES,
|
||||
)}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
|
||||
@@ -122,6 +122,9 @@ const ContainerRenamePathSchema = z.object({
|
||||
newPath: z.string().min(1).max(2048),
|
||||
});
|
||||
|
||||
const CONTAINER_UPLOAD_CONTENT_BASE64_MAX_LENGTH = 8_000_000;
|
||||
const CONTAINER_UPLOAD_BODY_LIMIT_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
const ContainerUploadFileSchema = z.object({
|
||||
directoryPath: z.string().min(1).max(2048),
|
||||
fileName: z
|
||||
@@ -129,7 +132,10 @@ const ContainerUploadFileSchema = z.object({
|
||||
.min(1)
|
||||
.max(255)
|
||||
.refine((value) => !value.includes("/")),
|
||||
contentBase64: z.string().min(1).max(8_000_000),
|
||||
contentBase64: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(CONTAINER_UPLOAD_CONTENT_BASE64_MAX_LENGTH),
|
||||
});
|
||||
|
||||
function mapDockerStatus(status: string): ContainerStatus {
|
||||
@@ -2006,7 +2012,10 @@ export async function containerRoutes(app: FastifyInstance) {
|
||||
|
||||
app.post(
|
||||
"/:id/file/upload",
|
||||
{ preHandler: containerWriteAccess },
|
||||
{
|
||||
preHandler: containerWriteAccess,
|
||||
bodyLimit: CONTAINER_UPLOAD_BODY_LIMIT_BYTES,
|
||||
},
|
||||
async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const body = ContainerUploadFileSchema.safeParse(req.body);
|
||||
|
||||
@@ -22,6 +22,7 @@ const DOCKER_FILE_LIST_TIMEOUT_MS = 10_000;
|
||||
const DOCKER_FILE_READ_TIMEOUT_MS = 15_000;
|
||||
const DOCKER_FILE_WRITE_TIMEOUT_MS = 20_000;
|
||||
const DOCKER_FILE_DOWNLOAD_TIMEOUT_MS = 30_000;
|
||||
const CONTAINER_FILE_UPLOAD_CHUNK_SIZE = 48_000;
|
||||
const DEPLOY_GIT_CLONE_TIMEOUT_MS = 10 * 60_000;
|
||||
const DEPLOY_BUILD_TIMEOUT_MS = 45 * 60_000;
|
||||
const DEPLOY_COMPOSE_TIMEOUT_MS = 45 * 60_000;
|
||||
@@ -1220,12 +1221,77 @@ export async function writeContainerFileBase64(
|
||||
filePath: string,
|
||||
contentBase64: string,
|
||||
): Promise<{ path: string; size: number }> {
|
||||
const script = [
|
||||
const uploadId = randomBytes(8).toString("hex");
|
||||
const tempBase64Path = `${filePath}.doktainer-upload-${uploadId}.b64`;
|
||||
const tempFilePath = `${filePath}.doktainer-upload-${uploadId}.tmp`;
|
||||
const prepareScript = [
|
||||
`TARGET=${escapeShellArg(filePath)}`,
|
||||
`TEMP_B64=${escapeShellArg(tempBase64Path)}`,
|
||||
`TEMP_FILE=${escapeShellArg(tempFilePath)}`,
|
||||
'PARENT=$(dirname "$TARGET")',
|
||||
'if [ ! -d "$PARENT" ]; then echo "__ERROR__\tParent directory not found"; exit 22; fi',
|
||||
'if [ -e "$TARGET" ] && [ ! -f "$TARGET" ]; then echo "__ERROR__\tOnly regular files can be overwritten"; exit 31; fi',
|
||||
`printf %s ${escapeShellArg(contentBase64)} | base64 -d > "$TARGET"`,
|
||||
'rm -f "$TEMP_B64" "$TEMP_FILE"',
|
||||
': > "$TEMP_B64"',
|
||||
'printf "__OK__\tprepared\n"',
|
||||
].join("\n");
|
||||
|
||||
await execContainerShell(
|
||||
server,
|
||||
containerId,
|
||||
prepareScript,
|
||||
DOCKER_FILE_WRITE_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
try {
|
||||
for (
|
||||
let offset = 0;
|
||||
offset < contentBase64.length;
|
||||
offset += CONTAINER_FILE_UPLOAD_CHUNK_SIZE
|
||||
) {
|
||||
const chunk = contentBase64.slice(
|
||||
offset,
|
||||
offset + CONTAINER_FILE_UPLOAD_CHUNK_SIZE,
|
||||
);
|
||||
const appendScript = [
|
||||
`TEMP_B64=${escapeShellArg(tempBase64Path)}`,
|
||||
`printf %s ${escapeShellArg(chunk)} >> "$TEMP_B64"`,
|
||||
'printf "__OK__\tappended\n"',
|
||||
].join("\n");
|
||||
|
||||
await execContainerShell(
|
||||
server,
|
||||
containerId,
|
||||
appendScript,
|
||||
DOCKER_FILE_WRITE_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const cleanupScript = [
|
||||
`TEMP_B64=${escapeShellArg(tempBase64Path)}`,
|
||||
`TEMP_FILE=${escapeShellArg(tempFilePath)}`,
|
||||
'rm -f "$TEMP_B64" "$TEMP_FILE"',
|
||||
].join("\n");
|
||||
try {
|
||||
await execContainerShell(
|
||||
server,
|
||||
containerId,
|
||||
cleanupScript,
|
||||
DOCKER_FILE_WRITE_TIMEOUT_MS,
|
||||
);
|
||||
} catch {
|
||||
// Best-effort cleanup; surface the original upload failure.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const finalizeScript = [
|
||||
`TARGET=${escapeShellArg(filePath)}`,
|
||||
`TEMP_B64=${escapeShellArg(tempBase64Path)}`,
|
||||
`TEMP_FILE=${escapeShellArg(tempFilePath)}`,
|
||||
'trap \'rm -f "$TEMP_B64" "$TEMP_FILE"\' EXIT',
|
||||
'base64 -d "$TEMP_B64" > "$TEMP_FILE"',
|
||||
'mv "$TEMP_FILE" "$TARGET"',
|
||||
'SIZE=$(stat -c "%s" "$TARGET" 2>/dev/null || stat -f "%z" "$TARGET" 2>/dev/null || echo "0")',
|
||||
'printf "__OK__\t%s\n" "$SIZE"',
|
||||
].join("\n");
|
||||
@@ -1233,7 +1299,7 @@ export async function writeContainerFileBase64(
|
||||
const stdout = await execContainerShell(
|
||||
server,
|
||||
containerId,
|
||||
script,
|
||||
finalizeScript,
|
||||
DOCKER_FILE_WRITE_TIMEOUT_MS,
|
||||
);
|
||||
const okLine = stdout
|
||||
|
||||
Reference in new issue
Block a user