Add group cloud: files index api, web UI, archive sync bot
The Telegram-like group file contract (packages/core shared-files.ts) becomes a working subsystem, built in three parallel tracks: API (NestJS): migration 029 group_files (chat_media_archive metadata-only index with partial replay-unique + RLS, group_file_shares with sha256 token persistence, archive_sync_state/outbox durable-claim seam incl. a unique claim_key index), group-files module (list/search with cursor pagination, direct upload ≤20 MiB streamed+hashed to oCIS WebDAV, download with ACL + retention rechecks, share links with expiry, retention modes incl. legalHold) — RBAC group-membership authz + audit chain events. Web: Files section per group — group picker, drag-drop upload (20 MiB cap), search + MIME/origin filters, cursor pagination, download via authenticated blob fetch, share dialog with expiry + shown-once token, admin retention dialog; bilingual EN/ES (wiki i18n pattern); nav sidebar/palette/bottom-nav wiring; MIME classes aligned to the DB CHECK (document/other). Archive bot (apps/archive-bot): plain sync-loop Matrix bot mirroring attachments from explicitly-mapped unencrypted archive rooms — raw /sync with durable since-cursor, outbox-claimed ingestion (hash-verified stream, no temp files), magic-byte MIME sniffing, safe-name normalization, WebDAV upload, indexed + replied with link, redaction→unavailable; health endpoint; Dockerfile. Tests 74/74, typecheck clean across api/web/bot.
This commit is contained in:
parent
eb1b60f67d
commit
d58db63fe6
58 changed files with 5543 additions and 4 deletions
50
apps/archive-bot/src/main.ts
Normal file
50
apps/archive-bot/src/main.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Archiver } from "./archiver.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { startHealthServer } from "./health.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { MatrixClient } from "./matrix.js";
|
||||
import { ArchiveProcessor } from "./processor.js";
|
||||
import { PgArchiveStore } from "./store.js";
|
||||
import { OcisWebDav } from "./webdav.js";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const shutdown = new AbortController();
|
||||
for (const signal of ["SIGTERM", "SIGINT"] as const) {
|
||||
process.once(signal, () => {
|
||||
logger.info({ signal }, "archive-bot: shutting down");
|
||||
shutdown.abort();
|
||||
});
|
||||
}
|
||||
|
||||
const matrix = new MatrixClient({
|
||||
homeserver: config.homeserverUrl,
|
||||
user: config.matrixUser,
|
||||
password: config.matrixPassword,
|
||||
deviceId: config.matrixDeviceId,
|
||||
mediaRedirectHosts: config.mediaRedirectHosts,
|
||||
roomIds: config.mappings.map((m) => m.roomId),
|
||||
logger,
|
||||
});
|
||||
await matrix.login(shutdown.signal);
|
||||
logger.info({ userId: matrix.userId, rooms: config.mappings.length }, "archive-bot: logged in");
|
||||
|
||||
const store = PgArchiveStore.create(config.databaseUrl, matrix.userId);
|
||||
const library = new OcisWebDav(config.webdav.baseUrl, config.webdav.user, config.webdav.password);
|
||||
const processor = new ArchiveProcessor(matrix, store, new Archiver(matrix, library, store), config.mappings, logger);
|
||||
await processor.init();
|
||||
|
||||
const health = startHealthServer(config.healthPort, () => processor.health(), logger);
|
||||
try {
|
||||
await processor.run(shutdown.signal, (signal) => matrix.login(signal));
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => health.close(() => resolve()));
|
||||
await store.close();
|
||||
}
|
||||
logger.info("archive-bot: stopped");
|
||||
}
|
||||
|
||||
void main().catch((error: unknown) => {
|
||||
logger.fatal({ err: error instanceof Error ? error.message : String(error) }, "archive-bot: fatal");
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue