feat: complete TASK-WP2-05 generation submission
This commit is contained in:
@@ -113,6 +113,9 @@ function sniffMime(prefix: Buffer) {
|
||||
return "image/png";
|
||||
}
|
||||
if (prefix.length >= 3 && prefix[0] === 0xff && prefix[1] === 0xd8 && prefix[2] === 0xff) return "image/jpeg";
|
||||
if (prefix.length >= 12 && prefix.subarray(0, 4).toString("ascii") === "RIFF" && prefix.subarray(8, 12).toString("ascii") === "WEBP") {
|
||||
return "image/webp";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
@@ -129,7 +132,7 @@ function listFiles(root: string): string[] {
|
||||
|
||||
export interface CommitStreamInput {
|
||||
content: Readable;
|
||||
expectedMimeType: "image/png" | "image/jpeg" | "application/octet-stream";
|
||||
expectedMimeType: "image/png" | "image/jpeg" | "image/webp" | "application/octet-stream";
|
||||
expectedSha256?: string;
|
||||
failurePoint?: CommitFailurePoint;
|
||||
fileKind: ManagedFileKind;
|
||||
@@ -139,6 +142,20 @@ export interface CommitStreamInput {
|
||||
projectedWriteBytes: number;
|
||||
}
|
||||
|
||||
export interface StagedManagedFile {
|
||||
bytes: number;
|
||||
destinationPath: string;
|
||||
fileId: string;
|
||||
fileKind: ManagedFileKind;
|
||||
mimeType: "image/png" | "image/jpeg" | "image/webp";
|
||||
operationId: string;
|
||||
ownerRef: string;
|
||||
relativePath: string;
|
||||
sha256: string;
|
||||
stagingDirectory: string;
|
||||
stagingPath: string;
|
||||
}
|
||||
|
||||
export class ManagedStorage {
|
||||
readonly dataRoot: string;
|
||||
readonly databasePath: string;
|
||||
@@ -479,6 +496,87 @@ export class ManagedStorage {
|
||||
}
|
||||
}
|
||||
|
||||
async stagePrivateImage(input: {
|
||||
content: Readable;
|
||||
expectedMimeType: "image/png" | "image/jpeg" | "image/webp";
|
||||
fileName: string;
|
||||
maximumBytes: number;
|
||||
operationId: string;
|
||||
ownerRef: string;
|
||||
projectedWriteBytes: number;
|
||||
}): Promise<StagedManagedFile> {
|
||||
const fileId = randomUUID();
|
||||
const destination = this.destination({
|
||||
content: input.content,
|
||||
expectedMimeType: input.expectedMimeType,
|
||||
fileKind: "reference",
|
||||
fileName: input.fileName,
|
||||
operationId: input.operationId,
|
||||
ownerRef: input.ownerRef,
|
||||
projectedWriteBytes: input.projectedWriteBytes,
|
||||
}, fileId);
|
||||
if (!Number.isSafeInteger(input.maximumBytes) || input.maximumBytes <= 0) throw new Error("maximum_bytes_invalid");
|
||||
this.reserve(input.operationId, input.projectedWriteBytes);
|
||||
const stagingDirectory = resolvePathWithinRoot(this.dataRoot, `staging/${input.operationId}`);
|
||||
const stagingPath = resolvePathWithinRoot(this.dataRoot, `staging/${input.operationId}/payload.tmp`);
|
||||
try {
|
||||
mkdirSync(stagingDirectory, { recursive: true });
|
||||
const hash = createHash("sha256");
|
||||
let byteSize = 0;
|
||||
let prefix = Buffer.alloc(0);
|
||||
const inspect = new Transform({
|
||||
transform(chunk: Buffer | string, encoding, callback) {
|
||||
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
||||
byteSize += bytes.byteLength;
|
||||
if (byteSize > input.maximumBytes) return callback(new Error("content_size_invalid"));
|
||||
hash.update(bytes);
|
||||
if (prefix.byteLength < 16) prefix = Buffer.concat([prefix, bytes.subarray(0, 16 - prefix.byteLength)]);
|
||||
callback(null, bytes);
|
||||
},
|
||||
});
|
||||
await pipeline(input.content, inspect, createWriteStream(stagingPath, { flags: "wx" }));
|
||||
validatePositiveBytes(byteSize, "actual_write_bytes");
|
||||
if (sniffMime(prefix) !== input.expectedMimeType) throw new Error("content_mime_invalid");
|
||||
const state = this.getState();
|
||||
const otherReservations = this.activeReservationBytes(input.operationId);
|
||||
if (state.managed_content_bytes + otherReservations + byteSize > HARD_LIMIT_BYTES) {
|
||||
throw new StorageCapacityError({ activeReservationBytes: otherReservations, managedContentBytes: state.managed_content_bytes, projectedWriteBytes: byteSize });
|
||||
}
|
||||
this.database.prepare("UPDATE storage_reservations SET projected_bytes = ? WHERE operation_id = ? AND status = 'active'")
|
||||
.run(byteSize, input.operationId);
|
||||
this.refreshState();
|
||||
return {
|
||||
bytes: byteSize,
|
||||
destinationPath: destination.absolutePath,
|
||||
fileId,
|
||||
fileKind: "reference",
|
||||
mimeType: input.expectedMimeType,
|
||||
operationId: input.operationId,
|
||||
ownerRef: input.ownerRef,
|
||||
relativePath: destination.relativePath,
|
||||
sha256: hash.digest("hex"),
|
||||
stagingDirectory,
|
||||
stagingPath,
|
||||
};
|
||||
} catch (error) {
|
||||
rmSync(stagingDirectory, { force: true, recursive: true });
|
||||
this.releaseReservation(input.operationId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
moveStagedFile(file: StagedManagedFile) {
|
||||
mkdirSync(dirname(file.destinationPath), { recursive: true });
|
||||
renameSync(file.stagingPath, file.destinationPath);
|
||||
rmSync(file.stagingDirectory, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
abandonStagedFile(file: StagedManagedFile) {
|
||||
if (existsSync(file.destinationPath)) this.queueCompensation(file.relativePath, statSync(file.destinationPath).size);
|
||||
else rmSync(file.stagingDirectory, { force: true, recursive: true });
|
||||
this.releaseReservation(file.operationId);
|
||||
}
|
||||
|
||||
async commitBufferFixture(fileKind: ManagedFileKind, fileName: string, bytes: Buffer) {
|
||||
return this.commitStream({
|
||||
content: Readable.from(bytes),
|
||||
|
||||
Reference in New Issue
Block a user