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

@ -28,8 +28,8 @@ import { appendTenantAudit, type AuditEvent } from "../audit/audit.service.js";
import { withSystem, withTenant } from "../db/client.js";
import { isUuid, parseLimit } from "../http/validation.js";
import { decodeFileListCursor, encodeFileListCursor } from "./file-cursor.js";
import { ocisConfigFromEnv, OcisWebdavClient, type FetchLike, type OcisDownload } from "./ocis-webdav.js";
import { S3Storage, s3ConfigFromEnv, type StorageService } from "../storage/storage.js";
import type { PayloadDownload } from "./verify-stream.js";
import { hashShareToken, mintShareToken } from "./share-token.js";
import { MIME_CLASSES, ORIGIN_KINDS, parseRetentionInput, type MimeClass, type OriginKind } from "./group-files.validation.js";
@ -38,7 +38,7 @@ export const GROUP_FILES_DEPENDENCIES: unique symbol = Symbol("GROUP_FILES_DEPEN
/** Wraps the raw payload handle the controller streams out. */
export interface FileDownloadHandle {
item: GroupFile;
download: OcisDownload;
download: PayloadDownload;
}
export interface ShareCreated {
@ -187,21 +187,11 @@ function rowToGroupFile(row: ArchiveRow, libraryUrl: (resourceId: string) => str
@Injectable()
export class GroupFilesService {
private client: OcisWebdavClient | null = null;
private store: StorageService | null = null;
constructor(@Optional() @Inject(GROUP_FILES_DEPENDENCIES) private readonly deps?: { fetcher?: FetchLike; storage?: StorageService }) {}
constructor(@Optional() @Inject(GROUP_FILES_DEPENDENCIES) private readonly deps?: { storage?: StorageService }) {}
/** Lazily bound: the oCIS env may be absent in dev/test contexts. Legacy rows only. */
private webdav(): OcisWebdavClient {
if (this.client === null) {
const fetcher: FetchLike = this.deps?.fetcher ?? ((url, init) => fetch(url, init));
this.client = new OcisWebdavClient(ocisConfigFromEnv(), fetcher);
}
return this.client;
}
/** R2 object storage — the primary payload store (041). */
/** R2 object storage — the only payload store (041; oCIS retired). */
private storage(): StorageService {
if (this.store === null) {
this.store = this.deps?.storage ?? new S3Storage(s3ConfigFromEnv());
@ -318,20 +308,18 @@ export class GroupFilesService {
async openDownload(workspaceId: string, userId: string, role: string, itemId: string): Promise<FileDownloadHandle> {
const item = await this.getFile(workspaceId, userId, role, itemId);
if (item.status !== "available") throw new GoneException(`file is ${item.status}`);
const download = await this.openPayload(item.file.storageKey ?? null, item.file.resourceId, item.mimeType).catch(() => {
const download = await this.openPayload(item.file.storageKey, item.mimeType).catch(() => {
throw new ServiceUnavailableException("file storage is unavailable");
});
return { item, download };
}
/** R2 for rows with a storage key (041+); oCIS WebDAV for legacy rows. */
private async openPayload(storageKey: string | null | undefined, resourceId: string, mime: string): Promise<OcisDownload> {
const key = storageKey ?? null;
if (key !== null) {
const bytes = await this.storage().getObject(key);
return { stream: Readable.from(bytes), sizeBytes: bytes.byteLength, contentType: mime };
private async openPayload(storageKey: string | null | undefined, mime: string): Promise<PayloadDownload> {
if (storageKey === null || storageKey === undefined) {
throw new ServiceUnavailableException("file was stored on retired storage and is no longer served");
}
return this.webdav().download(resourceId);
const bytes = await this.storage().getObject(storageKey);
return { stream: Readable.from(bytes), sizeBytes: bytes.byteLength, contentType: mime };
}
// -------------------------------------------------------------------------
@ -358,7 +346,7 @@ export class GroupFilesService {
},
});
const after = await this.requireRow(client, itemId);
return rowToGroupFile(after, (resourceId) => this.webdav().libraryUrl(resourceId));
return rowToGroupFile(after, (resourceId) => this.libraryUrlFor(resourceId));
});
}
@ -375,7 +363,7 @@ export class GroupFilesService {
diff: { name: before.safe_name, roomId: before.room_id, eventId: before.event_id },
});
const after = await this.requireRow(client, itemId);
return rowToGroupFile(after, (resourceId) => this.webdav().libraryUrl(resourceId));
return rowToGroupFile(after, (resourceId) => this.libraryUrlFor(resourceId));
});
}
@ -470,7 +458,7 @@ export class GroupFilesService {
throw new GoneException("file retention has expired");
}
const item = rowToGroupFile(row, (resourceId) => this.libraryUrlFor(resourceId));
const download = await this.openPayload(row.storage_key, item.file.resourceId, item.mimeType).catch(() => {
const download = await this.openPayload(row.storage_key, item.mimeType).catch(() => {
throw new ServiceUnavailableException("file storage is unavailable");
});
return { item, download };
@ -590,7 +578,7 @@ export class GroupFilesService {
const safe = safeFileName(file.originalName, sniff.extension);
// Object storage is the only payload path (user directive 2026-10-06):
// SPENC1-sealed, content-addressed, deduplicated. ocis_resource_id keeps
// carrying the resource handle (now the R2 key) for legacy readers.
// carrying the resource handle (the R2 key) for legacy readers.
const stored = await this.storage().putContent(file.buffer, sniff.mime).catch(() => {
throw new ServiceUnavailableException("file storage is unavailable");
});