fix(plugin): guard kubernetes upload edge cases
This commit is contained in:
@@ -132,6 +132,11 @@ export function buildSandboxExecCommand(
|
|||||||
return ["/bin/sh", "-l"];
|
return ["/bin/sh", "-l"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deriveUploadTargetDir(targetPath: string): string {
|
||||||
|
const slashIndex = targetPath.lastIndexOf("/");
|
||||||
|
return slashIndex >= 0 ? targetPath.slice(0, slashIndex) || "/" : ".";
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSandboxExecShellCommand(
|
export function buildSandboxExecShellCommand(
|
||||||
params: Pick<PluginEnvironmentExecuteParams, "args" | "command">,
|
params: Pick<PluginEnvironmentExecuteParams, "args" | "command">,
|
||||||
): string {
|
): string {
|
||||||
@@ -552,8 +557,7 @@ const plugin = definePlugin({
|
|||||||
}
|
}
|
||||||
if (decision.action === "flush") {
|
if (decision.action === "flush") {
|
||||||
const base64Body = decision.flush.payload.toString("base64");
|
const base64Body = decision.flush.payload.toString("base64");
|
||||||
const slashIndex = decision.flush.targetPath.lastIndexOf("/");
|
const dir = deriveUploadTargetDir(decision.flush.targetPath);
|
||||||
const dir = slashIndex > 0 ? decision.flush.targetPath.slice(0, slashIndex) : ".";
|
|
||||||
const script =
|
const script =
|
||||||
`mkdir -p ${shellQuoteArg(dir)} && ` +
|
`mkdir -p ${shellQuoteArg(dir)} && ` +
|
||||||
`base64 -d > ${shellQuoteArg(decision.flush.targetPath)}`;
|
`base64 -d > ${shellQuoteArg(decision.flush.targetPath)}`;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface BufferedUpload {
|
|||||||
targetPath: string;
|
targetPath: string;
|
||||||
chunks: string[];
|
chunks: string[];
|
||||||
totalBase64Chars: number;
|
totalBase64Chars: number;
|
||||||
|
sawPaddedChunk: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FastUploadInterceptor {
|
export class FastUploadInterceptor {
|
||||||
@@ -67,6 +68,7 @@ export class FastUploadInterceptor {
|
|||||||
targetPath,
|
targetPath,
|
||||||
chunks: [],
|
chunks: [],
|
||||||
totalBase64Chars: 0,
|
totalBase64Chars: 0,
|
||||||
|
sawPaddedChunk: false,
|
||||||
});
|
});
|
||||||
return { action: "ack", reason: `init upload to ${targetPath}` };
|
return { action: "ack", reason: `init upload to ${targetPath}` };
|
||||||
}
|
}
|
||||||
@@ -80,6 +82,13 @@ export class FastUploadInterceptor {
|
|||||||
if (!upload) {
|
if (!upload) {
|
||||||
return { action: "passthrough", reason: "chunk without prior init" };
|
return { action: "passthrough", reason: "chunk without prior init" };
|
||||||
}
|
}
|
||||||
|
if (upload.sawPaddedChunk) {
|
||||||
|
this.buffers.delete(tempPath);
|
||||||
|
return {
|
||||||
|
action: "error",
|
||||||
|
message: `Fast upload received data after a padded chunk for ${upload.targetPath}; retry the upload from the beginning.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (upload.totalBase64Chars + base64Chunk.length > (this.maxBufferBytes * 4) / 3) {
|
if (upload.totalBase64Chars + base64Chunk.length > (this.maxBufferBytes * 4) / 3) {
|
||||||
this.buffers.delete(tempPath);
|
this.buffers.delete(tempPath);
|
||||||
@@ -91,6 +100,7 @@ export class FastUploadInterceptor {
|
|||||||
|
|
||||||
upload.chunks.push(base64Chunk);
|
upload.chunks.push(base64Chunk);
|
||||||
upload.totalBase64Chars += base64Chunk.length;
|
upload.totalBase64Chars += base64Chunk.length;
|
||||||
|
upload.sawPaddedChunk = base64Chunk.endsWith("=");
|
||||||
return { action: "ack", reason: `buffered ${base64Chunk.length} base64 chars` };
|
return { action: "ack", reason: `buffered ${base64Chunk.length} base64 chars` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
|
|||||||
import plugin, {
|
import plugin, {
|
||||||
buildSandboxExecCommand,
|
buildSandboxExecCommand,
|
||||||
buildSandboxExecShellCommand,
|
buildSandboxExecShellCommand,
|
||||||
|
deriveUploadTargetDir,
|
||||||
extractAdapterEnvFromProcess,
|
extractAdapterEnvFromProcess,
|
||||||
} from "../../src/plugin.js";
|
} from "../../src/plugin.js";
|
||||||
|
|
||||||
@@ -179,4 +180,10 @@ describe("plugin", () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual(["/bin/sh", "-lc", "pnpm test -- --runInBand"]);
|
).toEqual(["/bin/sh", "-lc", "pnpm test -- --runInBand"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives upload target directories for root and nested paths", () => {
|
||||||
|
expect(deriveUploadTargetDir("/file")).toBe("/");
|
||||||
|
expect(deriveUploadTargetDir("/workspace/file")).toBe("/workspace");
|
||||||
|
expect(deriveUploadTargetDir("relative-file")).toBe(".");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,6 +62,27 @@ describe("FastUploadInterceptor", () => {
|
|||||||
expect(interceptor.pendingCount).toBe(1);
|
expect(interceptor.pendingCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails fast when data arrives after a padded chunk", () => {
|
||||||
|
const interceptor = new FastUploadInterceptor();
|
||||||
|
const target = "/workspace/file.bin";
|
||||||
|
|
||||||
|
expect(
|
||||||
|
interceptor.decide(
|
||||||
|
`mkdir -p '/workspace' && rm -f '${target}.paperclip-upload.b64' && : > '${target}.paperclip-upload.b64'`,
|
||||||
|
),
|
||||||
|
).toMatchObject({ action: "ack" });
|
||||||
|
expect(
|
||||||
|
interceptor.decide(`printf '%s' 'aGVs=' >> '${target}.paperclip-upload.b64'`),
|
||||||
|
).toMatchObject({ action: "ack" });
|
||||||
|
|
||||||
|
const decision = interceptor.decide(`printf '%s' 'bG8=' >> '${target}.paperclip-upload.b64'`);
|
||||||
|
expect(decision).toMatchObject({
|
||||||
|
action: "error",
|
||||||
|
message: expect.stringContaining("received data after a padded chunk"),
|
||||||
|
});
|
||||||
|
expect(interceptor.pendingCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("falls through when the init command does not match the target parent directory", () => {
|
it("falls through when the init command does not match the target parent directory", () => {
|
||||||
const interceptor = new FastUploadInterceptor();
|
const interceptor = new FastUploadInterceptor();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user