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,284 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type { Context, Hono } from 'hono';
|
|
3
|
+
import { runMigrations } from '../../migration/runner';
|
|
4
|
+
import { mountPrivateRoutes } from '../../private-media/mountPrivateRoutes';
|
|
5
|
+
import {
|
|
6
|
+
createUnlockService,
|
|
7
|
+
type UnlockPasswordPolicy,
|
|
8
|
+
type UnlockService,
|
|
9
|
+
} from '../../private-media/unlock';
|
|
10
|
+
import { mountMediaRoutes } from '../mountMediaRoutes';
|
|
11
|
+
import { createMediaUrls, type MediaUrls, SourceRegistry } from '../sources';
|
|
12
|
+
import { createStreamTokenService, type StreamTokenService } from '../streamToken';
|
|
13
|
+
import type { MediaCtx, MediaGallerySource, MediaSource, MediaStore, PhotoSource } from '../types';
|
|
14
|
+
import {
|
|
15
|
+
createCatalogueMediaSource,
|
|
16
|
+
createCataloguePhotoSource,
|
|
17
|
+
createCatalogueVideoSource,
|
|
18
|
+
} from './adapter';
|
|
19
|
+
import { createSqliteFileCatalogue, type SqliteFileCatalogueOptions } from './catalogue';
|
|
20
|
+
import {
|
|
21
|
+
type ChunkedUploadService,
|
|
22
|
+
createChunkedUploadService,
|
|
23
|
+
type MintChunkUpload,
|
|
24
|
+
} from './chunkedUpload';
|
|
25
|
+
import { createFileUrls, type FileUrls, mountFileRoutes } from './fileRoutes';
|
|
26
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
27
|
+
import { createStorageOpsLedger } from './storageOps';
|
|
28
|
+
import type { FileCatalogue, PrivateFileCatalogue } from './types';
|
|
29
|
+
|
|
30
|
+
// ── Files stack — the host recipe ───────────────────────────────────────────────
|
|
31
|
+
// One call that assembles the whole files/media server stack a host would otherwise hand-wire:
|
|
32
|
+
// migrations + SQLite catalogue over a MediaStore, the stream-token service, the media/file URL
|
|
33
|
+
// builders, the catalogue-as-video-source registered in a SourceRegistry, the optional private
|
|
34
|
+
// (encrypted) layer, and a `mount()` that wires every byte/upload route onto a Hono app. The auth
|
|
35
|
+
// seams (session → MediaCtx / {userId, userSalt}) stay host-injected — the stack owns no auth.
|
|
36
|
+
|
|
37
|
+
export interface FilesStackOptions {
|
|
38
|
+
/** The catalogue database. The stack runs FILE_CATALOGUE_MIGRATIONS on it itself. */
|
|
39
|
+
db: Database;
|
|
40
|
+
/** Byte backend (disk / binary-server / s3) — the swappable piece. */
|
|
41
|
+
store: MediaStore;
|
|
42
|
+
/** ffmpeg scratch dir for video processing. */
|
|
43
|
+
workDir: string;
|
|
44
|
+
/** Secret for the stream-token service (path-embedded byte-route auth). */
|
|
45
|
+
streamSecret: string;
|
|
46
|
+
/** Owner keyspace segment. Defaults to the userId (files + private are personal) — note the
|
|
47
|
+
* bare primitives default to tenantId; the stack picks the personal axis so every URL/route
|
|
48
|
+
* agrees with the private layer's per-user owner. */
|
|
49
|
+
owner?: (ctx: MediaCtx) => string;
|
|
50
|
+
/** Registered video-source id (also the media-token sourceId). Default 'files'. */
|
|
51
|
+
sourceId?: string;
|
|
52
|
+
/** Video source display title. Default 'Files'. */
|
|
53
|
+
sourceTitle?: string;
|
|
54
|
+
/** Registered photo-source id (the image-filtered view; also its media-token sourceId).
|
|
55
|
+
* Default 'photos'. */
|
|
56
|
+
photoSourceId?: string;
|
|
57
|
+
/** Photo source display title. Default 'Photos'. */
|
|
58
|
+
photoSourceTitle?: string;
|
|
59
|
+
/** Video byte-route mount + URL base (path-only). Default '/_media'. */
|
|
60
|
+
mediaBasePath?: string;
|
|
61
|
+
/** General file byte/upload route mount + URL base (path-only). Default '/_files'. */
|
|
62
|
+
filesBasePath?: string;
|
|
63
|
+
/** PUBLIC base for browser media URLs when it differs from the mount path — a split-origin
|
|
64
|
+
* deploy (SPA on the apex, API on a subdomain) passes the absolute origin+path here
|
|
65
|
+
* ('https://api.example.com/_media') while `mediaBasePath` stays the path-only mount. */
|
|
66
|
+
mediaPublicBase?: string;
|
|
67
|
+
/** PUBLIC base for browser file URLs (split-origin counterpart of `filesBasePath`). */
|
|
68
|
+
filesPublicBase?: string;
|
|
69
|
+
/** Private (encrypted) route mount. Default '/_private'. */
|
|
70
|
+
privateBasePath?: string;
|
|
71
|
+
/** HLS ladder heights for video ingest (createSqliteFileCatalogue passthrough). */
|
|
72
|
+
ladderHeights?: SqliteFileCatalogueOptions['ladderHeights'];
|
|
73
|
+
/** Cap on transcode kicks per file before it's marked failed (createSqliteFileCatalogue
|
|
74
|
+
* passthrough) — the boot-loop guard. Default 3. */
|
|
75
|
+
maxProcessingAttempts?: SqliteFileCatalogueOptions['maxProcessingAttempts'];
|
|
76
|
+
/** Transcode uploaded videos (createSqliteFileCatalogue passthrough). `false` ⇒ videos play
|
|
77
|
+
* raw, no ffmpeg — for memory-constrained hosts. Default true. */
|
|
78
|
+
transcodeVideos?: SqliteFileCatalogueOptions['transcodeVideos'];
|
|
79
|
+
/** Video ingest→derivatives implementation (createSqliteFileCatalogue passthrough) — inject
|
|
80
|
+
* a remote one (binary-server) when the byte store is remote. Default: local ffmpeg. */
|
|
81
|
+
videoProcessor?: SqliteFileCatalogueOptions['videoProcessor'];
|
|
82
|
+
/** Image thumbnail/display derivative implementation (createSqliteFileCatalogue passthrough).
|
|
83
|
+
* Default: local sharp with ffmpeg fallback. */
|
|
84
|
+
imageDerivatives?: SqliteFileCatalogueOptions['imageDerivatives'];
|
|
85
|
+
/** Enables the private (encrypted) stack: unlock service + /_private routes + private uploads.
|
|
86
|
+
* Omit ⇒ `unlock` is null and mount() wires only the public routes. `passwordPolicy` sets what
|
|
87
|
+
* a supplied password MEANS (derive = deniable sets, the default; gate = verify against the
|
|
88
|
+
* app's accounts + recoverable server-key sets — see UnlockPasswordPolicy). */
|
|
89
|
+
privateMedia?: { serverSecret: string; unlockSecret: string; passwordPolicy?: UnlockPasswordPolicy };
|
|
90
|
+
/** Re-kick transcodes a restart left stuck (catalogue.recoverProcessing) at build time.
|
|
91
|
+
* Default true. */
|
|
92
|
+
recoverProcessing?: boolean;
|
|
93
|
+
/** Backfill grid thumbnails at build time for finished PUBLIC rows missing a poster — videos
|
|
94
|
+
* (landed while transcodeVideos was off / failed before the poster step) AND images (e.g. HEIC
|
|
95
|
+
* whose sharp thumbnail failed before the ffmpeg fallback). Default false. */
|
|
96
|
+
backfillPosters?: boolean;
|
|
97
|
+
/** Wire LARGE-file chunked uploads (direct-to-backend). Give the backend-specific per-chunk URL
|
|
98
|
+
* minter (for binary-server: a signed `${base}/upload-chunk/${app}/${key}?token=`) and the stack
|
|
99
|
+
* builds the ChunkedUploadService over its own catalogue + a storage-ops ledger on `db`, then
|
|
100
|
+
* mount() auto-wires the /upload-init, /upload-complete, /upload-abort routes. Omit ⇒ chunked is
|
|
101
|
+
* off and clients fall back to the streamed single PUT (disk-only deploys). */
|
|
102
|
+
chunkedUpload?: {
|
|
103
|
+
mintChunkUpload: MintChunkUpload;
|
|
104
|
+
/** Byte-backend label recorded on each ledger op. Default 'binary'. */
|
|
105
|
+
backend?: string;
|
|
106
|
+
/** Per-chunk size. Default DEFAULT_UPLOAD_CHUNK_SIZE (8 MiB), always < MAX_SINGLE_UPLOAD_BYTES. */
|
|
107
|
+
chunkSize?: number;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The auth seams `mount()` needs from the host — the only pieces the stack cannot own. */
|
|
112
|
+
export interface FilesStackMountAuth {
|
|
113
|
+
/** Session auth for uploads + listings (same guard mountMediaRoutes/mountFileRoutes take).
|
|
114
|
+
* Return a MediaCtx to allow, null to reject. */
|
|
115
|
+
authorizeUpload: (c: Context) => Promise<MediaCtx | null> | MediaCtx | null;
|
|
116
|
+
/** Session auth for the private layer → {userId, userSalt}. REQUIRED when `privateMedia` is
|
|
117
|
+
* configured (mount() throws otherwise); ignored when it isn't. */
|
|
118
|
+
authorizePrivate?: (c: Context) => Promise<{ userId: string; userSalt: string } | null>;
|
|
119
|
+
/** Wire large-file chunked uploads (mountFileRoutes' `chunked` passthrough). */
|
|
120
|
+
mintChunkUpload?: ChunkedUploadService;
|
|
121
|
+
/** Private-upload password header (default 'x-private-password'). */
|
|
122
|
+
passwordHeader?: string;
|
|
123
|
+
/** Private-upload unlock-token header (default 'x-unlock-token'). */
|
|
124
|
+
tokenHeader?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The assembled stack. `catalogue` / `fileUrls` / `mediaUrls` / `videoSource` are intentionally
|
|
129
|
+
* named so a cursedshell host can pass the stack straight into `createFilesPlugin(stack)`
|
|
130
|
+
* structurally, and `{ registry }` into `createGalleryPlugin` — the stack does not (and must
|
|
131
|
+
* not, layering) import cursedshell.
|
|
132
|
+
*/
|
|
133
|
+
export interface FilesStack {
|
|
134
|
+
catalogue: FileCatalogue & PrivateFileCatalogue;
|
|
135
|
+
token: StreamTokenService;
|
|
136
|
+
mediaUrls: MediaUrls;
|
|
137
|
+
fileUrls: FileUrls;
|
|
138
|
+
videoSource: MediaSource;
|
|
139
|
+
/** The image-filtered sibling view (photo gallery data source). */
|
|
140
|
+
photoSource: PhotoSource;
|
|
141
|
+
/** The MIXED view — photos + videos in one feed (media gallery data source). */
|
|
142
|
+
mediaSource: MediaGallerySource;
|
|
143
|
+
registry: SourceRegistry;
|
|
144
|
+
/** Null unless `privateMedia` was configured. */
|
|
145
|
+
unlock: UnlockService | null;
|
|
146
|
+
/** The large-file chunked-upload service, or null unless `chunkedUpload` was configured. mount()
|
|
147
|
+
* auto-wires it into the file routes; exposed for observability (pending()) + tests. */
|
|
148
|
+
chunked: ChunkedUploadService | null;
|
|
149
|
+
/**
|
|
150
|
+
* Mount every byte/upload route the stack owns onto a Hono app: mountMediaRoutes
|
|
151
|
+
* (mediaBasePath) + mountFileRoutes (filesBasePath, incl. privateUpload when unlock exists) +
|
|
152
|
+
* mountPrivateRoutes (privateBasePath, when unlock exists). Auth stays host-injected.
|
|
153
|
+
*/
|
|
154
|
+
mount(app: Hono, auth: FilesStackMountAuth): void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Assemble the whole files/media server stack from one options bag. See {@link FilesStack}. */
|
|
158
|
+
export function createFilesStack(opts: FilesStackOptions): FilesStack {
|
|
159
|
+
const owner = opts.owner ?? ((c: MediaCtx) => c.userId);
|
|
160
|
+
const sourceId = opts.sourceId ?? 'files';
|
|
161
|
+
const mediaBasePath = opts.mediaBasePath ?? '/_media';
|
|
162
|
+
const filesBasePath = opts.filesBasePath ?? '/_files';
|
|
163
|
+
const { db, store, workDir } = opts;
|
|
164
|
+
|
|
165
|
+
const token = createStreamTokenService({ secret: opts.streamSecret });
|
|
166
|
+
const mediaUrls = createMediaUrls({
|
|
167
|
+
baseUrl: opts.mediaPublicBase ?? mediaBasePath,
|
|
168
|
+
token,
|
|
169
|
+
owner,
|
|
170
|
+
});
|
|
171
|
+
const fileUrls = createFileUrls({ baseUrl: opts.filesPublicBase ?? filesBasePath, token, owner });
|
|
172
|
+
|
|
173
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
174
|
+
const catalogue = createSqliteFileCatalogue({
|
|
175
|
+
db,
|
|
176
|
+
store,
|
|
177
|
+
workDir,
|
|
178
|
+
owner,
|
|
179
|
+
ladderHeights: opts.ladderHeights,
|
|
180
|
+
maxProcessingAttempts: opts.maxProcessingAttempts,
|
|
181
|
+
transcodeVideos: opts.transcodeVideos,
|
|
182
|
+
videoProcessor: opts.videoProcessor,
|
|
183
|
+
imageDerivatives: opts.imageDerivatives,
|
|
184
|
+
});
|
|
185
|
+
if (opts.recoverProcessing !== false) catalogue.recoverProcessing();
|
|
186
|
+
if (opts.backfillPosters) {
|
|
187
|
+
catalogue.backfillVideoPosters();
|
|
188
|
+
catalogue.backfillImageThumbnails();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const videoSource = createCatalogueVideoSource({
|
|
192
|
+
id: sourceId,
|
|
193
|
+
title: opts.sourceTitle ?? 'Files',
|
|
194
|
+
catalogue,
|
|
195
|
+
urls: mediaUrls,
|
|
196
|
+
});
|
|
197
|
+
const photoSource = createCataloguePhotoSource({
|
|
198
|
+
id: opts.photoSourceId ?? 'photos',
|
|
199
|
+
title: opts.photoSourceTitle ?? 'Photos',
|
|
200
|
+
catalogue,
|
|
201
|
+
fileUrls,
|
|
202
|
+
mediaUrls,
|
|
203
|
+
});
|
|
204
|
+
const mediaSource = createCatalogueMediaSource({
|
|
205
|
+
id: opts.photoSourceId ?? 'photos',
|
|
206
|
+
title: 'Media',
|
|
207
|
+
catalogue,
|
|
208
|
+
urls: mediaUrls,
|
|
209
|
+
fileUrls,
|
|
210
|
+
// Videos in the mixed feed stream through the registered video source's routes.
|
|
211
|
+
videoSourceId: sourceId,
|
|
212
|
+
});
|
|
213
|
+
const registry = new SourceRegistry().register(videoSource);
|
|
214
|
+
|
|
215
|
+
const unlock = opts.privateMedia ? createUnlockService(opts.privateMedia) : null;
|
|
216
|
+
|
|
217
|
+
const chunked = opts.chunkedUpload
|
|
218
|
+
? createChunkedUploadService({
|
|
219
|
+
catalogue,
|
|
220
|
+
ledger: createStorageOpsLedger(db),
|
|
221
|
+
backend: opts.chunkedUpload.backend ?? 'binary',
|
|
222
|
+
mintChunkUpload: opts.chunkedUpload.mintChunkUpload,
|
|
223
|
+
chunkSize: opts.chunkedUpload.chunkSize,
|
|
224
|
+
owner,
|
|
225
|
+
})
|
|
226
|
+
: null;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
catalogue,
|
|
230
|
+
token,
|
|
231
|
+
mediaUrls,
|
|
232
|
+
fileUrls,
|
|
233
|
+
videoSource,
|
|
234
|
+
photoSource,
|
|
235
|
+
mediaSource,
|
|
236
|
+
registry,
|
|
237
|
+
unlock,
|
|
238
|
+
chunked,
|
|
239
|
+
mount(app, auth) {
|
|
240
|
+
if (unlock && !auth.authorizePrivate) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
'createFilesStack: privateMedia is configured but mount() got no authorizePrivate — ' +
|
|
243
|
+
'the private routes need a session→{userId, userSalt} resolver.',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
mountMediaRoutes(app, {
|
|
247
|
+
store,
|
|
248
|
+
token,
|
|
249
|
+
basePath: mediaBasePath,
|
|
250
|
+
registry,
|
|
251
|
+
authorizeUpload: auth.authorizeUpload,
|
|
252
|
+
});
|
|
253
|
+
mountFileRoutes(app, {
|
|
254
|
+
store,
|
|
255
|
+
token,
|
|
256
|
+
basePath: filesBasePath,
|
|
257
|
+
catalogue,
|
|
258
|
+
authorizeUpload: auth.authorizeUpload,
|
|
259
|
+
// Prefer a mount-time override, else the service the stack built from `chunkedUpload` opts.
|
|
260
|
+
chunked: auth.mintChunkUpload ?? chunked ?? undefined,
|
|
261
|
+
privateUpload:
|
|
262
|
+
unlock && auth.authorizePrivate
|
|
263
|
+
? {
|
|
264
|
+
unlock,
|
|
265
|
+
authorize: auth.authorizePrivate,
|
|
266
|
+
passwordHeader: auth.passwordHeader,
|
|
267
|
+
tokenHeader: auth.tokenHeader,
|
|
268
|
+
passwordPolicy: opts.privateMedia?.passwordPolicy,
|
|
269
|
+
}
|
|
270
|
+
: undefined,
|
|
271
|
+
});
|
|
272
|
+
if (unlock && auth.authorizePrivate) {
|
|
273
|
+
mountPrivateRoutes(app, {
|
|
274
|
+
store,
|
|
275
|
+
index: catalogue.asPrivateIndex(),
|
|
276
|
+
unlock,
|
|
277
|
+
basePath: opts.privateBasePath,
|
|
278
|
+
authorize: auth.authorizePrivate,
|
|
279
|
+
passwordPolicy: opts.privateMedia?.passwordPolicy,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { runMigrations } from '../../migration/runner';
|
|
4
|
+
import type { MediaStore } from '../types';
|
|
5
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
6
|
+
import {
|
|
7
|
+
createLedgeredDeletes,
|
|
8
|
+
createStorageOpsLedger,
|
|
9
|
+
createStorageReconciler,
|
|
10
|
+
} from './storageOps';
|
|
11
|
+
|
|
12
|
+
// A fake store whose remove() fails the first `failTimes` calls per key, then succeeds — models a
|
|
13
|
+
// flaky remote backend (binary-server) so the reconciler's heal path is exercised deterministically.
|
|
14
|
+
function flakyStore(failTimes: number): MediaStore & { removed: Set<string> } {
|
|
15
|
+
const removed = new Set<string>();
|
|
16
|
+
const attempts = new Map<string, number>();
|
|
17
|
+
const fail = (key: string) => {
|
|
18
|
+
const n = (attempts.get(key) ?? 0) + 1;
|
|
19
|
+
attempts.set(key, n);
|
|
20
|
+
if (n <= failTimes) throw new Error(`transient failure ${n} for ${key}`);
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
removed,
|
|
24
|
+
async put() {},
|
|
25
|
+
async putStream() {},
|
|
26
|
+
async get() {
|
|
27
|
+
return null;
|
|
28
|
+
},
|
|
29
|
+
async remove(key) {
|
|
30
|
+
fail(key);
|
|
31
|
+
removed.add(key);
|
|
32
|
+
},
|
|
33
|
+
async removePrefix(prefix) {
|
|
34
|
+
fail(prefix);
|
|
35
|
+
removed.add(prefix);
|
|
36
|
+
},
|
|
37
|
+
async stat() {
|
|
38
|
+
return null;
|
|
39
|
+
},
|
|
40
|
+
async getRange() {
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('storage-ops ledger', () => {
|
|
47
|
+
let db: Database;
|
|
48
|
+
let clock = 0;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
db = new Database(':memory:');
|
|
51
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
52
|
+
clock = 0;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('records pending, marks done/failed, and reports outstanding correctly', () => {
|
|
56
|
+
const ledger = createStorageOpsLedger(db, () => ++clock);
|
|
57
|
+
const a = ledger.record({ owner: 'u1', action: 'delete', key: 'fs/u1/a', backend: 'disk' });
|
|
58
|
+
const b = ledger.record({ owner: 'u1', action: 'delete', key: 'fs/u1/b', backend: 'disk' });
|
|
59
|
+
|
|
60
|
+
expect(ledger.get(a)?.status).toBe('pending');
|
|
61
|
+
ledger.markDone(a);
|
|
62
|
+
expect(ledger.get(a)?.status).toBe('done');
|
|
63
|
+
ledger.markFailed(b, 'boom');
|
|
64
|
+
expect(ledger.get(b)?.status).toBe('failed');
|
|
65
|
+
expect(ledger.get(b)?.attempts).toBe(1);
|
|
66
|
+
|
|
67
|
+
// `done` excluded; `failed` under the cap still outstanding.
|
|
68
|
+
expect(ledger.outstanding().map((o) => o.opId)).toEqual([b]);
|
|
69
|
+
// Over the attempt cap → dropped from outstanding (a poison op won't spin forever).
|
|
70
|
+
expect(ledger.outstanding({ maxAttempts: 1 })).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('`done` is a monotonic terminal state — a late failure can never flip it back', () => {
|
|
74
|
+
const ledger = createStorageOpsLedger(db, () => ++clock);
|
|
75
|
+
const op = ledger.record({ owner: 'u1', action: 'upload', key: 'fs/u1/x', backend: 'binary' });
|
|
76
|
+
ledger.markDone(op);
|
|
77
|
+
// A duplicate completion webhook racing the reconciler (or a stale failure) must NOT corrupt it.
|
|
78
|
+
ledger.markFailed(op, 'late webhook says failed');
|
|
79
|
+
expect(ledger.get(op)?.status).toBe('done');
|
|
80
|
+
expect(ledger.get(op)?.attempts).toBe(0);
|
|
81
|
+
ledger.markDone(op); // idempotent
|
|
82
|
+
expect(ledger.get(op)?.status).toBe('done');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('ledgered deletes mark done on success and keep a failed row outstanding', async () => {
|
|
86
|
+
const ledger = createStorageOpsLedger(db, () => ++clock);
|
|
87
|
+
const store = flakyStore(0); // never fails
|
|
88
|
+
const del = createLedgeredDeletes(store, ledger, 'binary');
|
|
89
|
+
await del.removeFor('u1', 'f1', 'fs/u1/f1');
|
|
90
|
+
expect(store.removed.has('fs/u1/f1')).toBe(true);
|
|
91
|
+
expect(ledger.outstanding()).toEqual([]); // marked done
|
|
92
|
+
|
|
93
|
+
const flaky = createLedgeredDeletes(flakyStore(99), ledger, 'binary');
|
|
94
|
+
await expect(flaky.removeFor('u1', 'f2', 'fs/u1/f2')).rejects.toThrow();
|
|
95
|
+
// The failed delete survives for the reconciler.
|
|
96
|
+
expect(ledger.outstanding().map((o) => o.key)).toEqual(['fs/u1/f2']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('reconciler heals a transiently-failed delete on a later pass', async () => {
|
|
100
|
+
const ledger = createStorageOpsLedger(db, () => ++clock);
|
|
101
|
+
const store = flakyStore(1); // fails once per key, then succeeds
|
|
102
|
+
const del = createLedgeredDeletes(store, ledger, 'binary');
|
|
103
|
+
|
|
104
|
+
await expect(del.removeFor('u1', 'f3', 'fs/u1/f3')).rejects.toThrow(); // first attempt fails
|
|
105
|
+
expect(ledger.get(ledger.outstanding()[0].opId)?.status).toBe('failed');
|
|
106
|
+
|
|
107
|
+
// Reconcile → the retry succeeds (flaky store now lets it through) → healed, nothing left.
|
|
108
|
+
const reconciler = createStorageReconciler(store, ledger);
|
|
109
|
+
expect(await reconciler.reconcile()).toEqual({ healed: 1, failed: 0 });
|
|
110
|
+
expect(ledger.outstanding()).toEqual([]);
|
|
111
|
+
expect(store.removed.has('fs/u1/f3')).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('reconciler bumps attempts and gives up past the cap (no infinite retry)', async () => {
|
|
115
|
+
const ledger = createStorageOpsLedger(db, () => ++clock);
|
|
116
|
+
const store = flakyStore(99); // always fails
|
|
117
|
+
ledger.record({ owner: 'u1', action: 'delete', key: 'fs/u1/dead', backend: 'binary' });
|
|
118
|
+
|
|
119
|
+
const reconciler = createStorageReconciler(store, ledger);
|
|
120
|
+
// Each pass bumps attempts; after 3 passes attempts reaches the cap and it drops out (no
|
|
121
|
+
// infinite retry of a poison op).
|
|
122
|
+
for (let i = 0; i < 3; i++) await reconciler.reconcile({ maxAttempts: 3 });
|
|
123
|
+
expect(ledger.outstanding({ maxAttempts: 3 })).toEqual([]);
|
|
124
|
+
expect(store.removed.has('fs/u1/dead')).toBe(false); // never actually removed
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type { MediaStore } from '../types';
|
|
3
|
+
|
|
4
|
+
// ── Storage-operations ledger + reconciler ───────────────────────────────────────
|
|
5
|
+
// A durable write-ahead log for CROSS-BOUNDARY byte operations (delete / delete-prefix / an
|
|
6
|
+
// assembled chunked upload) against a remote backend like binary-server, where the call can
|
|
7
|
+
// succeed on our side but the ack never arrives — or fail transiently — leaving bytes orphaned
|
|
8
|
+
// or a delete un-applied. Before each such op we record a `pending` row; on success we mark it
|
|
9
|
+
// `done`; a crash / failed call leaves it `pending`, and the reconciler re-drives it to
|
|
10
|
+
// completion on the next boot (idempotent — remove/removePrefix must not throw on a missing key).
|
|
11
|
+
//
|
|
12
|
+
// Uploads that fail simply error the request (no ledger needed — nothing to heal). The ledger
|
|
13
|
+
// targets the ops whose FAILURE silently leaks: a delete binary-server never applied, or an
|
|
14
|
+
// assemble webhook we recorded but must confirm. The `storage_operations` table is created by the
|
|
15
|
+
// FileCatalogue migration; binary-server echoes our `op_id` in its own event ledger so the two
|
|
16
|
+
// can be reconciled against each other (see binary-server db.ts).
|
|
17
|
+
|
|
18
|
+
export type StorageAction = 'upload' | 'delete' | 'delete_prefix';
|
|
19
|
+
export type StorageOpStatus = 'pending' | 'done' | 'failed';
|
|
20
|
+
|
|
21
|
+
export type StorageOpRow = {
|
|
22
|
+
opId: string;
|
|
23
|
+
owner: string;
|
|
24
|
+
fileId: string | null;
|
|
25
|
+
action: StorageAction;
|
|
26
|
+
key: string;
|
|
27
|
+
backend: string;
|
|
28
|
+
status: StorageOpStatus;
|
|
29
|
+
attempts: number;
|
|
30
|
+
error: string | null;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
updatedAt: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type DbRow = {
|
|
36
|
+
op_id: string;
|
|
37
|
+
owner: string;
|
|
38
|
+
file_id: string | null;
|
|
39
|
+
action: string;
|
|
40
|
+
key: string;
|
|
41
|
+
backend: string;
|
|
42
|
+
status: string;
|
|
43
|
+
attempts: number;
|
|
44
|
+
error: string | null;
|
|
45
|
+
created_at: number;
|
|
46
|
+
updated_at: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const toRow = (r: DbRow): StorageOpRow => ({
|
|
50
|
+
opId: r.op_id,
|
|
51
|
+
owner: r.owner,
|
|
52
|
+
fileId: r.file_id,
|
|
53
|
+
action: r.action as StorageAction,
|
|
54
|
+
key: r.key,
|
|
55
|
+
backend: r.backend,
|
|
56
|
+
status: r.status as StorageOpStatus,
|
|
57
|
+
attempts: r.attempts,
|
|
58
|
+
error: r.error,
|
|
59
|
+
createdAt: r.created_at,
|
|
60
|
+
updatedAt: r.updated_at,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export interface StorageOpsLedger {
|
|
64
|
+
/** Record a pending op and return its id (caller may pass one, e.g. to share with binary-server
|
|
65
|
+
* as `x-op-id`; otherwise a uuid is minted). */
|
|
66
|
+
record(op: {
|
|
67
|
+
owner: string;
|
|
68
|
+
fileId?: string | null;
|
|
69
|
+
action: StorageAction;
|
|
70
|
+
key: string;
|
|
71
|
+
backend: string;
|
|
72
|
+
opId?: string;
|
|
73
|
+
}): string;
|
|
74
|
+
markDone(opId: string): void;
|
|
75
|
+
markFailed(opId: string, error: string): void;
|
|
76
|
+
get(opId: string): StorageOpRow | null;
|
|
77
|
+
/** Ops still needing work: `pending`, or `failed` under the attempt cap, created before `before`
|
|
78
|
+
* (default: everything). Oldest first. */
|
|
79
|
+
outstanding(opts?: { maxAttempts?: number; before?: number }): StorageOpRow[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createStorageOpsLedger(
|
|
83
|
+
db: Database,
|
|
84
|
+
now: () => number = () => Date.now(),
|
|
85
|
+
): StorageOpsLedger {
|
|
86
|
+
const insert = db.query(
|
|
87
|
+
`INSERT INTO storage_operations (op_id, owner, file_id, action, key, backend, status, attempts, created_at, updated_at)
|
|
88
|
+
VALUES ($op, $owner, $file, $action, $key, $backend, 'pending', 0, $now, $now)`,
|
|
89
|
+
);
|
|
90
|
+
// MONOTONIC terminal state: once an op is `done` it can never flip back (a duplicate completion
|
|
91
|
+
// webhook racing the reconciler, or a late failure, must not corrupt a settled success). `failed`
|
|
92
|
+
// is NOT terminal — the reconciler retries a failed delete and can heal it to `done`, or bump its
|
|
93
|
+
// attempts — so both writes guard only against overwriting `done`. (Ported from ca's resolveOp
|
|
94
|
+
// `CASE WHEN status='pending'` invariant, adapted to our two-writer model.)
|
|
95
|
+
const setDone = db.query(
|
|
96
|
+
`UPDATE storage_operations SET status = 'done', updated_at = $now WHERE op_id = $op AND status != 'done'`,
|
|
97
|
+
);
|
|
98
|
+
const setFailed = db.query(
|
|
99
|
+
`UPDATE storage_operations SET status = 'failed', attempts = attempts + 1, error = $error, updated_at = $now WHERE op_id = $op AND status != 'done'`,
|
|
100
|
+
);
|
|
101
|
+
const getOne = db.query<DbRow, { $op: string }>(
|
|
102
|
+
`SELECT * FROM storage_operations WHERE op_id = $op`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
record(op) {
|
|
107
|
+
const opId = op.opId ?? crypto.randomUUID();
|
|
108
|
+
insert.run({
|
|
109
|
+
$op: opId,
|
|
110
|
+
$owner: op.owner,
|
|
111
|
+
$file: op.fileId ?? null,
|
|
112
|
+
$action: op.action,
|
|
113
|
+
$key: op.key,
|
|
114
|
+
$backend: op.backend,
|
|
115
|
+
$now: now(),
|
|
116
|
+
});
|
|
117
|
+
return opId;
|
|
118
|
+
},
|
|
119
|
+
markDone(opId) {
|
|
120
|
+
setDone.run({ $op: opId, $now: now() });
|
|
121
|
+
},
|
|
122
|
+
markFailed(opId, error) {
|
|
123
|
+
setFailed.run({ $op: opId, $error: error.slice(0, 500), $now: now() });
|
|
124
|
+
},
|
|
125
|
+
get(opId) {
|
|
126
|
+
const r = getOne.get({ $op: opId });
|
|
127
|
+
return r ? toRow(r) : null;
|
|
128
|
+
},
|
|
129
|
+
outstanding(opts = {}) {
|
|
130
|
+
const maxAttempts = opts.maxAttempts ?? 5;
|
|
131
|
+
const before = opts.before ?? now() + 1;
|
|
132
|
+
return db
|
|
133
|
+
.query<DbRow, { $max: number; $before: number }>(
|
|
134
|
+
`SELECT * FROM storage_operations
|
|
135
|
+
WHERE status != 'done' AND attempts < $max AND created_at < $before
|
|
136
|
+
ORDER BY created_at ASC`,
|
|
137
|
+
)
|
|
138
|
+
.all({ $max: maxAttempts, $before: before })
|
|
139
|
+
.map(toRow);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Wrap a MediaStore so DELETE / DELETE-PREFIX are ledgered: a `pending` row is written first, the
|
|
145
|
+
// op attempted, then marked `done`; a failure marks it `failed` and rethrows (the row survives for
|
|
146
|
+
// the reconciler). Reads/writes pass straight through — only the leak-prone deletes are logged.
|
|
147
|
+
export function createLedgeredDeletes(
|
|
148
|
+
store: MediaStore,
|
|
149
|
+
ledger: StorageOpsLedger,
|
|
150
|
+
backend: string,
|
|
151
|
+
): Pick<MediaStore, 'remove' | 'removePrefix'> & {
|
|
152
|
+
removeFor(owner: string, fileId: string | null, key: string): Promise<void>;
|
|
153
|
+
removePrefixFor(owner: string, fileId: string | null, prefix: string): Promise<void>;
|
|
154
|
+
} {
|
|
155
|
+
const run = async (
|
|
156
|
+
action: StorageAction,
|
|
157
|
+
owner: string,
|
|
158
|
+
fileId: string | null,
|
|
159
|
+
key: string,
|
|
160
|
+
exec: () => Promise<void>,
|
|
161
|
+
) => {
|
|
162
|
+
const opId = ledger.record({ owner, fileId, action, key, backend });
|
|
163
|
+
try {
|
|
164
|
+
await exec();
|
|
165
|
+
ledger.markDone(opId);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
ledger.markFailed(opId, e instanceof Error ? e.message : String(e));
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
remove: (key) => run('delete', '', null, key, () => store.remove(key)),
|
|
173
|
+
removePrefix: (prefix) =>
|
|
174
|
+
run('delete_prefix', '', null, prefix, () => store.removePrefix(prefix)),
|
|
175
|
+
removeFor: (owner, fileId, key) => run('delete', owner, fileId, key, () => store.remove(key)),
|
|
176
|
+
removePrefixFor: (owner, fileId, prefix) =>
|
|
177
|
+
run('delete_prefix', owner, fileId, prefix, () => store.removePrefix(prefix)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface StorageReconciler {
|
|
182
|
+
/** Re-drive every outstanding op to completion (idempotent). Returns how many healed vs. still
|
|
183
|
+
* failing. Safe to call on boot and on an interval. */
|
|
184
|
+
reconcile(opts?: { maxAttempts?: number }): Promise<{ healed: number; failed: number }>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createStorageReconciler(
|
|
188
|
+
store: MediaStore,
|
|
189
|
+
ledger: StorageOpsLedger,
|
|
190
|
+
): StorageReconciler {
|
|
191
|
+
return {
|
|
192
|
+
async reconcile(opts = {}) {
|
|
193
|
+
let healed = 0;
|
|
194
|
+
let failed = 0;
|
|
195
|
+
for (const op of ledger.outstanding({ maxAttempts: opts.maxAttempts })) {
|
|
196
|
+
// Only DELETES are re-driven by re-issuing the store call (idempotent). An 'upload' op is
|
|
197
|
+
// healed by its completion webhook (or an app-level check against binary-server's own event
|
|
198
|
+
// ledger) — re-issuing store.remove here would wrongly delete the just-uploaded bytes.
|
|
199
|
+
if (op.action === 'upload') continue;
|
|
200
|
+
try {
|
|
201
|
+
if (op.action === 'delete') await store.remove(op.key);
|
|
202
|
+
else await store.removePrefix(op.key);
|
|
203
|
+
ledger.markDone(op.opId);
|
|
204
|
+
healed++;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
ledger.markFailed(op.opId, e instanceof Error ? e.message : String(e));
|
|
207
|
+
failed++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { healed, failed };
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|