164 lines
7.2 KiB
JavaScript
164 lines
7.2 KiB
JavaScript
import { afterEach, describe, expect, test } from "bun:test";
|
|
import { Database } from "bun:sqlite";
|
|
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fetchEdgeEverApp } from "./index.ts";
|
|
import { createSelfHostedStorageAdapter } from "./self-hosted-storage-adapter.ts";
|
|
import { createEdgeEverClient } from "../../../packages/client/src/index.ts";
|
|
|
|
const temporaryDirectories = [];
|
|
afterEach(async () => {
|
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
|
});
|
|
|
|
const requestJson = async (environment, path, init = {}) => {
|
|
const response = await fetchEdgeEverApp(
|
|
new Request(`http://edgeever.test${path}`, init),
|
|
environment,
|
|
{ waitUntil: () => undefined, passThroughOnException: () => undefined },
|
|
);
|
|
const body = await response.json();
|
|
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
|
|
return body;
|
|
};
|
|
|
|
describe("resource upload integration", () => {
|
|
test("uploads small images directly and assembles large resources through the filesystem adapter", async () => {
|
|
const directory = await mkdtemp(`${tmpdir()}/edgeever-upload-integration-`);
|
|
temporaryDirectories.push(directory);
|
|
const database = new Database(join(directory, "edgeever.sqlite"), { create: true });
|
|
database.exec("PRAGMA foreign_keys = ON");
|
|
const migrationDirectory = new URL("../../../migrations/", import.meta.url);
|
|
for (const name of (await readdir(migrationDirectory)).filter((value) => value.endsWith(".sql")).sort()) {
|
|
database.exec(await readFile(new URL(name, migrationDirectory), "utf8"));
|
|
}
|
|
database.query(
|
|
"INSERT OR IGNORE INTO workspaces (id, name, is_personal) VALUES ('ws_default', 'Personal', 1)",
|
|
).run();
|
|
|
|
const resourcesDirectory = join(directory, "resources");
|
|
const environment = {
|
|
storage: createSelfHostedStorageAdapter(database, resourcesDirectory),
|
|
EDGE_EVER_ALLOW_UNAUTHENTICATED: "true",
|
|
EDGE_EVER_RUNTIME: "self-hosted-bun",
|
|
};
|
|
const notebook = await requestJson(environment, "/api/v1/notebooks", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: "Uploads" }),
|
|
});
|
|
const memo = await requestJson(environment, "/api/v1/memos", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ notebookId: notebook.notebook.id, title: "Multipart", contentMarkdown: "" }),
|
|
});
|
|
|
|
const uploadPaths = [];
|
|
const client = createEdgeEverClient({
|
|
baseUrl: "http://edgeever.test",
|
|
fetch: (input, init) => {
|
|
uploadPaths.push(new URL(input).pathname);
|
|
return fetchEdgeEverApp(new Request(input, init), environment,
|
|
{ waitUntil: () => undefined, passThroughOnException: () => undefined });
|
|
},
|
|
});
|
|
const smallImage = new File([
|
|
Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64"),
|
|
], "small.png", { type: "image/png" });
|
|
const images = await Promise.all([0, 1].map(() => client.uploadMemoResource(memo.memo.id, smallImage)));
|
|
expect(uploadPaths).toEqual(Array(2).fill(`/api/v1/memos/${memo.memo.id}/resources`));
|
|
for (const { resource } of images) {
|
|
expect(resource).toMatchObject({ filename: "small.png", kind: "image", mimeType: "image/png", byteSize: smallImage.size });
|
|
const imageRow = database.query("SELECT object_key FROM resources WHERE id = ?").get(resource.id);
|
|
expect(await readFile(join(resourcesDirectory, imageRow.object_key)))
|
|
.toEqual(Buffer.from(await smallImage.arrayBuffer()));
|
|
}
|
|
|
|
const file = new Uint8Array(8 * 1024 * 1024 + 3);
|
|
file.fill(7);
|
|
file.set([1, 2, 3], file.byteLength - 3);
|
|
const started = await requestJson(environment, `/api/v1/memos/${memo.memo.id}/resource-uploads`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ filename: "archive.bin", mimeType: "application/octet-stream", byteSize: file.byteLength }),
|
|
});
|
|
expect(started.upload.partCount).toBe(2);
|
|
|
|
for (let partNumber = 1; partNumber <= started.upload.partCount; partNumber += 1) {
|
|
const start = (partNumber - 1) * started.upload.partSize;
|
|
const chunk = file.slice(start, Math.min(start + started.upload.partSize, file.byteLength));
|
|
await requestJson(environment, `/api/v1/resource-uploads/${started.upload.id}/parts/${partNumber}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/octet-stream", "Content-Length": String(chunk.byteLength) },
|
|
body: chunk,
|
|
});
|
|
}
|
|
const completed = await requestJson(environment, `/api/v1/resource-uploads/${started.upload.id}/complete`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
});
|
|
|
|
expect(completed.resource).toMatchObject({
|
|
memoId: memo.memo.id,
|
|
filename: "archive.bin",
|
|
byteSize: file.byteLength,
|
|
sha256: null,
|
|
});
|
|
const row = database.query("SELECT object_key FROM resources WHERE id = ?").get(completed.resource.id);
|
|
expect(await readFile(join(resourcesDirectory, row.object_key))).toEqual(file);
|
|
expect(database.query("SELECT COUNT(*) AS count FROM resource_uploads").get().count).toBe(0);
|
|
|
|
const restoredBytes = new Uint8Array([9, 8, 7, 6, 5]);
|
|
const restoredMetadata = {
|
|
id: "res_restored",
|
|
memoId: memo.memo.id,
|
|
originalMemoId: null,
|
|
kind: "attachment",
|
|
mimeType: "application/octet-stream",
|
|
filename: "restored.bin",
|
|
byteSize: restoredBytes.byteLength,
|
|
sha256: "backup-checksum",
|
|
width: null,
|
|
height: null,
|
|
createdAt: "2026-08-31T00:00:00.000Z",
|
|
updatedAt: "2026-08-31T00:00:00.000Z",
|
|
archivePath: "resources/res_restored/restored.bin",
|
|
};
|
|
const restoreStarted = await requestJson(
|
|
environment,
|
|
"/api/v1/restores/json/resources/res_restored/uploads",
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(restoredMetadata),
|
|
},
|
|
);
|
|
expect(restoreStarted.upload.resourceId).toBe("res_restored");
|
|
await requestJson(environment, `/api/v1/resource-uploads/${restoreStarted.upload.id}/parts/1`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/octet-stream",
|
|
"Content-Length": String(restoredBytes.byteLength),
|
|
},
|
|
body: restoredBytes,
|
|
});
|
|
const restored = await requestJson(environment, `/api/v1/resource-uploads/${restoreStarted.upload.id}/complete`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
});
|
|
expect(restored.resource).toMatchObject({
|
|
id: "res_restored",
|
|
memoId: memo.memo.id,
|
|
filename: "restored.bin",
|
|
byteSize: restoredBytes.byteLength,
|
|
sha256: "backup-checksum",
|
|
});
|
|
const restoredRow = database.query("SELECT object_key FROM resources WHERE id = ?").get("res_restored");
|
|
expect(await readFile(join(resourcesDirectory, restoredRow.object_key))).toEqual(restoredBytes);
|
|
database.close();
|
|
});
|
|
});
|