cursedbelt-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/dist/server/activity/index.d.ts +36 -0
- package/dist/server/activity/index.js +36 -0
- package/dist/server/activity/migrations.d.ts +21 -0
- package/dist/server/activity/migrations.js +55 -0
- package/dist/server/activity/shareSource.d.ts +11 -0
- package/dist/server/activity/shareSource.js +45 -0
- package/dist/server/activity/store.d.ts +123 -0
- package/dist/server/activity/store.js +178 -0
- package/dist/server/app.d.ts +56 -0
- package/dist/server/app.js +45 -0
- package/dist/server/audit/index.d.ts +2 -0
- package/dist/server/audit/index.js +2 -0
- package/dist/server/audit/migrations.d.ts +2 -0
- package/dist/server/audit/migrations.js +79 -0
- package/dist/server/audit/sqliteSink.d.ts +36 -0
- package/dist/server/audit/sqliteSink.js +95 -0
- package/dist/server/audit/sqliteSink.test.d.ts +1 -0
- package/dist/server/audit/sqliteSink.test.js +180 -0
- package/dist/server/auth/cookie.d.ts +22 -0
- package/dist/server/auth/cookie.js +35 -0
- package/dist/server/auth/csrf.d.ts +42 -0
- package/dist/server/auth/csrf.js +36 -0
- package/dist/server/auth/fileTokenSigner.d.ts +10 -0
- package/dist/server/auth/fileTokenSigner.js +22 -0
- package/dist/server/auth/getOnly.d.ts +20 -0
- package/dist/server/auth/getOnly.js +27 -0
- package/dist/server/auth/index.d.ts +12 -0
- package/dist/server/auth/index.js +15 -0
- package/dist/server/auth/jwt.d.ts +18 -0
- package/dist/server/auth/jwt.js +56 -0
- package/dist/server/auth/loginThrottle.d.ts +118 -0
- package/dist/server/auth/loginThrottle.js +204 -0
- package/dist/server/auth/middleware.d.ts +14 -0
- package/dist/server/auth/middleware.js +47 -0
- package/dist/server/auth/password.d.ts +14 -0
- package/dist/server/auth/password.js +65 -0
- package/dist/server/auth/sessionStore.d.ts +19 -0
- package/dist/server/auth/sessionStore.js +221 -0
- package/dist/server/auth/totp.d.ts +34 -0
- package/dist/server/auth/totp.js +81 -0
- package/dist/server/auth/types.d.ts +133 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/vault.d.ts +27 -0
- package/dist/server/auth/vault.js +57 -0
- package/dist/server/context.d.ts +35 -0
- package/dist/server/context.js +1 -0
- package/dist/server/ctgr.d.ts +32 -0
- package/dist/server/ctgr.js +69 -0
- package/dist/server/db/kysely.d.ts +14 -0
- package/dist/server/db/kysely.js +16 -0
- package/dist/server/db/plumbingTypes.d.ts +116 -0
- package/dist/server/db/plumbingTypes.js +12 -0
- package/dist/server/dropzone/duplicates.d.ts +82 -0
- package/dist/server/dropzone/duplicates.js +90 -0
- package/dist/server/dropzone/index.d.ts +13 -0
- package/dist/server/dropzone/index.js +13 -0
- package/dist/server/dropzone/scan.d.ts +166 -0
- package/dist/server/dropzone/scan.js +272 -0
- package/dist/server/errors.d.ts +7 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/guard/argon2Binding.d.ts +97 -0
- package/dist/server/guard/argon2Binding.js +138 -0
- package/dist/server/guard/index.d.ts +4 -0
- package/dist/server/guard/index.js +6 -0
- package/dist/server/guard/kdfWorker.d.ts +101 -0
- package/dist/server/guard/kdfWorker.js +301 -0
- package/dist/server/guard/kdfWorkerEntry.d.ts +1 -0
- package/dist/server/guard/kdfWorkerEntry.js +15 -0
- package/dist/server/guard/passwordVerifier.d.ts +65 -0
- package/dist/server/guard/passwordVerifier.js +122 -0
- package/dist/server/guard/revocationStore.d.ts +132 -0
- package/dist/server/guard/revocationStore.js +211 -0
- package/dist/server/guard/sessionSigner.d.ts +132 -0
- package/dist/server/guard/sessionSigner.js +303 -0
- package/dist/server/guard/verifier.d.ts +21 -0
- package/dist/server/guard/verifier.js +1 -0
- package/dist/server/incidents/channels.d.ts +161 -0
- package/dist/server/incidents/channels.js +357 -0
- package/dist/server/incidents/checks.d.ts +61 -0
- package/dist/server/incidents/checks.js +55 -0
- package/dist/server/incidents/index.d.ts +51 -0
- package/dist/server/incidents/index.js +51 -0
- package/dist/server/incidents/migrations.d.ts +20 -0
- package/dist/server/incidents/migrations.js +58 -0
- package/dist/server/incidents/notifier.d.ts +79 -0
- package/dist/server/incidents/notifier.js +141 -0
- package/dist/server/incidents/reporter.d.ts +60 -0
- package/dist/server/incidents/reporter.js +49 -0
- package/dist/server/incidents/routes.d.ts +91 -0
- package/dist/server/incidents/routes.js +202 -0
- package/dist/server/incidents/store.d.ts +75 -0
- package/dist/server/incidents/store.js +176 -0
- package/dist/server/incidents/types.d.ts +107 -0
- package/dist/server/incidents/types.js +32 -0
- package/dist/server/index.d.ts +25 -0
- package/dist/server/index.js +53 -0
- package/dist/server/jobs/fireQueue.d.ts +12 -0
- package/dist/server/jobs/fireQueue.js +37 -0
- package/dist/server/jobs/index.d.ts +6 -0
- package/dist/server/jobs/index.js +10 -0
- package/dist/server/jobs/logger.d.ts +10 -0
- package/dist/server/jobs/logger.js +14 -0
- package/dist/server/jobs/plainjobAdapter.d.ts +19 -0
- package/dist/server/jobs/plainjobAdapter.js +48 -0
- package/dist/server/jobs/polling.d.ts +17 -0
- package/dist/server/jobs/polling.js +79 -0
- package/dist/server/jobs/runner.d.ts +24 -0
- package/dist/server/jobs/runner.js +186 -0
- package/dist/server/jobs/timeout.d.ts +28 -0
- package/dist/server/jobs/timeout.js +47 -0
- package/dist/server/jobs/types.d.ts +167 -0
- package/dist/server/jobs/types.js +12 -0
- package/dist/server/master-lock/guard.d.ts +53 -0
- package/dist/server/master-lock/guard.js +205 -0
- package/dist/server/master-lock/index.d.ts +21 -0
- package/dist/server/master-lock/index.js +21 -0
- package/dist/server/master-lock/lockPage.d.ts +34 -0
- package/dist/server/master-lock/lockPage.js +281 -0
- package/dist/server/master-lock/masterLock.d.ts +294 -0
- package/dist/server/master-lock/masterLock.js +523 -0
- package/dist/server/master-lock/principals.d.ts +76 -0
- package/dist/server/master-lock/principals.js +157 -0
- package/dist/server/master-lock/seed.d.ts +47 -0
- package/dist/server/master-lock/seed.js +93 -0
- package/dist/server/master-lock/store.d.ts +19 -0
- package/dist/server/master-lock/store.js +20 -0
- package/dist/server/media-bun/image.d.ts +7 -0
- package/dist/server/media-bun/image.js +96 -0
- package/dist/server/media-bun/index.d.ts +4 -0
- package/dist/server/media-bun/index.js +10 -0
- package/dist/server/media-bun/streaming.d.ts +86 -0
- package/dist/server/media-bun/streaming.js +237 -0
- package/dist/server/media-bun/video.d.ts +35 -0
- package/dist/server/media-bun/video.js +181 -0
- package/dist/server/metrics/metricsBuffer.d.ts +39 -0
- package/dist/server/metrics/metricsBuffer.js +36 -0
- package/dist/server/metrics/normalizeRoute.d.ts +5 -0
- package/dist/server/metrics/normalizeRoute.js +58 -0
- package/dist/server/middleware/correlationId.d.ts +9 -0
- package/dist/server/middleware/correlationId.js +20 -0
- package/dist/server/middleware/corsAllowlist.d.ts +38 -0
- package/dist/server/middleware/corsAllowlist.js +37 -0
- package/dist/server/middleware/errorEnvelope.d.ts +17 -0
- package/dist/server/middleware/errorEnvelope.js +32 -0
- package/dist/server/middleware/index.d.ts +9 -0
- package/dist/server/middleware/index.js +10 -0
- package/dist/server/middleware/localOrigin.d.ts +71 -0
- package/dist/server/middleware/localOrigin.js +119 -0
- package/dist/server/middleware/rateLimit.d.ts +56 -0
- package/dist/server/middleware/rateLimit.js +86 -0
- package/dist/server/middleware/requestLogger.d.ts +29 -0
- package/dist/server/middleware/requestLogger.js +47 -0
- package/dist/server/middleware/securityHeaders.d.ts +29 -0
- package/dist/server/middleware/securityHeaders.js +44 -0
- package/dist/server/middleware/zValidator.d.ts +29 -0
- package/dist/server/middleware/zValidator.js +57 -0
- package/dist/server/migration/runner.d.ts +42 -0
- package/dist/server/migration/runner.js +70 -0
- package/dist/server/migration/types.d.ts +16 -0
- package/dist/server/migration/types.js +1 -0
- package/dist/server/net/outboundGuard.d.ts +30 -0
- package/dist/server/net/outboundGuard.js +199 -0
- package/dist/server/notifications/hub.d.ts +67 -0
- package/dist/server/notifications/hub.js +187 -0
- package/dist/server/notifications/index.d.ts +47 -0
- package/dist/server/notifications/index.js +46 -0
- package/dist/server/notifications/migrations.d.ts +13 -0
- package/dist/server/notifications/migrations.js +20 -0
- package/dist/server/notifications/routes.d.ts +58 -0
- package/dist/server/notifications/routes.js +113 -0
- package/dist/server/notifications/service.d.ts +58 -0
- package/dist/server/notifications/service.js +41 -0
- package/dist/server/paths/appDataHome.d.ts +19 -0
- package/dist/server/paths/appDataHome.js +68 -0
- package/dist/server/private-media/crypto.d.ts +37 -0
- package/dist/server/private-media/crypto.js +111 -0
- package/dist/server/private-media/index-store.d.ts +31 -0
- package/dist/server/private-media/index-store.js +36 -0
- package/dist/server/private-media/mountPrivateRoutes.d.ts +26 -0
- package/dist/server/private-media/mountPrivateRoutes.js +219 -0
- package/dist/server/private-media/pipeline.d.ts +37 -0
- package/dist/server/private-media/pipeline.js +97 -0
- package/dist/server/private-media/unlock.d.ts +52 -0
- package/dist/server/private-media/unlock.js +55 -0
- package/dist/server/resume/index.d.ts +2 -0
- package/dist/server/resume/index.js +2 -0
- package/dist/server/resume/routes.d.ts +41 -0
- package/dist/server/resume/routes.js +50 -0
- package/dist/server/resume/store.d.ts +37 -0
- package/dist/server/resume/store.js +38 -0
- package/dist/server/retention/archive.d.ts +78 -0
- package/dist/server/retention/archive.js +284 -0
- package/dist/server/retention/fleet.d.ts +50 -0
- package/dist/server/retention/fleet.js +66 -0
- package/dist/server/retention/index.d.ts +38 -0
- package/dist/server/retention/index.js +38 -0
- package/dist/server/retention/policy.d.ts +263 -0
- package/dist/server/retention/policy.js +264 -0
- package/dist/server/retention/store.d.ts +101 -0
- package/dist/server/retention/store.js +251 -0
- package/dist/server/retention/sweeper.d.ts +69 -0
- package/dist/server/retention/sweeper.js +289 -0
- package/dist/server/retention/target.d.ts +143 -0
- package/dist/server/retention/target.js +438 -0
- package/dist/server/search/index.d.ts +2 -0
- package/dist/server/search/index.js +6 -0
- package/dist/server/search/pgSearchProvider.d.ts +91 -0
- package/dist/server/search/pgSearchProvider.js +330 -0
- package/dist/server/search/pgSearchProvider.pg.test.d.ts +1 -0
- package/dist/server/search/pgSearchProvider.pg.test.js +338 -0
- package/dist/server/search/pgSearchSchema.d.ts +81 -0
- package/dist/server/search/pgSearchSchema.js +89 -0
- package/dist/server/sharing/index.d.ts +18 -0
- package/dist/server/sharing/index.js +16 -0
- package/dist/server/sharing/migrations.d.ts +6 -0
- package/dist/server/sharing/migrations.js +79 -0
- package/dist/server/sharing/router.d.ts +111 -0
- package/dist/server/sharing/router.js +93 -0
- package/dist/server/sharing/sql.d.ts +67 -0
- package/dist/server/sharing/sql.js +88 -0
- package/dist/server/sharing/store.d.ts +173 -0
- package/dist/server/sharing/store.js +484 -0
- package/dist/server/sqlite/backup.d.ts +48 -0
- package/dist/server/sqlite/backup.js +97 -0
- package/dist/server/sqlite/browse.d.ts +42 -0
- package/dist/server/sqlite/browse.js +84 -0
- package/dist/server/sqlite/export.d.ts +32 -0
- package/dist/server/sqlite/export.js +42 -0
- package/dist/server/sqlite/fts5.d.ts +106 -0
- package/dist/server/sqlite/fts5.js +154 -0
- package/dist/server/sqlite/health.d.ts +44 -0
- package/dist/server/sqlite/health.js +52 -0
- package/dist/server/sqlite/import.d.ts +36 -0
- package/dist/server/sqlite/import.js +108 -0
- package/dist/server/sqlite/index.d.ts +9 -0
- package/dist/server/sqlite/index.js +15 -0
- package/dist/server/sqlite/readonlySql.d.ts +15 -0
- package/dist/server/sqlite/readonlySql.js +65 -0
- package/dist/server/sqlite/stats.d.ts +22 -0
- package/dist/server/sqlite/stats.js +32 -0
- package/dist/server/sqlite/viewer.d.ts +17 -0
- package/dist/server/sqlite/viewer.js +56 -0
- package/dist/server/storage/binaryServerProcessing.d.ts +23 -0
- package/dist/server/storage/binaryServerProcessing.js +81 -0
- package/dist/server/storage/binaryServerStore.d.ts +3 -0
- package/dist/server/storage/binaryServerStore.js +95 -0
- package/dist/server/storage/diskStore.d.ts +2 -0
- package/dist/server/storage/diskStore.js +88 -0
- package/dist/server/storage/files/adapter.d.ts +33 -0
- package/dist/server/storage/files/adapter.js +203 -0
- package/dist/server/storage/files/catalogue.d.ts +72 -0
- package/dist/server/storage/files/catalogue.js +1033 -0
- package/dist/server/storage/files/chunkedUpload.d.ts +56 -0
- package/dist/server/storage/files/chunkedUpload.js +70 -0
- package/dist/server/storage/files/fileRoutes.d.ts +51 -0
- package/dist/server/storage/files/fileRoutes.js +405 -0
- package/dist/server/storage/files/index.d.ts +9 -0
- package/dist/server/storage/files/index.js +11 -0
- package/dist/server/storage/files/migrations.d.ts +2 -0
- package/dist/server/storage/files/migrations.js +163 -0
- package/dist/server/storage/files/mimeByExt.d.ts +1 -0
- package/dist/server/storage/files/mimeByExt.js +13 -0
- package/dist/server/storage/files/stack.d.ts +136 -0
- package/dist/server/storage/files/stack.js +132 -0
- package/dist/server/storage/files/storageOps.d.ts +54 -0
- package/dist/server/storage/files/storageOps.js +110 -0
- package/dist/server/storage/files/types.d.ts +261 -0
- package/dist/server/storage/files/types.js +22 -0
- package/dist/server/storage/imageThumbnail.d.ts +3 -0
- package/dist/server/storage/imageThumbnail.js +50 -0
- package/dist/server/storage/index.d.ts +15 -0
- package/dist/server/storage/index.js +27 -0
- package/dist/server/storage/mountMediaRoutes.d.ts +28 -0
- package/dist/server/storage/mountMediaRoutes.js +141 -0
- package/dist/server/storage/objectStoreMediaStore.d.ts +4 -0
- package/dist/server/storage/objectStoreMediaStore.js +57 -0
- package/dist/server/storage/s3ObjectStore.d.ts +63 -0
- package/dist/server/storage/s3ObjectStore.js +187 -0
- package/dist/server/storage/sources.d.ts +47 -0
- package/dist/server/storage/sources.js +148 -0
- package/dist/server/storage/streamRange.d.ts +19 -0
- package/dist/server/storage/streamRange.js +69 -0
- package/dist/server/storage/streamToken.d.ts +16 -0
- package/dist/server/storage/streamToken.js +54 -0
- package/dist/server/storage/transcodeJobs.d.ts +65 -0
- package/dist/server/storage/transcodeJobs.js +104 -0
- package/dist/server/storage/types.d.ts +193 -0
- package/dist/server/storage/types.js +41 -0
- package/dist/server/storage/videoDerivativeKeys.d.ts +127 -0
- package/dist/server/storage/videoDerivativeKeys.js +149 -0
- package/dist/server/storage/videoProcessor.d.ts +30 -0
- package/dist/server/storage/videoProcessor.js +148 -0
- package/dist/server/sync/alarm.d.ts +34 -0
- package/dist/server/sync/alarm.js +91 -0
- package/dist/server/sync/commands.d.ts +87 -0
- package/dist/server/sync/commands.js +241 -0
- package/dist/server/sync/engine.d.ts +62 -0
- package/dist/server/sync/engine.js +184 -0
- package/dist/server/sync/http.d.ts +106 -0
- package/dist/server/sync/http.js +243 -0
- package/dist/server/sync/index.d.ts +41 -0
- package/dist/server/sync/index.js +41 -0
- package/dist/server/sync/opLog.d.ts +61 -0
- package/dist/server/sync/opLog.js +96 -0
- package/dist/server/sync/planner.d.ts +31 -0
- package/dist/server/sync/planner.js +30 -0
- package/dist/server/sync/status.d.ts +63 -0
- package/dist/server/sync/status.js +49 -0
- package/dist/server/sync/timer.d.ts +52 -0
- package/dist/server/sync/timer.js +170 -0
- package/dist/server/sync/tokens.d.ts +25 -0
- package/dist/server/sync/tokens.js +51 -0
- package/dist/server/sync/types.d.ts +101 -0
- package/dist/server/sync/types.js +29 -0
- package/dist/server/ws/eventBus.d.ts +14 -0
- package/dist/server/ws/eventBus.js +42 -0
- package/dist/server/ws/hub.d.ts +20 -0
- package/dist/server/ws/hub.js +154 -0
- package/dist/server/ws/index.d.ts +5 -0
- package/dist/server/ws/index.js +7 -0
- package/dist/server/ws/presence.d.ts +11 -0
- package/dist/server/ws/presence.js +20 -0
- package/dist/server/ws/room.d.ts +17 -0
- package/dist/server/ws/room.js +23 -0
- package/dist/server/ws/types.d.ts +122 -0
- package/dist/server/ws/types.js +1 -0
- package/package.json +234 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/leafSubpathsImportNothing.spec.ts +358 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/server/activity/index.ts +59 -0
- package/src/server/activity/migrations.ts +67 -0
- package/src/server/activity/shareSource.ts +57 -0
- package/src/server/activity/store.spec.ts +255 -0
- package/src/server/activity/store.ts +368 -0
- package/src/server/app.spec.ts +243 -0
- package/src/server/app.ts +89 -0
- package/src/server/audit/index.ts +2 -0
- package/src/server/audit/migrations.ts +88 -0
- package/src/server/audit/sqliteSink.test.ts +200 -0
- package/src/server/audit/sqliteSink.ts +162 -0
- package/src/server/auth/cookie.spec.ts +46 -0
- package/src/server/auth/cookie.ts +42 -0
- package/src/server/auth/csrf.spec.ts +81 -0
- package/src/server/auth/csrf.ts +83 -0
- package/src/server/auth/fileTokenSigner.ts +40 -0
- package/src/server/auth/getOnly.spec.ts +53 -0
- package/src/server/auth/getOnly.ts +32 -0
- package/src/server/auth/index.ts +51 -0
- package/src/server/auth/jwt.spec.ts +106 -0
- package/src/server/auth/jwt.ts +75 -0
- package/src/server/auth/loginThrottle.spec.ts +213 -0
- package/src/server/auth/loginThrottle.ts +261 -0
- package/src/server/auth/middleware.spec.ts +148 -0
- package/src/server/auth/middleware.ts +56 -0
- package/src/server/auth/password.spec.ts +48 -0
- package/src/server/auth/password.ts +71 -0
- package/src/server/auth/sessionStore.spec.ts +189 -0
- package/src/server/auth/sessionStore.ts +261 -0
- package/src/server/auth/totp.spec.ts +73 -0
- package/src/server/auth/totp.ts +92 -0
- package/src/server/auth/types.ts +146 -0
- package/src/server/auth/vault.spec.ts +51 -0
- package/src/server/auth/vault.ts +71 -0
- package/src/server/context.ts +37 -0
- package/src/server/ctgr.spec.ts +138 -0
- package/src/server/ctgr.ts +108 -0
- package/src/server/db/kysely.spec.ts +62 -0
- package/src/server/db/kysely.ts +21 -0
- package/src/server/db/plumbingTypes.ts +126 -0
- package/src/server/dropzone/duplicates.ts +121 -0
- package/src/server/dropzone/index.ts +30 -0
- package/src/server/dropzone/scan.spec.ts +255 -0
- package/src/server/dropzone/scan.ts +342 -0
- package/src/server/errors.spec.ts +76 -0
- package/src/server/errors.ts +22 -0
- package/src/server/guard/argon2Binding.spec.ts +117 -0
- package/src/server/guard/argon2Binding.ts +195 -0
- package/src/server/guard/argon2Kat.spec.ts +250 -0
- package/src/server/guard/guard.spec.ts +390 -0
- package/src/server/guard/index.ts +6 -0
- package/src/server/guard/kdfWorker.spec.ts +283 -0
- package/src/server/guard/kdfWorker.ts +367 -0
- package/src/server/guard/kdfWorkerEntry.ts +16 -0
- package/src/server/guard/passwordVerifier.spec.ts +71 -0
- package/src/server/guard/passwordVerifier.ts +175 -0
- package/src/server/guard/revocationStore.spec.ts +135 -0
- package/src/server/guard/revocationStore.ts +291 -0
- package/src/server/guard/sessionSigner.ts +425 -0
- package/src/server/guard/verifier.ts +27 -0
- package/src/server/incidents/channels.ts +487 -0
- package/src/server/incidents/checks.ts +120 -0
- package/src/server/incidents/incidents.spec.ts +463 -0
- package/src/server/incidents/index.ts +96 -0
- package/src/server/incidents/migrations.ts +66 -0
- package/src/server/incidents/notifier.ts +215 -0
- package/src/server/incidents/reporter.ts +110 -0
- package/src/server/incidents/routes.ts +314 -0
- package/src/server/incidents/store.ts +329 -0
- package/src/server/incidents/types.ts +116 -0
- package/src/server/index.ts +248 -0
- package/src/server/jobs/fireQueue.spec.ts +85 -0
- package/src/server/jobs/fireQueue.ts +37 -0
- package/src/server/jobs/index.ts +26 -0
- package/src/server/jobs/logger.ts +16 -0
- package/src/server/jobs/plainjobAdapter.spec.ts +34 -0
- package/src/server/jobs/plainjobAdapter.ts +54 -0
- package/src/server/jobs/polling.spec.ts +219 -0
- package/src/server/jobs/polling.ts +81 -0
- package/src/server/jobs/runner.spec.ts +213 -0
- package/src/server/jobs/runner.ts +213 -0
- package/src/server/jobs/timeout.spec.ts +35 -0
- package/src/server/jobs/timeout.ts +53 -0
- package/src/server/jobs/types.ts +188 -0
- package/src/server/master-lock/guard.spec.ts +347 -0
- package/src/server/master-lock/guard.ts +290 -0
- package/src/server/master-lock/index.ts +44 -0
- package/src/server/master-lock/lockPage.spec.ts +88 -0
- package/src/server/master-lock/lockPage.ts +313 -0
- package/src/server/master-lock/masterLock.spec.ts +338 -0
- package/src/server/master-lock/masterLock.ts +627 -0
- package/src/server/master-lock/principals.spec.ts +378 -0
- package/src/server/master-lock/principals.ts +175 -0
- package/src/server/master-lock/seed.spec.ts +69 -0
- package/src/server/master-lock/seed.ts +112 -0
- package/src/server/master-lock/store.ts +35 -0
- package/src/server/media-bun/image.spec.ts +115 -0
- package/src/server/media-bun/image.ts +134 -0
- package/src/server/media-bun/index.ts +52 -0
- package/src/server/media-bun/streaming.spec.ts +134 -0
- package/src/server/media-bun/streaming.ts +347 -0
- package/src/server/media-bun/video.spec.ts +115 -0
- package/src/server/media-bun/video.ts +232 -0
- package/src/server/metrics/metricsBuffer.ts +92 -0
- package/src/server/metrics/normalizeRoute.spec.ts +51 -0
- package/src/server/metrics/normalizeRoute.ts +63 -0
- package/src/server/middleware/correlationId.ts +24 -0
- package/src/server/middleware/corsAllowlist.spec.ts +118 -0
- package/src/server/middleware/corsAllowlist.ts +69 -0
- package/src/server/middleware/errorEnvelope.ts +38 -0
- package/src/server/middleware/index.ts +19 -0
- package/src/server/middleware/localOrigin.spec.ts +168 -0
- package/src/server/middleware/localOrigin.ts +139 -0
- package/src/server/middleware/rateLimit.spec.ts +72 -0
- package/src/server/middleware/rateLimit.ts +125 -0
- package/src/server/middleware/requestLogger.ts +95 -0
- package/src/server/middleware/securityHeaders.ts +78 -0
- package/src/server/middleware/zValidator.spec.ts +251 -0
- package/src/server/middleware/zValidator.ts +114 -0
- package/src/server/migration/runner.spec.ts +88 -0
- package/src/server/migration/runner.ts +87 -0
- package/src/server/migration/types.ts +17 -0
- package/src/server/net/outboundGuard.spec.ts +90 -0
- package/src/server/net/outboundGuard.ts +205 -0
- package/src/server/notifications/hub.ts +245 -0
- package/src/server/notifications/index.ts +77 -0
- package/src/server/notifications/migrations.ts +39 -0
- package/src/server/notifications/routes.ts +177 -0
- package/src/server/notifications/service.spec.ts +305 -0
- package/src/server/notifications/service.ts +112 -0
- package/src/server/paths/appDataHome.spec.ts +87 -0
- package/src/server/paths/appDataHome.ts +84 -0
- package/src/server/private-media/blobRoute.spec.ts +165 -0
- package/src/server/private-media/crypto.spec.ts +81 -0
- package/src/server/private-media/crypto.ts +155 -0
- package/src/server/private-media/gatePolicy.spec.ts +98 -0
- package/src/server/private-media/index-store.ts +83 -0
- package/src/server/private-media/mountPrivateRoutes.ts +265 -0
- package/src/server/private-media/pipeline.spec.ts +111 -0
- package/src/server/private-media/pipeline.ts +170 -0
- package/src/server/private-media/routes.spec.ts +265 -0
- package/src/server/private-media/unlock.ts +112 -0
- package/src/server/resume/index.ts +8 -0
- package/src/server/resume/resume.spec.ts +118 -0
- package/src/server/resume/routes.ts +92 -0
- package/src/server/resume/store.ts +82 -0
- package/src/server/retention/archive.spec.ts +181 -0
- package/src/server/retention/archive.ts +353 -0
- package/src/server/retention/fleet.ts +104 -0
- package/src/server/retention/index.ts +101 -0
- package/src/server/retention/policy.spec.ts +132 -0
- package/src/server/retention/policy.ts +392 -0
- package/src/server/retention/store.spec.ts +236 -0
- package/src/server/retention/store.ts +338 -0
- package/src/server/retention/sweeper.spec.ts +321 -0
- package/src/server/retention/sweeper.ts +367 -0
- package/src/server/retention/target.spec.ts +592 -0
- package/src/server/retention/target.ts +613 -0
- package/src/server/search/index.ts +12 -0
- package/src/server/search/pgSearchProvider.pg.test.ts +392 -0
- package/src/server/search/pgSearchProvider.ts +421 -0
- package/src/server/search/pgSearchSchema.ts +147 -0
- package/src/server/sharing/index.ts +65 -0
- package/src/server/sharing/migrations.ts +128 -0
- package/src/server/sharing/router.spec.ts +167 -0
- package/src/server/sharing/router.ts +194 -0
- package/src/server/sharing/sql.ts +109 -0
- package/src/server/sharing/store.spec.ts +408 -0
- package/src/server/sharing/store.ts +737 -0
- package/src/server/sqlite/backup.spec.ts +114 -0
- package/src/server/sqlite/backup.ts +154 -0
- package/src/server/sqlite/browse.spec.ts +70 -0
- package/src/server/sqlite/browse.ts +120 -0
- package/src/server/sqlite/export.spec.ts +43 -0
- package/src/server/sqlite/export.ts +74 -0
- package/src/server/sqlite/fts5.spec.ts +175 -0
- package/src/server/sqlite/fts5.ts +255 -0
- package/src/server/sqlite/health.spec.ts +79 -0
- package/src/server/sqlite/health.ts +105 -0
- package/src/server/sqlite/import.spec.ts +75 -0
- package/src/server/sqlite/import.ts +141 -0
- package/src/server/sqlite/index.ts +64 -0
- package/src/server/sqlite/readonlySql.spec.ts +58 -0
- package/src/server/sqlite/readonlySql.ts +67 -0
- package/src/server/sqlite/stats.spec.ts +21 -0
- package/src/server/sqlite/stats.ts +61 -0
- package/src/server/sqlite/viewer.spec.ts +127 -0
- package/src/server/sqlite/viewer.ts +72 -0
- package/src/server/storage/binaryServerProcessing.integration.spec.ts +222 -0
- package/src/server/storage/binaryServerProcessing.spec.ts +150 -0
- package/src/server/storage/binaryServerProcessing.ts +127 -0
- package/src/server/storage/binaryServerStore.ts +102 -0
- package/src/server/storage/chunked.integration.spec.ts +185 -0
- package/src/server/storage/diskStore.ts +83 -0
- package/src/server/storage/files/adapter.photo.display.spec.ts +169 -0
- package/src/server/storage/files/adapter.photo.spec.ts +128 -0
- package/src/server/storage/files/adapter.spec.ts +121 -0
- package/src/server/storage/files/adapter.ts +255 -0
- package/src/server/storage/files/catalogue.media-host.spec.ts +184 -0
- package/src/server/storage/files/catalogue.orderings.spec.ts +157 -0
- package/src/server/storage/files/catalogue.private.spec.ts +106 -0
- package/src/server/storage/files/catalogue.recover.spec.ts +131 -0
- package/src/server/storage/files/catalogue.sort.spec.ts +154 -0
- package/src/server/storage/files/catalogue.spec.ts +133 -0
- package/src/server/storage/files/catalogue.trash-import.spec.ts +125 -0
- package/src/server/storage/files/catalogue.ts +1326 -0
- package/src/server/storage/files/chunkedUpload.spec.ts +111 -0
- package/src/server/storage/files/chunkedUpload.ts +146 -0
- package/src/server/storage/files/collections.spec.ts +294 -0
- package/src/server/storage/files/fileRoutes.replace.spec.ts +88 -0
- package/src/server/storage/files/fileRoutes.spec.ts +327 -0
- package/src/server/storage/files/fileRoutes.ts +462 -0
- package/src/server/storage/files/index.ts +66 -0
- package/src/server/storage/files/migrations.ts +187 -0
- package/src/server/storage/files/mimeByExt.ts +14 -0
- package/src/server/storage/files/stack.spec.ts +258 -0
- package/src/server/storage/files/stack.ts +284 -0
- package/src/server/storage/files/storageOps.spec.ts +126 -0
- package/src/server/storage/files/storageOps.ts +213 -0
- package/src/server/storage/files/types.ts +285 -0
- package/src/server/storage/imageThumbnail.spec.ts +81 -0
- package/src/server/storage/imageThumbnail.ts +60 -0
- package/src/server/storage/index.ts +122 -0
- package/src/server/storage/mountMediaRoutes.ts +231 -0
- package/src/server/storage/objectStore.spec.ts +272 -0
- package/src/server/storage/objectStoreMediaStore.ts +63 -0
- package/src/server/storage/s3ObjectStore.ts +239 -0
- package/src/server/storage/sources.ts +245 -0
- package/src/server/storage/storage.spec.ts +219 -0
- package/src/server/storage/streamRange.ts +92 -0
- package/src/server/storage/streamToken.ts +76 -0
- package/src/server/storage/transcode.integration.spec.ts +134 -0
- package/src/server/storage/transcodeJobs.spec.ts +152 -0
- package/src/server/storage/transcodeJobs.ts +171 -0
- package/src/server/storage/types.ts +230 -0
- package/src/server/storage/videoDerivativeKeys.spec.ts +112 -0
- package/src/server/storage/videoDerivativeKeys.ts +172 -0
- package/src/server/storage/videoProcessor.posterOnly.spec.ts +106 -0
- package/src/server/storage/videoProcessor.ts +229 -0
- package/src/server/sync/alarm.spec.ts +149 -0
- package/src/server/sync/alarm.ts +137 -0
- package/src/server/sync/commands.spec.ts +145 -0
- package/src/server/sync/commands.ts +361 -0
- package/src/server/sync/engine.spec.ts +496 -0
- package/src/server/sync/engine.ts +255 -0
- package/src/server/sync/http.ts +316 -0
- package/src/server/sync/httpRemote.spec.ts +158 -0
- package/src/server/sync/index.ts +90 -0
- package/src/server/sync/opLog.spec.ts +110 -0
- package/src/server/sync/opLog.ts +189 -0
- package/src/server/sync/planner.spec.ts +53 -0
- package/src/server/sync/planner.ts +62 -0
- package/src/server/sync/status.spec.ts +92 -0
- package/src/server/sync/status.ts +108 -0
- package/src/server/sync/timer.spec.ts +150 -0
- package/src/server/sync/timer.ts +207 -0
- package/src/server/sync/tokens.spec.ts +38 -0
- package/src/server/sync/tokens.ts +94 -0
- package/src/server/sync/types.ts +108 -0
- package/src/server/ws/eventBus.spec.ts +67 -0
- package/src/server/ws/eventBus.ts +46 -0
- package/src/server/ws/hub.integration.spec.ts +93 -0
- package/src/server/ws/hub.spec.ts +282 -0
- package/src/server/ws/hub.ts +168 -0
- package/src/server/ws/index.ts +20 -0
- package/src/server/ws/presence.spec.ts +27 -0
- package/src/server/ws/presence.ts +26 -0
- package/src/server/ws/room.spec.ts +36 -0
- package/src/server/ws/room.ts +29 -0
- package/src/server/ws/types.ts +134 -0
- package/src/skippedTestsAreAnswered.spec.ts +215 -0
- package/src/testTmpRoot.spec.ts +102 -0
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
import { mkdir, readdir, rm, stat as fsStat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { renderImage } from '../../media-bun/image';
|
|
4
|
+
import { sealMeta } from '../../private-media/crypto';
|
|
5
|
+
import { processPrivateBlob, processPrivateVideo } from '../../private-media/pipeline';
|
|
6
|
+
import { ffmpegImageThumbnail, isBrowserRenderableImage } from '../imageThumbnail';
|
|
7
|
+
import { fsDisplayKey, fsHlsPrefix, fsMp4Key, fsPosterKey, fsSourceKey, fsSpriteKey, fsVttKey, privateFsKey, } from '../types';
|
|
8
|
+
import { processVideo } from '../videoProcessor';
|
|
9
|
+
import { mimeByExt } from './mimeByExt';
|
|
10
|
+
import { classifyFileType, } from './types';
|
|
11
|
+
// Stream an upload body to a local plaintext file (staged for the private pipeline; wiped after).
|
|
12
|
+
// Bun.write(dest, new Response(stream)) hangs in Bun 1.3.x — pump via a FileSink instead.
|
|
13
|
+
async function streamToFile(body, dest) {
|
|
14
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
15
|
+
const sink = Bun.file(dest).writer();
|
|
16
|
+
const reader = body.getReader();
|
|
17
|
+
let size = 0;
|
|
18
|
+
try {
|
|
19
|
+
while (true) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done)
|
|
22
|
+
break;
|
|
23
|
+
size += value.byteLength;
|
|
24
|
+
sink.write(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await sink.end();
|
|
29
|
+
}
|
|
30
|
+
return size;
|
|
31
|
+
}
|
|
32
|
+
// The general primary binary for a NON-video file (images/docs/…) lives here; video files store a
|
|
33
|
+
// raw source (fsSourceKey) + derived mp4/HLS/poster/sprite via the shared video keyspace.
|
|
34
|
+
export const fileBinaryKey = (owner, id) => `fs/${owner}/${id}`;
|
|
35
|
+
/** Where a file's uploaded bytes must land for `registerUploaded` to find them: a video's raw
|
|
36
|
+
* source (fed to the transcode), else the primary binary. The chunked-upload path targets this key
|
|
37
|
+
* so an out-of-band upload lands exactly where the catalogue + serve routes expect it. */
|
|
38
|
+
export const catalogueUploadKey = (owner, id, mime) => mime.startsWith('video/') ? fsSourceKey(owner, id) : fileBinaryKey(owner, id);
|
|
39
|
+
const b64 = (s) => Buffer.from(s).toString('base64url');
|
|
40
|
+
const unb64 = (s) => Buffer.from(s, 'base64url').toString();
|
|
41
|
+
// Sort-aware keyset pagination. The cursor encodes (optional shuffle seed, sort key, id tie-break);
|
|
42
|
+
// the key is inner-b64'd so a file NAME containing '|' can't break parsing. The `random` sort mints
|
|
43
|
+
// a seed on the first page and threads it here so every subsequent page reshuffles identically. A
|
|
44
|
+
// cursor is only meaningful under the sort that minted it (the caller passes the same `sort` on
|
|
45
|
+
// every page). Segment order is `seed|b64(key)|id` — b64(key) and a UUID id never contain '|', and
|
|
46
|
+
// seed is a bare integer, so a 3-way split is unambiguous.
|
|
47
|
+
const encodeCursor = (key, id, seed) => b64(`${seed ?? ''}|${b64(String(key))}|${id}`);
|
|
48
|
+
function decodeCursor(c) {
|
|
49
|
+
try {
|
|
50
|
+
const [seed, k, id] = unb64(c).split('|');
|
|
51
|
+
if (!id)
|
|
52
|
+
return null;
|
|
53
|
+
return { seed: seed === '' ? undefined : Number(seed), key: unb64(k), id };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// The seeded `random` remap uses modular arithmetic under a Mersenne prime (2^31 - 1). rand_key and
|
|
60
|
+
// the derived coefficients are all < 2^31, so `rand_key * mul + add` stays < 2^62 — exact in both
|
|
61
|
+
// SQLite's signed-64-bit integer math and JS BigInt (the two must agree for keyset boundaries).
|
|
62
|
+
const RANDOM_MOD = 2147483647;
|
|
63
|
+
/** Derive the `random` sort spec for a given session seed: order by the seed-remapped rand_key
|
|
64
|
+
* (ties broken by id). The remap `(rand_key * mul + add) mod P` reshuffles per seed while staying a
|
|
65
|
+
* stable total order, so keyset pagination holds as long as the cursor carries the same seed. */
|
|
66
|
+
function randomSortSpec(seed) {
|
|
67
|
+
const mul = (seed % (RANDOM_MOD - 1)) + 1; // [1, P-1] — nonzero mod P
|
|
68
|
+
const add = seed % RANDOM_MOD; // [0, P-1]
|
|
69
|
+
const expr = '((f.rand_key * $rmul + $radd) % $rmod)';
|
|
70
|
+
return {
|
|
71
|
+
orderBy: `${expr} ASC, f.id ASC`,
|
|
72
|
+
keysetWhere: `(${expr} > $curKey OR (${expr} = $curKey AND f.id > $curId))`,
|
|
73
|
+
key: (r) => Number((BigInt(r.rand_key) * BigInt(mul) + BigInt(add)) % BigInt(RANDOM_MOD)),
|
|
74
|
+
numericKey: true,
|
|
75
|
+
params: { $rmul: mul, $radd: add, $rmod: RANDOM_MOD },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The `lastViewed` sort: most-recently-opened first, never-viewed files (no ledger row) last via
|
|
79
|
+
* COALESCE(...,0), ties broken by id. LEFT JOIN keeps files with no view. */
|
|
80
|
+
const LAST_VIEWED_SPEC = {
|
|
81
|
+
join: 'LEFT JOIN file_views v ON v.file_id = f.id',
|
|
82
|
+
selectExtra: 'COALESCE(v.viewed_at, 0) AS view_key',
|
|
83
|
+
orderBy: 'COALESCE(v.viewed_at, 0) DESC, f.id DESC',
|
|
84
|
+
keysetWhere: '(COALESCE(v.viewed_at, 0) < $curKey OR (COALESCE(v.viewed_at, 0) = $curKey AND f.id < $curId))',
|
|
85
|
+
key: (r) => r.view_key ?? 0,
|
|
86
|
+
numericKey: true,
|
|
87
|
+
};
|
|
88
|
+
/** Per-sort SQL for the STATIC (parameter-free) orders. `recent` = newest first; `oldest` = oldest
|
|
89
|
+
* first; `name` = A→Z; `size` = largest first. `random` / `lastViewed` are built per-request above. */
|
|
90
|
+
const SORT_SPECS = {
|
|
91
|
+
recent: {
|
|
92
|
+
orderBy: 'f.created_at DESC, f.id DESC',
|
|
93
|
+
keysetWhere: '(f.created_at < $curKey OR (f.created_at = $curKey AND f.id < $curId))',
|
|
94
|
+
key: (r) => r.created_at,
|
|
95
|
+
numericKey: true,
|
|
96
|
+
},
|
|
97
|
+
oldest: {
|
|
98
|
+
orderBy: 'f.created_at ASC, f.id ASC',
|
|
99
|
+
keysetWhere: '(f.created_at > $curKey OR (f.created_at = $curKey AND f.id > $curId))',
|
|
100
|
+
key: (r) => r.created_at,
|
|
101
|
+
numericKey: true,
|
|
102
|
+
},
|
|
103
|
+
name: {
|
|
104
|
+
orderBy: 'f.name ASC, f.id ASC',
|
|
105
|
+
keysetWhere: '(f.name > $curKey OR (f.name = $curKey AND f.id > $curId))',
|
|
106
|
+
key: (r) => r.name,
|
|
107
|
+
numericKey: false,
|
|
108
|
+
},
|
|
109
|
+
size: {
|
|
110
|
+
orderBy: 'f.size DESC, f.id DESC',
|
|
111
|
+
keysetWhere: '(f.size < $curKey OR (f.size = $curKey AND f.id < $curId))',
|
|
112
|
+
key: (r) => r.size,
|
|
113
|
+
numericKey: true,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
export function createSqliteFileCatalogue(opts) {
|
|
117
|
+
const { db, store, workDir } = opts;
|
|
118
|
+
const ownerOf = opts.owner ?? ((c) => c.tenantId);
|
|
119
|
+
const now = opts.now ?? (() => Date.now());
|
|
120
|
+
const rand31 = () => Math.floor(Math.random() * RANDOM_MOD);
|
|
121
|
+
const randKey = opts.randKey ?? rand31;
|
|
122
|
+
const randomSeed = opts.randomSeed ?? rand31;
|
|
123
|
+
const insertFile = db.query(`INSERT INTO files (id, owner, name, mime, size, file_type, is_private, processing_status, processing_progress, storage_backend, rand_key, created_at, updated_at)
|
|
124
|
+
VALUES ($id, $owner, $name, $mime, $size, $type, 0, $status, $progress, 'disk', $rand, $now, $now)`);
|
|
125
|
+
const insertTag = db.query(`INSERT OR IGNORE INTO file_tags (file_id, tag, owner, is_private) VALUES ($file, $tag, $owner, 0)`);
|
|
126
|
+
const deleteTags = db.query(`DELETE FROM file_tags WHERE file_id = $file`);
|
|
127
|
+
const getRow = db.query(`SELECT * FROM files WHERE id = $id AND owner = $owner`);
|
|
128
|
+
const deleteFile = db.query(`DELETE FROM files WHERE id = $id AND owner = $owner`);
|
|
129
|
+
const trashFile = db.query(`UPDATE files SET trashed_at = $now, updated_at = $now WHERE id = $id AND owner = $owner AND trashed_at IS NULL`);
|
|
130
|
+
const restoreFile = db.query(`UPDATE files SET trashed_at = NULL, updated_at = $now WHERE id = $id AND owner = $owner`);
|
|
131
|
+
const setProcessing = db.query(`UPDATE files SET processing_status = $status, processing_progress = $progress, updated_at = $now WHERE id = $id`);
|
|
132
|
+
const bumpProcessingAttempts = db.query(`UPDATE files SET processing_attempts = processing_attempts + 1 WHERE id = $id`);
|
|
133
|
+
const maxProcessingAttempts = opts.maxProcessingAttempts ?? 3;
|
|
134
|
+
const setProcessed = db.query(`UPDATE files SET processing_status = $status, processing_progress = 100, has_hls = $hls,
|
|
135
|
+
has_thumbnail = $thumb, has_sprites = $sprites, duration = $duration, updated_at = $now WHERE id = $id`);
|
|
136
|
+
const setMeta = db.query(`UPDATE files SET name = $name, updated_at = $now WHERE id = $id AND owner = $owner`);
|
|
137
|
+
const setThumbnail = db.query(`UPDATE files SET has_thumbnail = $thumb, updated_at = $now WHERE id = $id AND owner = $owner`);
|
|
138
|
+
// replaceContent: new bytes under the SAME id — identity (tags/collections/views) survives,
|
|
139
|
+
// byte-derived columns reset and re-derive.
|
|
140
|
+
const replaceMeta = db.query(`UPDATE files SET name = $name, mime = $mime, size = $size, file_type = $type,
|
|
141
|
+
processing_status = $status, processing_progress = $progress,
|
|
142
|
+
has_hls = 0, has_thumbnail = 0, has_sprites = 0, duration = NULL, updated_at = $now
|
|
143
|
+
WHERE id = $id AND owner = $owner AND is_private = 0`);
|
|
144
|
+
// Private (encrypted) rows: name/tags are sealed in enc_name (sealedMeta); the wrapped AES-128
|
|
145
|
+
// content key is in enc_dek; enc_key_type records the wrap mode. is_private = 1 keeps them out of
|
|
146
|
+
// the public feed. These rows are surfaced to `mountPrivateRoutes` via `asPrivateIndex()`.
|
|
147
|
+
const insertPrivate = db.query(`INSERT INTO files (id, owner, name, mime, size, file_type, is_private, has_hls, duration,
|
|
148
|
+
processing_status, processing_progress, storage_backend, enc_name, enc_dek, enc_key_type, rand_key, created_at, updated_at)
|
|
149
|
+
VALUES ($id, $owner, '', '', $size, $type, 1, 0, NULL, $status, $progress, 'disk', $sealed, $wrapped, $mode, $rand, $now, $now)`);
|
|
150
|
+
const finishPrivate = db.query(`UPDATE files SET processing_status = $status, processing_progress = $progress, has_hls = $hls,
|
|
151
|
+
duration = $duration, size = $size, enc_name = $sealed, enc_dek = $wrapped, updated_at = $now WHERE id = $id`);
|
|
152
|
+
const getPrivateRow = db.query(`SELECT * FROM files WHERE id = $id AND owner = $owner AND is_private = 1`);
|
|
153
|
+
const listPrivateFullRows = db.query(`SELECT * FROM files WHERE owner = $owner AND is_private = 1 ORDER BY created_at DESC, id DESC`);
|
|
154
|
+
const updatePrivateRow = db.query(`UPDATE files SET size = $size, processing_status = $status, enc_name = $sealed, enc_dek = $wrapped,
|
|
155
|
+
enc_key_type = $mode, updated_at = $now WHERE id = $id AND owner = $owner AND is_private = 1`);
|
|
156
|
+
const deletePrivateRow = db.query(`DELETE FROM files WHERE id = $id AND owner = $owner AND is_private = 1`);
|
|
157
|
+
// ── Collection prepared statements ───────────────────────────────────────────────
|
|
158
|
+
const insertCollection = db.query(`INSERT INTO file_collections (id, owner, name, kind, visibility, created_at, updated_at)
|
|
159
|
+
VALUES ($id, $owner, $name, $kind, $visibility, $now, $now)
|
|
160
|
+
RETURNING *`);
|
|
161
|
+
const getCollection = db.query(`SELECT * FROM file_collections WHERE id = $id AND owner = $owner`);
|
|
162
|
+
const getFavoritesCollection = db.query(`SELECT * FROM file_collections WHERE owner = $owner AND kind = 'favorites'`);
|
|
163
|
+
const updateCollectionRow = db.query(`UPDATE file_collections SET name = $name, visibility = $visibility, updated_at = $now
|
|
164
|
+
WHERE id = $id AND owner = $owner`);
|
|
165
|
+
const deleteCollection = db.query(`DELETE FROM file_collections WHERE id = $id AND owner = $owner AND kind != 'favorites'`);
|
|
166
|
+
const deleteCollectionMembers = db.query(`DELETE FROM file_collection_files WHERE collection_id = $id`);
|
|
167
|
+
// Membership manipulation — owner is enforced by joining against files.owner so a tenant can
|
|
168
|
+
// only add their own files.
|
|
169
|
+
const insertMember = db.query(`INSERT OR IGNORE INTO file_collection_files (collection_id, file_id, owner, added_at)
|
|
170
|
+
SELECT $cid, f.id, f.owner, $now FROM files f
|
|
171
|
+
WHERE f.id = $fid AND f.owner = $owner`);
|
|
172
|
+
const deleteMember = db.query(`DELETE FROM file_collection_files
|
|
173
|
+
WHERE collection_id = $cid AND file_id = $fid AND owner = $owner`);
|
|
174
|
+
// Remove all membership rows for a given file (used when the file itself is deleted).
|
|
175
|
+
const deleteFileMemberships = db.query(`DELETE FROM file_collection_files WHERE file_id = $file AND owner = $owner`);
|
|
176
|
+
// View-event ledger: upsert the last-viewed timestamp + bump the count. The SELECT-from-files
|
|
177
|
+
// gate means a view can only be recorded for one of the caller's OWN files (unknown/other-owner
|
|
178
|
+
// ids insert nothing). Powers the `lastViewed` sort.
|
|
179
|
+
const recordViewStmt = db.query(`INSERT INTO file_views (file_id, owner, viewed_at, view_count)
|
|
180
|
+
SELECT f.id, f.owner, $now, 1 FROM files f WHERE f.id = $id AND f.owner = $owner
|
|
181
|
+
ON CONFLICT(file_id) DO UPDATE SET viewed_at = excluded.viewed_at, view_count = view_count + 1`);
|
|
182
|
+
const deleteFileViews = db.query(`DELETE FROM file_views WHERE file_id = $file`);
|
|
183
|
+
const rowToPrivateItem = (r, owner) => ({
|
|
184
|
+
id: r.id,
|
|
185
|
+
owner,
|
|
186
|
+
sizeBytes: r.size,
|
|
187
|
+
createdAt: r.created_at,
|
|
188
|
+
status: r.processing_status,
|
|
189
|
+
keyMode: (r.enc_key_type || 'deniable'),
|
|
190
|
+
wrappedContentKey: r.enc_dek,
|
|
191
|
+
sealedMeta: r.enc_name,
|
|
192
|
+
});
|
|
193
|
+
const tagsFor = (ids) => {
|
|
194
|
+
const map = new Map();
|
|
195
|
+
if (ids.length === 0)
|
|
196
|
+
return map;
|
|
197
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
198
|
+
const rows = db
|
|
199
|
+
.query(`SELECT file_id, tag FROM file_tags WHERE file_id IN (${placeholders}) ORDER BY tag`)
|
|
200
|
+
.all(...ids);
|
|
201
|
+
for (const r of rows) {
|
|
202
|
+
const list = map.get(r.file_id) ?? [];
|
|
203
|
+
list.push(r.tag);
|
|
204
|
+
map.set(r.file_id, list);
|
|
205
|
+
}
|
|
206
|
+
return map;
|
|
207
|
+
};
|
|
208
|
+
/** Batch-fetch collection memberships for a page of file ids. Returns a map of file_id →
|
|
209
|
+
* collection_ids[], and a separate Set of favorited file_ids (by their Favorites collection). */
|
|
210
|
+
const collectionsFor = (owner, fileIds) => {
|
|
211
|
+
const collMap = new Map();
|
|
212
|
+
const favSet = new Set();
|
|
213
|
+
if (fileIds.length === 0)
|
|
214
|
+
return { collMap, favSet };
|
|
215
|
+
const placeholders = fileIds.map(() => '?').join(',');
|
|
216
|
+
const rows = db
|
|
217
|
+
.query(`SELECT fcf.file_id, fcf.collection_id, fc.kind
|
|
218
|
+
FROM file_collection_files fcf
|
|
219
|
+
JOIN file_collections fc ON fc.id = fcf.collection_id
|
|
220
|
+
WHERE fcf.file_id IN (${placeholders}) AND fcf.owner = ?`)
|
|
221
|
+
.all(...fileIds, owner);
|
|
222
|
+
for (const r of rows) {
|
|
223
|
+
const list = collMap.get(r.file_id) ?? [];
|
|
224
|
+
list.push(r.collection_id);
|
|
225
|
+
collMap.set(r.file_id, list);
|
|
226
|
+
if (r.kind === 'favorites')
|
|
227
|
+
favSet.add(r.file_id);
|
|
228
|
+
}
|
|
229
|
+
return { collMap, favSet };
|
|
230
|
+
};
|
|
231
|
+
const rowToRecord = (row, tags, collectionIds, favorite) => ({
|
|
232
|
+
id: row.id,
|
|
233
|
+
name: row.name,
|
|
234
|
+
mime: row.mime,
|
|
235
|
+
size: row.size,
|
|
236
|
+
fileType: row.file_type,
|
|
237
|
+
tags,
|
|
238
|
+
isPrivate: row.is_private === 1,
|
|
239
|
+
hasThumbnail: row.has_thumbnail === 1,
|
|
240
|
+
hasHls: row.has_hls === 1,
|
|
241
|
+
hasSprites: row.has_sprites === 1,
|
|
242
|
+
durationSeconds: row.duration ?? undefined,
|
|
243
|
+
processingStatus: row.processing_status,
|
|
244
|
+
processingProgress: row.processing_progress,
|
|
245
|
+
favorite,
|
|
246
|
+
collectionIds,
|
|
247
|
+
createdAt: row.created_at,
|
|
248
|
+
updatedAt: row.updated_at,
|
|
249
|
+
trashedAt: row.trashed_at ?? undefined,
|
|
250
|
+
});
|
|
251
|
+
const collectionRowToRecord = (row, fileCount) => ({
|
|
252
|
+
id: row.id,
|
|
253
|
+
name: row.name,
|
|
254
|
+
kind: row.kind,
|
|
255
|
+
visibility: row.visibility,
|
|
256
|
+
fileCount,
|
|
257
|
+
createdAt: row.created_at,
|
|
258
|
+
updatedAt: row.updated_at,
|
|
259
|
+
});
|
|
260
|
+
// A background job can outlive this catalogue's Database — a host shutting down mid-transcode,
|
|
261
|
+
// or a test teardown that closed the db after its settle timeout. The job's late progress/result
|
|
262
|
+
// writes must not crash the process (the rows they'd update are gone with the closed db, and
|
|
263
|
+
// recoverProcessing re-kicks anything left 'processing' on the next boot) — drop exactly the
|
|
264
|
+
// finalized/closed-statement failure, surface everything else.
|
|
265
|
+
const lateWrite = (write) => {
|
|
266
|
+
try {
|
|
267
|
+
write();
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
271
|
+
if (/finalized|closed/i.test(message))
|
|
272
|
+
return;
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
// Fire-and-forget the transcode for a public video whose source sits at fsSourceKey; the row's
|
|
277
|
+
// processing columns flip to ready/failed when it finishes. Reused by ingest, registerUploaded,
|
|
278
|
+
// and recoverProcessing (re-kick after a restart lost an in-flight job).
|
|
279
|
+
// Returns a promise that resolves when the derivatives finish, so callers that must NOT run
|
|
280
|
+
// ffmpeg jobs concurrently (backfill on a memory-constrained host) can await + serialize. Normal
|
|
281
|
+
// ingest ignores the return value (fire-and-forget).
|
|
282
|
+
const kickVideoProcessing = (owner, id) => {
|
|
283
|
+
if (opts.transcodeVideos === false) {
|
|
284
|
+
// No derivatives at all: the raw source plays progressively, matching the ffmpeg-unavailable
|
|
285
|
+
// path. The strictest memory guard — not even a poster frame is decoded.
|
|
286
|
+
setProcessed.run({
|
|
287
|
+
$id: id,
|
|
288
|
+
$status: 'ready',
|
|
289
|
+
$hls: 0,
|
|
290
|
+
$thumb: 0,
|
|
291
|
+
$sprites: 0,
|
|
292
|
+
$duration: null,
|
|
293
|
+
$now: now(),
|
|
294
|
+
});
|
|
295
|
+
return Promise.resolve();
|
|
296
|
+
}
|
|
297
|
+
// transcodeVideos:'poster' = the memory-constrained-host sweet spot (2026-07-04 OOM guard). We
|
|
298
|
+
// skip the heavy HLS/re-encode ladder that OOM'd the box, but STILL extract a cheap single-frame
|
|
299
|
+
// poster so the grid keeps thumbnails and the player shows no "optimization failed" badge.
|
|
300
|
+
const posterOnly = opts.transcodeVideos === 'poster';
|
|
301
|
+
bumpProcessingAttempts.run({ $id: id });
|
|
302
|
+
return (opts.videoProcessor ?? processVideo)({
|
|
303
|
+
store,
|
|
304
|
+
workDir,
|
|
305
|
+
owner,
|
|
306
|
+
id,
|
|
307
|
+
sourceKey: fsSourceKey(owner, id),
|
|
308
|
+
ladderHeights: opts.ladderHeights,
|
|
309
|
+
posterOnly,
|
|
310
|
+
onProgress: (status, pct) => lateWrite(() => setProcessing.run({ $id: id, $status: status, $progress: pct, $now: now() })),
|
|
311
|
+
}).then((res) => {
|
|
312
|
+
lateWrite(() => setProcessed.run({
|
|
313
|
+
$id: id,
|
|
314
|
+
$status: res.status,
|
|
315
|
+
$hls: res.hasHls ? 1 : 0,
|
|
316
|
+
$thumb: res.hasPoster ? 1 : 0,
|
|
317
|
+
$sprites: res.hasSprite ? 1 : 0,
|
|
318
|
+
$duration: res.durationSeconds || null,
|
|
319
|
+
$now: now(),
|
|
320
|
+
}));
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
// The local image-derivative implementation: sharp first (fast path), ffmpeg fallback for
|
|
324
|
+
// formats sharp's prebuilt binary can't decode (notably HEIC/HEVC iPhone photos). Browsers
|
|
325
|
+
// can't render HEIC either, so the JPEG poster is what the grid AND any <img> fallback display;
|
|
326
|
+
// non-renderable formats additionally get a full-size display JPEG for the lightbox.
|
|
327
|
+
const localImageDerivatives = async (args) => {
|
|
328
|
+
const bytes = await args.store.get(args.binaryKey);
|
|
329
|
+
if (!bytes)
|
|
330
|
+
return { hasThumbnail: false };
|
|
331
|
+
let thumb = null;
|
|
332
|
+
try {
|
|
333
|
+
thumb = new Uint8Array(await renderImage({
|
|
334
|
+
input: bytes,
|
|
335
|
+
operations: [{ type: 'resize', width: 512 }], // width-only ⇒ aspect preserved
|
|
336
|
+
output: { format: 'jpeg', quality: 78 },
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
thumb = await ffmpegImageThumbnail(bytes, args.workDir, args.id);
|
|
341
|
+
}
|
|
342
|
+
if (!thumb)
|
|
343
|
+
return { hasThumbnail: false }; // both paths failed; leave has_thumbnail 0
|
|
344
|
+
await args.store.put(args.posterKey, thumb, 'image/jpeg');
|
|
345
|
+
// Write the display derivative BEFORE the caller flips has_thumbnail (the photo source only
|
|
346
|
+
// advertises the display URL once the poster flag is set — this ordering keeps it ready).
|
|
347
|
+
if (args.needsDisplay) {
|
|
348
|
+
const display = await ffmpegImageThumbnail(bytes, args.workDir, `${args.id}-display`, 2048);
|
|
349
|
+
if (display)
|
|
350
|
+
await args.store.put(args.displayKey, display, 'image/jpeg');
|
|
351
|
+
}
|
|
352
|
+
return { hasThumbnail: true };
|
|
353
|
+
};
|
|
354
|
+
// Fire-and-forget a grid thumbnail for an image at the shared poster key (the poster serve
|
|
355
|
+
// route is type-agnostic). Best-effort: a failed derivative keeps has_thumbnail 0 and the UI
|
|
356
|
+
// falls back to the full image. Returns a promise so the backfill can serialize image jobs
|
|
357
|
+
// (mirrors kickVideoProcessing); normal ingest ignores it (fire-and-forget).
|
|
358
|
+
const kickImageThumbnail = (owner, id, mime) => (async () => {
|
|
359
|
+
const { hasThumbnail } = await (opts.imageDerivatives ?? localImageDerivatives)({
|
|
360
|
+
store,
|
|
361
|
+
workDir,
|
|
362
|
+
owner,
|
|
363
|
+
id,
|
|
364
|
+
mime,
|
|
365
|
+
binaryKey: fileBinaryKey(owner, id),
|
|
366
|
+
posterKey: fsPosterKey(owner, id),
|
|
367
|
+
displayKey: fsDisplayKey(owner, id),
|
|
368
|
+
needsDisplay: !isBrowserRenderableImage(mime),
|
|
369
|
+
});
|
|
370
|
+
if (!hasThumbnail)
|
|
371
|
+
return;
|
|
372
|
+
setThumbnail.run({ $id: id, $owner: owner, $thumb: 1, $now: now() });
|
|
373
|
+
})().catch(() => { });
|
|
374
|
+
// Insert a public row + kick background processing (video transcode / image thumbnail). Shared
|
|
375
|
+
// by ingest (bytes stored here) and registerUploaded (bytes already at the key, e.g. a chunked
|
|
376
|
+
// upload direct to binary-server).
|
|
377
|
+
const finalizePublicRow = (owner, id, meta, size, ts) => {
|
|
378
|
+
const isVideo = meta.mime.startsWith('video/');
|
|
379
|
+
db.transaction(() => {
|
|
380
|
+
insertFile.run({
|
|
381
|
+
$id: id,
|
|
382
|
+
$owner: owner,
|
|
383
|
+
$name: meta.name,
|
|
384
|
+
$mime: meta.mime,
|
|
385
|
+
$size: size,
|
|
386
|
+
$type: classifyFileType(meta.mime),
|
|
387
|
+
$status: isVideo ? 'processing' : 'ready',
|
|
388
|
+
$progress: isVideo ? 0 : 100,
|
|
389
|
+
$rand: randKey(),
|
|
390
|
+
$now: ts,
|
|
391
|
+
});
|
|
392
|
+
for (const tag of meta.tags ?? [])
|
|
393
|
+
insertTag.run({ $file: id, $tag: tag, $owner: owner });
|
|
394
|
+
})();
|
|
395
|
+
if (isVideo)
|
|
396
|
+
void kickVideoProcessing(owner, id);
|
|
397
|
+
else if (meta.mime.startsWith('image/'))
|
|
398
|
+
void kickImageThumbnail(owner, id, meta.mime);
|
|
399
|
+
// Freshly ingested file has no collections yet.
|
|
400
|
+
return rowToRecord(getRow.get({ $id: id, $owner: owner }), meta.tags ?? [], [], false);
|
|
401
|
+
};
|
|
402
|
+
/** Lazily get-or-create the owner's Favorites collection. */
|
|
403
|
+
const getOrCreateFavorites = (owner, ts) => {
|
|
404
|
+
const existing = getFavoritesCollection.get({ $owner: owner });
|
|
405
|
+
if (existing)
|
|
406
|
+
return existing;
|
|
407
|
+
const id = crypto.randomUUID();
|
|
408
|
+
const rows = insertCollection.all({
|
|
409
|
+
$id: id,
|
|
410
|
+
$owner: owner,
|
|
411
|
+
$name: 'Favorites',
|
|
412
|
+
$kind: 'favorites',
|
|
413
|
+
$visibility: 'normal',
|
|
414
|
+
$now: ts,
|
|
415
|
+
});
|
|
416
|
+
return rows[0];
|
|
417
|
+
};
|
|
418
|
+
const statsByType = db.query(`SELECT file_type, COUNT(*) AS n, SUM(size) AS bytes FROM files
|
|
419
|
+
WHERE owner = $owner AND is_private = 0 GROUP BY file_type`);
|
|
420
|
+
const statsPrivate = db.query(`SELECT COUNT(*) AS n, SUM(size) AS bytes FROM files WHERE owner = $owner AND is_private = 1`);
|
|
421
|
+
const statsStatus = db.query(`SELECT processing_status, COUNT(*) AS n FROM files
|
|
422
|
+
WHERE owner = $owner GROUP BY processing_status`);
|
|
423
|
+
return {
|
|
424
|
+
async stats(ctx) {
|
|
425
|
+
const owner = ownerOf(ctx);
|
|
426
|
+
const byType = {};
|
|
427
|
+
let files = 0;
|
|
428
|
+
let bytes = 0;
|
|
429
|
+
for (const row of statsByType.all({ $owner: owner })) {
|
|
430
|
+
byType[row.file_type] = row.n;
|
|
431
|
+
files += row.n;
|
|
432
|
+
bytes += row.bytes ?? 0;
|
|
433
|
+
}
|
|
434
|
+
const priv = statsPrivate.get({ $owner: owner });
|
|
435
|
+
let processing = 0;
|
|
436
|
+
let failed = 0;
|
|
437
|
+
for (const row of statsStatus.all({ $owner: owner })) {
|
|
438
|
+
if (row.processing_status === 'processing' || row.processing_status === 'pending')
|
|
439
|
+
processing += row.n;
|
|
440
|
+
if (row.processing_status === 'failed')
|
|
441
|
+
failed += row.n;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
files,
|
|
445
|
+
bytes,
|
|
446
|
+
byType,
|
|
447
|
+
privateItems: priv?.n ?? 0,
|
|
448
|
+
privateBytes: priv?.bytes ?? 0,
|
|
449
|
+
processing,
|
|
450
|
+
failed,
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
async list(ctx, options = {}) {
|
|
454
|
+
const owner = ownerOf(ctx);
|
|
455
|
+
const limit = Math.min(options.limit ?? 40, 200);
|
|
456
|
+
const params = { $owner: owner, $limit: limit + 1 };
|
|
457
|
+
const where = [
|
|
458
|
+
'f.owner = $owner',
|
|
459
|
+
'f.is_private = 0',
|
|
460
|
+
options.trashedOnly ? 'f.trashed_at IS NOT NULL' : 'f.trashed_at IS NULL',
|
|
461
|
+
];
|
|
462
|
+
let from = 'files f';
|
|
463
|
+
if (options.type) {
|
|
464
|
+
where.push('f.file_type = $type');
|
|
465
|
+
params.$type = options.type;
|
|
466
|
+
}
|
|
467
|
+
if (options.query) {
|
|
468
|
+
where.push('f.name LIKE $q');
|
|
469
|
+
params.$q = `%${options.query}%`;
|
|
470
|
+
}
|
|
471
|
+
if (options.tag) {
|
|
472
|
+
from = 'files f JOIN file_tags t ON t.file_id = f.id AND t.tag = $tag';
|
|
473
|
+
params.$tag = options.tag;
|
|
474
|
+
}
|
|
475
|
+
if (options.collectionId) {
|
|
476
|
+
// Unlock-gated collection: return empty page unless caller opts in.
|
|
477
|
+
const coll = db
|
|
478
|
+
.query(`SELECT visibility FROM file_collections WHERE id = $id AND owner = $owner`)
|
|
479
|
+
.get({ $id: options.collectionId, $owner: owner });
|
|
480
|
+
if (coll?.visibility === 'unlock' && !options.includeUnlockable) {
|
|
481
|
+
return { items: [] };
|
|
482
|
+
}
|
|
483
|
+
from = `files f JOIN file_collection_files fcf ON fcf.file_id = f.id AND fcf.collection_id = $cid`;
|
|
484
|
+
params.$cid = options.collectionId;
|
|
485
|
+
}
|
|
486
|
+
else if (options.favorite) {
|
|
487
|
+
// Filter to files in the owner's Favorites collection.
|
|
488
|
+
const fav = getFavoritesCollection.get({ $owner: owner });
|
|
489
|
+
if (!fav)
|
|
490
|
+
return { items: [] };
|
|
491
|
+
// Unlock-gated favorites: also respect the gate.
|
|
492
|
+
if (fav.visibility === 'unlock' && !options.includeUnlockable)
|
|
493
|
+
return { items: [] };
|
|
494
|
+
from = `files f JOIN file_collection_files fcf ON fcf.file_id = f.id AND fcf.collection_id = $cid`;
|
|
495
|
+
params.$cid = fav.id;
|
|
496
|
+
}
|
|
497
|
+
const sort = options.sort ?? 'recent';
|
|
498
|
+
const cur = options.cursor ? decodeCursor(options.cursor) : null;
|
|
499
|
+
// `random` mints a shuffle seed on the first page and threads it through the cursor so every
|
|
500
|
+
// page reshuffles identically; `lastViewed` joins the view ledger; the rest are static specs.
|
|
501
|
+
let seed;
|
|
502
|
+
let spec;
|
|
503
|
+
if (sort === 'random') {
|
|
504
|
+
seed = cur?.seed ?? randomSeed();
|
|
505
|
+
spec = randomSortSpec(seed);
|
|
506
|
+
}
|
|
507
|
+
else if (sort === 'lastViewed') {
|
|
508
|
+
spec = LAST_VIEWED_SPEC;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
spec = SORT_SPECS[sort];
|
|
512
|
+
}
|
|
513
|
+
if (spec.join)
|
|
514
|
+
from = `${from} ${spec.join}`;
|
|
515
|
+
if (spec.params)
|
|
516
|
+
Object.assign(params, spec.params);
|
|
517
|
+
if (cur) {
|
|
518
|
+
where.push(spec.keysetWhere);
|
|
519
|
+
params.$curKey = spec.numericKey ? Number(cur.key) : cur.key;
|
|
520
|
+
params.$curId = cur.id;
|
|
521
|
+
}
|
|
522
|
+
const selectExtra = spec.selectExtra ? `, ${spec.selectExtra}` : '';
|
|
523
|
+
const rows = db
|
|
524
|
+
.query(`SELECT f.*${selectExtra} FROM ${from} WHERE ${where.join(' AND ')}
|
|
525
|
+
ORDER BY ${spec.orderBy} LIMIT $limit`)
|
|
526
|
+
.all(params);
|
|
527
|
+
const hasMore = rows.length > limit;
|
|
528
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
529
|
+
const tags = tagsFor(page.map((r) => r.id));
|
|
530
|
+
const { collMap, favSet } = collectionsFor(owner, page.map((r) => r.id));
|
|
531
|
+
const result = {
|
|
532
|
+
items: page.map((r) => rowToRecord(r, tags.get(r.id) ?? [], collMap.get(r.id) ?? [], favSet.has(r.id))),
|
|
533
|
+
};
|
|
534
|
+
if (hasMore) {
|
|
535
|
+
const last = page[page.length - 1];
|
|
536
|
+
result.nextCursor = encodeCursor(spec.key(last), last.id, seed);
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
},
|
|
540
|
+
async get(ctx, id) {
|
|
541
|
+
const owner = ownerOf(ctx);
|
|
542
|
+
const row = getRow.get({ $id: id, $owner: owner });
|
|
543
|
+
if (!row)
|
|
544
|
+
return null;
|
|
545
|
+
const { collMap, favSet } = collectionsFor(owner, [id]);
|
|
546
|
+
return rowToRecord(row, tagsFor([id]).get(id) ?? [], collMap.get(id) ?? [], favSet.has(id));
|
|
547
|
+
},
|
|
548
|
+
async tagFacets(ctx) {
|
|
549
|
+
const rows = db
|
|
550
|
+
.query(`SELECT tag, COUNT(*) as count FROM file_tags WHERE owner = $owner AND is_private = 0
|
|
551
|
+
GROUP BY tag ORDER BY count DESC, tag ASC`)
|
|
552
|
+
.all({ $owner: ownerOf(ctx) });
|
|
553
|
+
return rows;
|
|
554
|
+
},
|
|
555
|
+
async ingest(ctx, upload) {
|
|
556
|
+
const owner = ownerOf(ctx);
|
|
557
|
+
const id = crypto.randomUUID();
|
|
558
|
+
const isVideo = upload.mime.startsWith('video/');
|
|
559
|
+
// Video → stage the source for processing; other → store the primary binary directly.
|
|
560
|
+
const key = isVideo ? fsSourceKey(owner, id) : fileBinaryKey(owner, id);
|
|
561
|
+
await store.putStream(key, upload.body, upload.mime, { contentLength: upload.size });
|
|
562
|
+
const size = (await store.stat(key))?.size ?? upload.size ?? 0;
|
|
563
|
+
return finalizePublicRow(owner, id, upload, size, now());
|
|
564
|
+
},
|
|
565
|
+
async registerUploaded(ctx, upload) {
|
|
566
|
+
// Bytes were uploaded out-of-band (a chunked upload direct to binary-server) and already sit
|
|
567
|
+
// at the catalogue's key (fsSourceKey for video, fileBinaryKey otherwise). Just create the
|
|
568
|
+
// row + kick processing — no re-store. The caller owns having placed the bytes correctly.
|
|
569
|
+
return finalizePublicRow(ownerOf(ctx), upload.id, upload, upload.size, now());
|
|
570
|
+
},
|
|
571
|
+
async update(ctx, id, patch) {
|
|
572
|
+
const owner = ownerOf(ctx);
|
|
573
|
+
const row = getRow.get({ $id: id, $owner: owner });
|
|
574
|
+
if (!row)
|
|
575
|
+
return null;
|
|
576
|
+
db.transaction(() => {
|
|
577
|
+
if (patch.name !== undefined)
|
|
578
|
+
setMeta.run({ $id: id, $owner: owner, $name: patch.name, $now: now() });
|
|
579
|
+
if (patch.tags !== undefined) {
|
|
580
|
+
deleteTags.run({ $file: id });
|
|
581
|
+
for (const tag of patch.tags)
|
|
582
|
+
insertTag.run({ $file: id, $tag: tag, $owner: owner });
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
const updated = getRow.get({ $id: id, $owner: owner });
|
|
586
|
+
const { collMap, favSet } = collectionsFor(owner, [id]);
|
|
587
|
+
return rowToRecord(updated, tagsFor([id]).get(id) ?? [], collMap.get(id) ?? [], favSet.has(id));
|
|
588
|
+
},
|
|
589
|
+
async replaceContent(ctx, id, upload) {
|
|
590
|
+
const owner = ownerOf(ctx);
|
|
591
|
+
const row = getRow.get({ $id: id, $owner: owner });
|
|
592
|
+
if (!row)
|
|
593
|
+
return null;
|
|
594
|
+
if (row.is_private === 1) {
|
|
595
|
+
throw new Error('replaceContent: private files cannot be replaced in place');
|
|
596
|
+
}
|
|
597
|
+
const isVideo = upload.mime.startsWith('video/');
|
|
598
|
+
// Store the new bytes FIRST — a failed write leaves the old row + bytes fully intact.
|
|
599
|
+
const key = isVideo ? fsSourceKey(owner, id) : fileBinaryKey(owner, id);
|
|
600
|
+
await store.putStream(key, upload.body, upload.mime, { contentLength: upload.size });
|
|
601
|
+
const size = (await store.stat(key))?.size ?? upload.size ?? 0;
|
|
602
|
+
// Wipe stale derivatives + the other-kind primary (all best-effort/idempotent), keeping the
|
|
603
|
+
// key the new bytes just landed at.
|
|
604
|
+
await Promise.all([
|
|
605
|
+
isVideo ? store.remove(fileBinaryKey(owner, id)) : store.remove(fsSourceKey(owner, id)),
|
|
606
|
+
store.remove(fsMp4Key(owner, id)),
|
|
607
|
+
store.remove(fsPosterKey(owner, id)),
|
|
608
|
+
store.remove(fsSpriteKey(owner, id)),
|
|
609
|
+
store.remove(fsVttKey(owner, id)),
|
|
610
|
+
store.removePrefix(fsHlsPrefix(owner, id)),
|
|
611
|
+
]);
|
|
612
|
+
replaceMeta.run({
|
|
613
|
+
$id: id,
|
|
614
|
+
$owner: owner,
|
|
615
|
+
$name: upload.name ?? row.name,
|
|
616
|
+
$mime: upload.mime,
|
|
617
|
+
$size: size,
|
|
618
|
+
$type: classifyFileType(upload.mime),
|
|
619
|
+
$status: isVideo ? 'processing' : 'ready',
|
|
620
|
+
$progress: isVideo ? 0 : 100,
|
|
621
|
+
$now: now(),
|
|
622
|
+
});
|
|
623
|
+
if (isVideo)
|
|
624
|
+
void kickVideoProcessing(owner, id);
|
|
625
|
+
else if (upload.mime.startsWith('image/'))
|
|
626
|
+
void kickImageThumbnail(owner, id, upload.mime);
|
|
627
|
+
const updated = getRow.get({ $id: id, $owner: owner });
|
|
628
|
+
const { collMap, favSet } = collectionsFor(owner, [id]);
|
|
629
|
+
return rowToRecord(updated, tagsFor([id]).get(id) ?? [], collMap.get(id) ?? [], favSet.has(id));
|
|
630
|
+
},
|
|
631
|
+
async remove(ctx, id) {
|
|
632
|
+
const owner = ownerOf(ctx);
|
|
633
|
+
// Remove every key this id can have — removes are idempotent, and images have a
|
|
634
|
+
// poster too (kickImageThumbnail writes fsPosterKey), so no video-only branching:
|
|
635
|
+
// branching here is what used to orphan image thumbnails on disk.
|
|
636
|
+
await Promise.all([
|
|
637
|
+
store.remove(fileBinaryKey(owner, id)),
|
|
638
|
+
store.remove(fsSourceKey(owner, id)),
|
|
639
|
+
store.remove(fsMp4Key(owner, id)),
|
|
640
|
+
store.remove(fsPosterKey(owner, id)),
|
|
641
|
+
store.remove(fsSpriteKey(owner, id)),
|
|
642
|
+
store.remove(fsVttKey(owner, id)),
|
|
643
|
+
store.removePrefix(fsHlsPrefix(owner, id)),
|
|
644
|
+
]);
|
|
645
|
+
db.transaction(() => {
|
|
646
|
+
deleteTags.run({ $file: id });
|
|
647
|
+
deleteFileMemberships.run({ $file: id, $owner: owner });
|
|
648
|
+
deleteFileViews.run({ $file: id });
|
|
649
|
+
deleteFile.run({ $id: id, $owner: owner });
|
|
650
|
+
})();
|
|
651
|
+
},
|
|
652
|
+
async trash(ctx, id) {
|
|
653
|
+
trashFile.run({ $id: id, $owner: ownerOf(ctx), $now: now() });
|
|
654
|
+
},
|
|
655
|
+
async restore(ctx, id) {
|
|
656
|
+
restoreFile.run({ $id: id, $owner: ownerOf(ctx), $now: now() });
|
|
657
|
+
},
|
|
658
|
+
async importFromPath(ctx, absPath, importOpts) {
|
|
659
|
+
const owner = ownerOf(ctx);
|
|
660
|
+
const extraTags = importOpts?.tags ?? [];
|
|
661
|
+
const imported = [];
|
|
662
|
+
const skipped = [];
|
|
663
|
+
const importOne = async (filePath, folderTag) => {
|
|
664
|
+
let st;
|
|
665
|
+
try {
|
|
666
|
+
st = await fsStat(filePath);
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
skipped.push({ path: filePath, reason: err instanceof Error ? err.message : String(err) });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (!st.isFile())
|
|
673
|
+
return; // symlinks/devices/etc. — directories are handled by the caller
|
|
674
|
+
const name = path.basename(filePath);
|
|
675
|
+
const mime = mimeByExt(name);
|
|
676
|
+
const tags = folderTag ? [...extraTags, folderTag] : extraTags;
|
|
677
|
+
try {
|
|
678
|
+
const id = crypto.randomUUID();
|
|
679
|
+
const isVideo = mime.startsWith('video/');
|
|
680
|
+
const key = isVideo ? fsSourceKey(owner, id) : fileBinaryKey(owner, id);
|
|
681
|
+
await store.putStream(key, Bun.file(filePath).stream(), mime, {
|
|
682
|
+
contentLength: st.size,
|
|
683
|
+
});
|
|
684
|
+
const size = (await store.stat(key))?.size ?? st.size;
|
|
685
|
+
imported.push(finalizePublicRow(owner, id, { name, mime, tags }, size, now()));
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
skipped.push({ path: filePath, reason: err instanceof Error ? err.message : String(err) });
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
let rootStat;
|
|
692
|
+
try {
|
|
693
|
+
rootStat = await fsStat(absPath);
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
return {
|
|
697
|
+
imported: [],
|
|
698
|
+
skipped: [{ path: absPath, reason: err instanceof Error ? err.message : String(err) }],
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (rootStat.isFile()) {
|
|
702
|
+
await importOne(absPath);
|
|
703
|
+
return { imported, skipped };
|
|
704
|
+
}
|
|
705
|
+
// Directory: recurse, tagging each file with its immediate containing folder name so the
|
|
706
|
+
// origin structure survives as a (flat) tag facet rather than a tree the catalogue doesn't
|
|
707
|
+
// model. The top-level directory itself is NOT used as a tag (it's just the import root).
|
|
708
|
+
const walk = async (dir, isRoot) => {
|
|
709
|
+
let entries;
|
|
710
|
+
try {
|
|
711
|
+
entries = await readdir(dir, { withFileTypes: true, encoding: 'utf8' });
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
skipped.push({ path: dir, reason: err instanceof Error ? err.message : String(err) });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const folderTag = isRoot ? undefined : path.basename(dir);
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
const entryPath = path.join(dir, entry.name);
|
|
720
|
+
if (entry.isDirectory())
|
|
721
|
+
await walk(entryPath, false);
|
|
722
|
+
else if (entry.isFile())
|
|
723
|
+
await importOne(entryPath, folderTag);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
await walk(absPath, true);
|
|
727
|
+
return { imported, skipped };
|
|
728
|
+
},
|
|
729
|
+
recoverProcessing() {
|
|
730
|
+
// A restart mid-transcode leaves a row stuck in `processing`/`pending` — the in-process
|
|
731
|
+
// job is gone. For PUBLIC videos the source still sits at fsSourceKey, so re-kick the pipeline.
|
|
732
|
+
// PRIVATE rows can't be re-kicked at boot (the set key isn't available) — video or blob alike,
|
|
733
|
+
// mark them failed; the user re-unlocks + re-uploads. Public non-video rows are `ready` at
|
|
734
|
+
// insert and never appear here. (Ported from ca's recoverOnBoot; our jobs are in-process,
|
|
735
|
+
// so recovery is app-invoked at startup rather than a persistent queue.)
|
|
736
|
+
// Re-kicks are CAPPED at maxProcessingAttempts total kicks: if the transcode itself is what
|
|
737
|
+
// keeps killing the process (2026-07-04 prod outage: ffmpeg OOM on a 1GB host → systemd
|
|
738
|
+
// restart → unconditional re-kick → OOM again, forever), the row must land on `failed`
|
|
739
|
+
// instead of holding the whole service down.
|
|
740
|
+
const stuck = db
|
|
741
|
+
.query(`SELECT id, owner, is_private, processing_attempts FROM files
|
|
742
|
+
WHERE (file_type = 'video' OR is_private = 1)
|
|
743
|
+
AND processing_status IN ('pending', 'processing')`)
|
|
744
|
+
.all();
|
|
745
|
+
let requeued = 0;
|
|
746
|
+
let failed = 0;
|
|
747
|
+
for (const r of stuck) {
|
|
748
|
+
if (r.is_private === 1 || r.processing_attempts >= maxProcessingAttempts) {
|
|
749
|
+
setProcessing.run({ $id: r.id, $status: 'failed', $progress: 0, $now: now() });
|
|
750
|
+
failed++;
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
void kickVideoProcessing(r.owner, r.id);
|
|
754
|
+
requeued++;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return { requeued, failed };
|
|
758
|
+
},
|
|
759
|
+
// Backfill grid thumbnails for PUBLIC video rows that finished (ready/failed) without a poster —
|
|
760
|
+
// e.g. rows that landed while transcodeVideos was off entirely, or that failed a full transcode
|
|
761
|
+
// before the poster step. Re-kicks the (now poster-capable) pipeline; the attempts counter is
|
|
762
|
+
// reset first so the maxProcessingAttempts cap doesn't pre-empt a fresh, cheaper poster-only run.
|
|
763
|
+
backfillVideoPosters() {
|
|
764
|
+
const missing = db
|
|
765
|
+
.query(`SELECT id, owner FROM files
|
|
766
|
+
WHERE file_type = 'video' AND is_private = 0 AND has_thumbnail = 0
|
|
767
|
+
AND processing_status IN ('ready', 'failed')`)
|
|
768
|
+
.all();
|
|
769
|
+
// Serialize: on the memory-constrained hosts this exists for, running every poster extract
|
|
770
|
+
// at once could spike RAM. Chain them so at most one ffmpeg job is live at a time.
|
|
771
|
+
void missing.reduce(async (prev, r) => {
|
|
772
|
+
await prev;
|
|
773
|
+
db.query(`UPDATE files SET processing_attempts = 0 WHERE id = $id`).run({ $id: r.id });
|
|
774
|
+
await kickVideoProcessing(r.owner, r.id).catch(() => { });
|
|
775
|
+
}, Promise.resolve());
|
|
776
|
+
return { requeued: missing.length };
|
|
777
|
+
},
|
|
778
|
+
// Backfill grid thumbnails for PUBLIC image rows with no poster — e.g. HEIC/HEVC iPhone photos
|
|
779
|
+
// whose sharp thumbnail silently failed before the ffmpeg fallback existed. Re-kicks the (now
|
|
780
|
+
// HEIC-capable) image pipeline, serialized so at most one ffmpeg decode runs at a time.
|
|
781
|
+
backfillImageThumbnails() {
|
|
782
|
+
// Candidates: images with no poster (has_thumbnail = 0), plus every browser-incompatible
|
|
783
|
+
// image (HEIC) — the latter may have a poster from before the display derivative existed but
|
|
784
|
+
// still lack the full-size display JPEG. The loop skips any HEIC whose display is already
|
|
785
|
+
// present, so a warm boot re-runs nothing.
|
|
786
|
+
const candidates = db
|
|
787
|
+
.query(`SELECT id, owner, mime, has_thumbnail FROM files
|
|
788
|
+
WHERE file_type = 'image' AND is_private = 0
|
|
789
|
+
AND (has_thumbnail = 0 OR mime IN ('image/heic', 'image/heif'))`)
|
|
790
|
+
.all();
|
|
791
|
+
void candidates.reduce(async (prev, r) => {
|
|
792
|
+
await prev;
|
|
793
|
+
if (r.has_thumbnail === 1 &&
|
|
794
|
+
!isBrowserRenderableImage(r.mime) &&
|
|
795
|
+
(await store.stat(fsDisplayKey(r.owner, r.id))))
|
|
796
|
+
return; // already fully derived — nothing to do
|
|
797
|
+
await kickImageThumbnail(r.owner, r.id, r.mime).catch(() => { });
|
|
798
|
+
}, Promise.resolve());
|
|
799
|
+
return { requeued: candidates.length };
|
|
800
|
+
},
|
|
801
|
+
// ── Collections ──────────────────────────────────────────────────────────────
|
|
802
|
+
async listCollections(ctx, opts2 = {}) {
|
|
803
|
+
const owner = ownerOf(ctx);
|
|
804
|
+
const visFilter = opts2.includeUnlockable ? '' : `AND fc.visibility != 'unlock'`;
|
|
805
|
+
const rows = db
|
|
806
|
+
.query(`SELECT fc.*, COUNT(fcf.file_id) as file_count
|
|
807
|
+
FROM file_collections fc
|
|
808
|
+
LEFT JOIN file_collection_files fcf ON fcf.collection_id = fc.id
|
|
809
|
+
WHERE fc.owner = $owner ${visFilter}
|
|
810
|
+
GROUP BY fc.id
|
|
811
|
+
ORDER BY fc.kind DESC, fc.name ASC`)
|
|
812
|
+
.all({ $owner: owner });
|
|
813
|
+
return rows.map((r) => collectionRowToRecord(r, r.file_count));
|
|
814
|
+
},
|
|
815
|
+
async createCollection(ctx, input) {
|
|
816
|
+
const owner = ownerOf(ctx);
|
|
817
|
+
const name = input.name.trim();
|
|
818
|
+
if (!name)
|
|
819
|
+
throw new Error('Collection name must not be empty');
|
|
820
|
+
const id = crypto.randomUUID();
|
|
821
|
+
const rows = insertCollection.all({
|
|
822
|
+
$id: id,
|
|
823
|
+
$owner: owner,
|
|
824
|
+
$name: name,
|
|
825
|
+
$kind: 'custom',
|
|
826
|
+
$visibility: input.visibility ?? 'normal',
|
|
827
|
+
$now: now(),
|
|
828
|
+
});
|
|
829
|
+
return collectionRowToRecord(rows[0], 0);
|
|
830
|
+
},
|
|
831
|
+
async updateCollection(ctx, id, patch) {
|
|
832
|
+
const owner = ownerOf(ctx);
|
|
833
|
+
const existing = getCollection.get({ $id: id, $owner: owner });
|
|
834
|
+
if (!existing)
|
|
835
|
+
return null;
|
|
836
|
+
const newName = patch.name !== undefined ? patch.name.trim() : existing.name;
|
|
837
|
+
if (patch.name !== undefined && !newName)
|
|
838
|
+
throw new Error('Collection name must not be empty');
|
|
839
|
+
updateCollectionRow.run({
|
|
840
|
+
$id: id,
|
|
841
|
+
$owner: owner,
|
|
842
|
+
$name: newName,
|
|
843
|
+
$visibility: patch.visibility ?? existing.visibility,
|
|
844
|
+
$now: now(),
|
|
845
|
+
});
|
|
846
|
+
const updated = getCollection.get({ $id: id, $owner: owner });
|
|
847
|
+
const count = db
|
|
848
|
+
.query(`SELECT COUNT(*) as c FROM file_collection_files WHERE collection_id = $id`)
|
|
849
|
+
.get({ $id: id })?.c ?? 0;
|
|
850
|
+
return collectionRowToRecord(updated, count);
|
|
851
|
+
},
|
|
852
|
+
async removeCollection(ctx, id) {
|
|
853
|
+
const owner = ownerOf(ctx);
|
|
854
|
+
const existing = getCollection.get({ $id: id, $owner: owner });
|
|
855
|
+
if (!existing)
|
|
856
|
+
return;
|
|
857
|
+
if (existing.kind === 'favorites')
|
|
858
|
+
throw new Error('The Favorites collection cannot be removed');
|
|
859
|
+
db.transaction(() => {
|
|
860
|
+
deleteCollectionMembers.run({ $id: id });
|
|
861
|
+
deleteCollection.run({ $id: id, $owner: owner });
|
|
862
|
+
})();
|
|
863
|
+
},
|
|
864
|
+
async addToCollection(ctx, collectionId, fileIds) {
|
|
865
|
+
const owner = ownerOf(ctx);
|
|
866
|
+
if (fileIds.length === 0)
|
|
867
|
+
return;
|
|
868
|
+
const ts = now();
|
|
869
|
+
db.transaction(() => {
|
|
870
|
+
for (const fid of fileIds) {
|
|
871
|
+
insertMember.run({ $cid: collectionId, $fid: fid, $owner: owner, $now: ts });
|
|
872
|
+
}
|
|
873
|
+
})();
|
|
874
|
+
},
|
|
875
|
+
async removeFromCollection(ctx, collectionId, fileIds) {
|
|
876
|
+
const owner = ownerOf(ctx);
|
|
877
|
+
if (fileIds.length === 0)
|
|
878
|
+
return;
|
|
879
|
+
db.transaction(() => {
|
|
880
|
+
for (const fid of fileIds) {
|
|
881
|
+
deleteMember.run({ $cid: collectionId, $fid: fid, $owner: owner });
|
|
882
|
+
}
|
|
883
|
+
})();
|
|
884
|
+
},
|
|
885
|
+
async setFavorite(ctx, fileId, favorite) {
|
|
886
|
+
const owner = ownerOf(ctx);
|
|
887
|
+
const ts = now();
|
|
888
|
+
const fav = getOrCreateFavorites(owner, ts);
|
|
889
|
+
if (favorite) {
|
|
890
|
+
insertMember.run({ $cid: fav.id, $fid: fileId, $owner: owner, $now: ts });
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
deleteMember.run({ $cid: fav.id, $fid: fileId, $owner: owner });
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
async recordView(ctx, fileId) {
|
|
897
|
+
// Upsert-gated to the caller's own files; unknown/other-owner ids record nothing.
|
|
898
|
+
recordViewStmt.run({ $id: fileId, $owner: ownerOf(ctx), $now: now() });
|
|
899
|
+
},
|
|
900
|
+
async ingestPrivate(ctx, upload, cfg) {
|
|
901
|
+
const owner = ownerOf(ctx);
|
|
902
|
+
const id = crypto.randomUUID();
|
|
903
|
+
const ts = now();
|
|
904
|
+
// Seal the descriptor NOW — sealing is independent of the transcode (only the wrapped content
|
|
905
|
+
// KEY needs the pipeline). Storing enc_name at insert means a try-a-password reveal matches the
|
|
906
|
+
// item immediately, before its HLS finishes encrypting (no seal race).
|
|
907
|
+
const descriptor = {
|
|
908
|
+
name: upload.name,
|
|
909
|
+
mime: upload.mime,
|
|
910
|
+
tags: upload.tags ?? [],
|
|
911
|
+
};
|
|
912
|
+
const sealed = sealMeta(cfg.setKey, descriptor);
|
|
913
|
+
const isVideo = upload.mime.startsWith('video/');
|
|
914
|
+
insertPrivate.run({
|
|
915
|
+
$id: id,
|
|
916
|
+
$owner: owner,
|
|
917
|
+
$size: 0,
|
|
918
|
+
$type: classifyFileType(upload.mime),
|
|
919
|
+
$mode: cfg.keyMode,
|
|
920
|
+
$status: 'processing',
|
|
921
|
+
$progress: 0,
|
|
922
|
+
$sealed: sealed,
|
|
923
|
+
$wrapped: '',
|
|
924
|
+
$rand: randKey(),
|
|
925
|
+
$now: ts,
|
|
926
|
+
});
|
|
927
|
+
// Stage plaintext to a scratch file, then encrypt: video → AES-128 HLS ladder; anything
|
|
928
|
+
// else (photo/doc/audio) → one AES-128-CTR blob at privateSourceKey. Plaintext is never
|
|
929
|
+
// persisted either way. We MUST await: the wrapped key is produced by the pipeline and
|
|
930
|
+
// there is nothing to serve without it.
|
|
931
|
+
const srcPath = path.join(workDir, 'ingest', `${owner}-${id}.src`);
|
|
932
|
+
let size = 0;
|
|
933
|
+
try {
|
|
934
|
+
size = await streamToFile(upload.body, srcPath);
|
|
935
|
+
const res = isVideo
|
|
936
|
+
? await processPrivateVideo({
|
|
937
|
+
store,
|
|
938
|
+
workDir,
|
|
939
|
+
owner,
|
|
940
|
+
id,
|
|
941
|
+
sourcePath: srcPath,
|
|
942
|
+
setKey: cfg.setKey,
|
|
943
|
+
ladderHeights: opts.ladderHeights,
|
|
944
|
+
onProgress: (status, pct) => setProcessing.run({ $id: id, $status: status, $progress: pct, $now: now() }),
|
|
945
|
+
})
|
|
946
|
+
: {
|
|
947
|
+
...(await processPrivateBlob({
|
|
948
|
+
store,
|
|
949
|
+
owner,
|
|
950
|
+
id,
|
|
951
|
+
sourcePath: srcPath,
|
|
952
|
+
setKey: cfg.setKey,
|
|
953
|
+
})),
|
|
954
|
+
durationSeconds: 0,
|
|
955
|
+
hasHls: false,
|
|
956
|
+
};
|
|
957
|
+
finishPrivate.run({
|
|
958
|
+
$id: id,
|
|
959
|
+
$status: res.status,
|
|
960
|
+
$progress: res.status === 'ready' ? 100 : 0,
|
|
961
|
+
$hls: res.hasHls ? 1 : 0,
|
|
962
|
+
$duration: res.durationSeconds || null,
|
|
963
|
+
$size: size,
|
|
964
|
+
$sealed: sealed,
|
|
965
|
+
$wrapped: res.wrappedContentKey ?? '',
|
|
966
|
+
$now: now(),
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
finally {
|
|
970
|
+
await rm(srcPath, { force: true }).catch(() => { });
|
|
971
|
+
}
|
|
972
|
+
const row = getRow.get({ $id: id, $owner: owner });
|
|
973
|
+
return {
|
|
974
|
+
id,
|
|
975
|
+
size,
|
|
976
|
+
createdAt: ts,
|
|
977
|
+
processingStatus: row.processing_status,
|
|
978
|
+
processingProgress: row.processing_progress,
|
|
979
|
+
hasHls: row.has_hls === 1,
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
asPrivateIndex() {
|
|
983
|
+
// A PrivateIndex view over the catalogue's private rows — the seam mountPrivateRoutes reads.
|
|
984
|
+
// `owner` here is the private owner axis the routes pass (a userId), matching how ingestPrivate
|
|
985
|
+
// stored the row (both go through the files.owner column).
|
|
986
|
+
return {
|
|
987
|
+
async add(item) {
|
|
988
|
+
insertPrivate.run({
|
|
989
|
+
$id: item.id,
|
|
990
|
+
$owner: item.owner,
|
|
991
|
+
$size: item.sizeBytes,
|
|
992
|
+
// External index writers predate blob items and only ever added videos.
|
|
993
|
+
$type: 'video',
|
|
994
|
+
$mode: item.keyMode,
|
|
995
|
+
$status: item.status,
|
|
996
|
+
$progress: item.status === 'ready' ? 100 : 0,
|
|
997
|
+
$sealed: item.sealedMeta,
|
|
998
|
+
$wrapped: item.wrappedContentKey,
|
|
999
|
+
$rand: randKey(),
|
|
1000
|
+
$now: now(),
|
|
1001
|
+
});
|
|
1002
|
+
},
|
|
1003
|
+
async get(owner, id) {
|
|
1004
|
+
const row = getPrivateRow.get({ $id: id, $owner: owner });
|
|
1005
|
+
return row ? rowToPrivateItem(row, owner) : null;
|
|
1006
|
+
},
|
|
1007
|
+
async list(owner) {
|
|
1008
|
+
return listPrivateFullRows.all({ $owner: owner }).map((r) => rowToPrivateItem(r, owner));
|
|
1009
|
+
},
|
|
1010
|
+
async update(owner, id, patch) {
|
|
1011
|
+
const cur = getPrivateRow.get({ $id: id, $owner: owner });
|
|
1012
|
+
if (!cur)
|
|
1013
|
+
return;
|
|
1014
|
+
const merged = { ...rowToPrivateItem(cur, owner), ...patch };
|
|
1015
|
+
updatePrivateRow.run({
|
|
1016
|
+
$id: id,
|
|
1017
|
+
$owner: owner,
|
|
1018
|
+
$size: merged.sizeBytes,
|
|
1019
|
+
$status: merged.status,
|
|
1020
|
+
$sealed: merged.sealedMeta,
|
|
1021
|
+
$wrapped: merged.wrappedContentKey,
|
|
1022
|
+
$mode: merged.keyMode,
|
|
1023
|
+
$now: now(),
|
|
1024
|
+
});
|
|
1025
|
+
},
|
|
1026
|
+
async remove(owner, id) {
|
|
1027
|
+
await store.removePrefix(privateFsKey(owner, id));
|
|
1028
|
+
deletePrivateRow.run({ $id: id, $owner: owner });
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
},
|
|
1032
|
+
};
|
|
1033
|
+
}
|