feat: retire oCIS — the hub runs entirely on R2 object storage

Files payloads moved to R2 in 041; this removes the remaining oCIS coupling:
- delete the oCIS WebDAV client; verifyWhileStreaming moves to its own
  storage-agnostic module (verify-stream.ts)
- group-files: storage_key is the only payload source; legacy rows answer
  'retired storage' instead of touching oCIS
- collective share: the ocis-resource item kind is gone (peertube links stay);
  share-into-library was R2-backed since the previous commit
- import-drive endpoint removed (no web caller); import-chat (homeserver) stays
- web: Drive tab + embedded oCIS page removed; Library is the Files view
Legacy e2e rows (2) deleted; every remaining archive row is R2-backed.
This commit is contained in:
glass beadian 2026-10-16 01:32:56 +00:00
parent 4cbb63920a
commit e7bf870de9
13 changed files with 88 additions and 750 deletions

View file

@ -16,7 +16,7 @@ import {
requireRoomId,
requireSha256,
} from "./group-files.validation.js";
import { OcisVerificationError, ocisConfigFromEnv, OcisWebdavClient, verifyWhileStreaming } from "./ocis-webdav.js";
import { OcisVerificationError, verifyWhileStreaming } from "./verify-stream.js";
import { hashShareToken, isPlausibleShareToken, mintShareToken, sameToken } from "./share-token.js";
/** Deterministic 64-hex helper: repeat a pattern to exactly 64 chars. */
@ -185,59 +185,7 @@ describe("share tokens", () => {
});
});
describe("ocis-webdav config + streaming verification", () => {
it("reads env lazily and throws 503 when unconfigured", () => {
expect(() => ocisConfigFromEnv({})).toThrow(ServiceUnavailableException);
expect(() => ocisConfigFromEnv({ FILES_OCID_WEBDAV_URL: "https://files.example.org" })).toThrow(ServiceUnavailableException);
const config = ocisConfigFromEnv({
FILES_OCID_WEBDAV_URL: "https://files.example.org/",
FILES_OCID_USER: "svc",
FILES_OCID_PASSWORD: "pw",
FILES_PUBLIC_BASE: "https://files.example.org/",
});
expect(config.webdavUrl).toBe("https://files.example.org");
expect(config.publicBase).toBe("https://files.example.org");
});
it("uploads hash the actual streamed bytes and surface oc-fileid/oc-etag", async () => {
const bytes = Buffer.from("group file payload");
const calls: string[] = [];
const fetcher = (async (url: string, init: RequestInit) => {
calls.push(`${init.method} ${url}`);
return new Response(null, {
status: 201,
headers: init.method === "MKCOL" ? new Headers() : { "oc-fileid": "res-1", "oc-etag": "v-9" },
});
}) as unknown as typeof fetch;
const client = new OcisWebdavClient(
{ webdavUrl: "https://files.example.org", user: "svc", password: "pw", publicBase: "https://files.example.org" },
fetcher,
);
const result = await client.upload("group-1", "clip.mp4", Readable.from([bytes]), "video/mp4");
const { createHash } = await import("node:crypto");
expect(result.sha256).toBe(createHash("sha256").update(bytes).digest("hex"));
expect(result.sizeBytes).toBe(bytes.byteLength);
expect(result.resourceId).toBe("res-1");
expect(result.version).toBe("v-9");
expect(calls[0]).toBe("MKCOL https://files.example.org/remote.php/dav/files/svc/hub-groups");
expect(calls[1]).toBe("MKCOL https://files.example.org/remote.php/dav/files/svc/hub-groups/group-1");
expect(calls[2]).toBe("PUT https://files.example.org/remote.php/dav/files/svc/hub-groups/group-1/clip.mp4");
expect(client.uploadUrl("g", "n.mp4")).toContain("/remote.php/dav/files/svc/hub-groups/g/n.mp4");
expect(client.libraryUrl("res-1")).toBe("https://files.example.org/f/res-1");
});
it("fails the upload when the group folder cannot be created", async () => {
const fetcher = (async (url: string, init: RequestInit) =>
new Response(null, { status: init.method === "MKCOL" ? 500 : 201 })) as unknown as typeof fetch;
const client = new OcisWebdavClient(
{ webdavUrl: "https://files.example.org", user: "svc", password: "pw", publicBase: "https://files.example.org" },
fetcher,
);
await expect(
client.upload("group-1", "clip.mp4", Readable.from(["x"]), "text/plain"),
).rejects.toThrow("oCIS folder creation failed with 500");
});
describe("verify-stream (storage-agnostic streaming verification)", () => {
it("verifyWhileStreaming passes on match and aborts on tamper", async () => {
const { createHash } = await import("node:crypto");
const payload = Buffer.from("0123456789");