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,163 @@
|
|
|
1
|
+
// Schema for the FileCatalogue. Runs through cursedbelt's migration runner (idempotent, recorded
|
|
2
|
+
// in `_migrations`). Improves on ca's fs_nodes: the smart-view `file_type` is stored + indexed
|
|
3
|
+
// (not re-derived per list), tags live in an indexed junction (fast facets, not a JSON scan),
|
|
4
|
+
// processing state is typed columns (not an opaque blob), and a storage-operations ledger backs
|
|
5
|
+
// durable cross-boundary (binary-server/S3) ops. Flat — no parent_id tree.
|
|
6
|
+
export const FILE_CATALOGUE_MIGRATIONS = [
|
|
7
|
+
{
|
|
8
|
+
name: '0001_file_catalogue',
|
|
9
|
+
up(db) {
|
|
10
|
+
db.run(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
owner TEXT NOT NULL,
|
|
14
|
+
name TEXT NOT NULL DEFAULT '',
|
|
15
|
+
mime TEXT NOT NULL DEFAULT '',
|
|
16
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
file_type TEXT NOT NULL DEFAULT 'other',
|
|
18
|
+
is_private INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
has_thumbnail INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
has_hls INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
has_sprites INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
duration REAL,
|
|
23
|
+
processing_status TEXT NOT NULL DEFAULT 'ready',
|
|
24
|
+
processing_progress INTEGER NOT NULL DEFAULT 100,
|
|
25
|
+
storage_backend TEXT NOT NULL DEFAULT 'disk',
|
|
26
|
+
enc_name TEXT NOT NULL DEFAULT '',
|
|
27
|
+
enc_dek TEXT NOT NULL DEFAULT '',
|
|
28
|
+
enc_key_type TEXT NOT NULL DEFAULT '',
|
|
29
|
+
created_at INTEGER NOT NULL,
|
|
30
|
+
updated_at INTEGER NOT NULL
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
// Listing is cursor-keyed on (created_at, id); these cover the flat feed + smart-view filter.
|
|
34
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_created ON files(owner, created_at DESC, id DESC)`);
|
|
35
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_type_created ON files(owner, file_type, created_at DESC, id DESC)`);
|
|
36
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_private ON files(owner, is_private)`);
|
|
37
|
+
// Tags as a junction (indexed) — single-tag filter + facet counts are index lookups, not a
|
|
38
|
+
// full-table JSON parse (ca's weakness). `owner` denormalized so facet queries stay covering.
|
|
39
|
+
db.run(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS file_tags (
|
|
41
|
+
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
42
|
+
tag TEXT NOT NULL,
|
|
43
|
+
owner TEXT NOT NULL,
|
|
44
|
+
is_private INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
PRIMARY KEY (file_id, tag)
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_file_tags_owner_tag ON file_tags(owner, tag)`);
|
|
49
|
+
// Durable ledger for cross-boundary storage ops (binary-server/S3 put/delete) — the
|
|
50
|
+
// reconciler heals `pending` rows the server never got an ack for.
|
|
51
|
+
db.run(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS storage_operations (
|
|
53
|
+
op_id TEXT PRIMARY KEY,
|
|
54
|
+
owner TEXT NOT NULL,
|
|
55
|
+
file_id TEXT,
|
|
56
|
+
action TEXT NOT NULL,
|
|
57
|
+
key TEXT NOT NULL,
|
|
58
|
+
backend TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
60
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
error TEXT,
|
|
62
|
+
created_at INTEGER NOT NULL,
|
|
63
|
+
updated_at INTEGER NOT NULL
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_storage_ops_status ON storage_operations(status, created_at)`);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: '0002_file_collections',
|
|
71
|
+
up(db) {
|
|
72
|
+
// Collections (Apple-Photos-style groupings). kind='favorites' is a system singleton per owner;
|
|
73
|
+
// kind='custom' are user-defined. visibility='unlock' hides the collection unless explicitly
|
|
74
|
+
// requested (like a private shelf). Membership is in file_collection_files; files are never
|
|
75
|
+
// deleted by removing a collection.
|
|
76
|
+
db.run(`
|
|
77
|
+
CREATE TABLE IF NOT EXISTS file_collections (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
owner TEXT NOT NULL,
|
|
80
|
+
name TEXT NOT NULL,
|
|
81
|
+
kind TEXT NOT NULL DEFAULT 'custom',
|
|
82
|
+
visibility TEXT NOT NULL DEFAULT 'normal',
|
|
83
|
+
created_at INTEGER NOT NULL,
|
|
84
|
+
updated_at INTEGER NOT NULL
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_file_collections_owner ON file_collections(owner, name)`);
|
|
88
|
+
// Unique partial index: at most one 'favorites' collection per owner.
|
|
89
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_file_collections_owner_favorites
|
|
90
|
+
ON file_collections(owner) WHERE kind = 'favorites'`);
|
|
91
|
+
// Junction: which files belong to which collection. owner is denormalized so membership
|
|
92
|
+
// queries can stay covering without a join back to files. Foreign-key cascades are declared
|
|
93
|
+
// but SQLite's FK pragma may be off at runtime, so catalogue.remove() explicitly cleans
|
|
94
|
+
// file_collection_files rows (matching the file_tags pattern).
|
|
95
|
+
db.run(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS file_collection_files (
|
|
97
|
+
collection_id TEXT NOT NULL REFERENCES file_collections(id) ON DELETE CASCADE,
|
|
98
|
+
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
99
|
+
owner TEXT NOT NULL,
|
|
100
|
+
added_at INTEGER NOT NULL,
|
|
101
|
+
PRIMARY KEY (collection_id, file_id)
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_fcf_owner_file ON file_collection_files(owner, file_id)`);
|
|
105
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_fcf_collection ON file_collection_files(collection_id, added_at DESC)`);
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: '0003_file_sort_indexes',
|
|
110
|
+
up(db) {
|
|
111
|
+
// Keyset pagination under the `name` / `size` sorts stays an index walk, mirroring
|
|
112
|
+
// idx_files_owner_created for the default `recent` sort.
|
|
113
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_name ON files(owner, name ASC, id ASC)`);
|
|
114
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_size ON files(owner, size DESC, id DESC)`);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: '0004_file_random_and_views',
|
|
119
|
+
up(db) {
|
|
120
|
+
// Per-row stable random key backing the seeded `random` gallery order. A session mints a
|
|
121
|
+
// seed (folded into the cursor); the sort key is `rand_key` remapped by that seed, so the
|
|
122
|
+
// order is a stable total order for keyset pagination while each visit reshuffles. ADD COLUMN
|
|
123
|
+
// can't take a non-constant DEFAULT, so seed existing rows here; new rows set it at insert.
|
|
124
|
+
db.run(`ALTER TABLE files ADD COLUMN rand_key INTEGER NOT NULL DEFAULT 0`);
|
|
125
|
+
// random() is a 64-bit int; mask to a non-negative 31-bit value so the seeded remap
|
|
126
|
+
// (rand_key * mul + add) stays well inside SQLite's signed-64-bit integer range.
|
|
127
|
+
db.run(`UPDATE files SET rand_key = (random() & 2147483647)`);
|
|
128
|
+
// View-event ledger: last-viewed timestamp + count per file, powering the `lastViewed` sort.
|
|
129
|
+
// One row per file (upserted on each open). owner is denormalized so the sort's join stays
|
|
130
|
+
// owner-scoped. FK cascade is declared but the pragma may be off, so catalogue.remove() also
|
|
131
|
+
// cleans these rows explicitly (matching the file_tags / file_collection_files pattern).
|
|
132
|
+
db.run(`
|
|
133
|
+
CREATE TABLE IF NOT EXISTS file_views (
|
|
134
|
+
file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
|
|
135
|
+
owner TEXT NOT NULL,
|
|
136
|
+
viewed_at INTEGER NOT NULL,
|
|
137
|
+
view_count INTEGER NOT NULL DEFAULT 0
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_file_views_owner_viewed ON file_views(owner, viewed_at DESC)`);
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: '0005_processing_attempts',
|
|
145
|
+
up(db) {
|
|
146
|
+
// Counts transcode kicks per file so recoverProcessing can stop re-kicking a job that
|
|
147
|
+
// keeps dying (e.g. ffmpeg OOM-killed on a small host → service restart → re-kick loop).
|
|
148
|
+
db.run(`ALTER TABLE files ADD COLUMN processing_attempts INTEGER NOT NULL DEFAULT 0`);
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: '0006_file_trash',
|
|
153
|
+
up(db) {
|
|
154
|
+
// Trash-first delete: `trash()` sets trashed_at instead of dropping the row, so a mistaken
|
|
155
|
+
// delete is recoverable via `restore()`. `remove()` stays the PERMANENT hard-delete — now a
|
|
156
|
+
// deliberate second action (typically only invoked on an already-trashed row) rather than the
|
|
157
|
+
// primary delete path. `list()` excludes trashed rows by default (NULL check stays an index
|
|
158
|
+
// hit); pass `trashedOnly` to browse the trash bin itself.
|
|
159
|
+
db.run(`ALTER TABLE files ADD COLUMN trashed_at INTEGER`);
|
|
160
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_trashed ON files(owner, trashed_at)`);
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mimeByExt(name: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mimeForFilename } from 'cwip/string';
|
|
2
|
+
// Ext → mime for local-disk import (catalogue.importFromPath) — the catalogue needs SOME mime
|
|
3
|
+
// to classify + serve a file that never passed through a browser's upload Content-Type.
|
|
4
|
+
//
|
|
5
|
+
// The table itself moved DOWN to `cwip/string`'s `fileKind` (2026-07-31): the viewer UI and the
|
|
6
|
+
// notes ingest pipeline need the same knowledge, and cwip is the only layer all three can read
|
|
7
|
+
// from. This wrapper stays so existing server call sites keep working; it is now one line, and
|
|
8
|
+
// the lookup it delegates to is a superset of what used to live here (code, data, archive and
|
|
9
|
+
// markup types the old table lacked). Unknown still falls back to 'application/octet-stream',
|
|
10
|
+
// which `classifyFileType` maps to 'other' — importable, just without image/video processing.
|
|
11
|
+
export function mimeByExt(name) {
|
|
12
|
+
return mimeForFilename(name);
|
|
13
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type { Context, Hono } from 'hono';
|
|
3
|
+
import { type UnlockPasswordPolicy, type UnlockService } from '../../private-media/unlock';
|
|
4
|
+
import { type MediaUrls, SourceRegistry } from '../sources';
|
|
5
|
+
import { type StreamTokenService } from '../streamToken';
|
|
6
|
+
import type { MediaCtx, MediaGallerySource, MediaSource, MediaStore, PhotoSource } from '../types';
|
|
7
|
+
import { type SqliteFileCatalogueOptions } from './catalogue';
|
|
8
|
+
import { type ChunkedUploadService, type MintChunkUpload } from './chunkedUpload';
|
|
9
|
+
import { type FileUrls } from './fileRoutes';
|
|
10
|
+
import type { FileCatalogue, PrivateFileCatalogue } from './types';
|
|
11
|
+
export interface FilesStackOptions {
|
|
12
|
+
/** The catalogue database. The stack runs FILE_CATALOGUE_MIGRATIONS on it itself. */
|
|
13
|
+
db: Database;
|
|
14
|
+
/** Byte backend (disk / binary-server / s3) — the swappable piece. */
|
|
15
|
+
store: MediaStore;
|
|
16
|
+
/** ffmpeg scratch dir for video processing. */
|
|
17
|
+
workDir: string;
|
|
18
|
+
/** Secret for the stream-token service (path-embedded byte-route auth). */
|
|
19
|
+
streamSecret: string;
|
|
20
|
+
/** Owner keyspace segment. Defaults to the userId (files + private are personal) — note the
|
|
21
|
+
* bare primitives default to tenantId; the stack picks the personal axis so every URL/route
|
|
22
|
+
* agrees with the private layer's per-user owner. */
|
|
23
|
+
owner?: (ctx: MediaCtx) => string;
|
|
24
|
+
/** Registered video-source id (also the media-token sourceId). Default 'files'. */
|
|
25
|
+
sourceId?: string;
|
|
26
|
+
/** Video source display title. Default 'Files'. */
|
|
27
|
+
sourceTitle?: string;
|
|
28
|
+
/** Registered photo-source id (the image-filtered view; also its media-token sourceId).
|
|
29
|
+
* Default 'photos'. */
|
|
30
|
+
photoSourceId?: string;
|
|
31
|
+
/** Photo source display title. Default 'Photos'. */
|
|
32
|
+
photoSourceTitle?: string;
|
|
33
|
+
/** Video byte-route mount + URL base (path-only). Default '/_media'. */
|
|
34
|
+
mediaBasePath?: string;
|
|
35
|
+
/** General file byte/upload route mount + URL base (path-only). Default '/_files'. */
|
|
36
|
+
filesBasePath?: string;
|
|
37
|
+
/** PUBLIC base for browser media URLs when it differs from the mount path — a split-origin
|
|
38
|
+
* deploy (SPA on the apex, API on a subdomain) passes the absolute origin+path here
|
|
39
|
+
* ('https://api.example.com/_media') while `mediaBasePath` stays the path-only mount. */
|
|
40
|
+
mediaPublicBase?: string;
|
|
41
|
+
/** PUBLIC base for browser file URLs (split-origin counterpart of `filesBasePath`). */
|
|
42
|
+
filesPublicBase?: string;
|
|
43
|
+
/** Private (encrypted) route mount. Default '/_private'. */
|
|
44
|
+
privateBasePath?: string;
|
|
45
|
+
/** HLS ladder heights for video ingest (createSqliteFileCatalogue passthrough). */
|
|
46
|
+
ladderHeights?: SqliteFileCatalogueOptions['ladderHeights'];
|
|
47
|
+
/** Cap on transcode kicks per file before it's marked failed (createSqliteFileCatalogue
|
|
48
|
+
* passthrough) — the boot-loop guard. Default 3. */
|
|
49
|
+
maxProcessingAttempts?: SqliteFileCatalogueOptions['maxProcessingAttempts'];
|
|
50
|
+
/** Transcode uploaded videos (createSqliteFileCatalogue passthrough). `false` ⇒ videos play
|
|
51
|
+
* raw, no ffmpeg — for memory-constrained hosts. Default true. */
|
|
52
|
+
transcodeVideos?: SqliteFileCatalogueOptions['transcodeVideos'];
|
|
53
|
+
/** Video ingest→derivatives implementation (createSqliteFileCatalogue passthrough) — inject
|
|
54
|
+
* a remote one (binary-server) when the byte store is remote. Default: local ffmpeg. */
|
|
55
|
+
videoProcessor?: SqliteFileCatalogueOptions['videoProcessor'];
|
|
56
|
+
/** Image thumbnail/display derivative implementation (createSqliteFileCatalogue passthrough).
|
|
57
|
+
* Default: local sharp with ffmpeg fallback. */
|
|
58
|
+
imageDerivatives?: SqliteFileCatalogueOptions['imageDerivatives'];
|
|
59
|
+
/** Enables the private (encrypted) stack: unlock service + /_private routes + private uploads.
|
|
60
|
+
* Omit ⇒ `unlock` is null and mount() wires only the public routes. `passwordPolicy` sets what
|
|
61
|
+
* a supplied password MEANS (derive = deniable sets, the default; gate = verify against the
|
|
62
|
+
* app's accounts + recoverable server-key sets — see UnlockPasswordPolicy). */
|
|
63
|
+
privateMedia?: {
|
|
64
|
+
serverSecret: string;
|
|
65
|
+
unlockSecret: string;
|
|
66
|
+
passwordPolicy?: UnlockPasswordPolicy;
|
|
67
|
+
};
|
|
68
|
+
/** Re-kick transcodes a restart left stuck (catalogue.recoverProcessing) at build time.
|
|
69
|
+
* Default true. */
|
|
70
|
+
recoverProcessing?: boolean;
|
|
71
|
+
/** Backfill grid thumbnails at build time for finished PUBLIC rows missing a poster — videos
|
|
72
|
+
* (landed while transcodeVideos was off / failed before the poster step) AND images (e.g. HEIC
|
|
73
|
+
* whose sharp thumbnail failed before the ffmpeg fallback). Default false. */
|
|
74
|
+
backfillPosters?: boolean;
|
|
75
|
+
/** Wire LARGE-file chunked uploads (direct-to-backend). Give the backend-specific per-chunk URL
|
|
76
|
+
* minter (for binary-server: a signed `${base}/upload-chunk/${app}/${key}?token=`) and the stack
|
|
77
|
+
* builds the ChunkedUploadService over its own catalogue + a storage-ops ledger on `db`, then
|
|
78
|
+
* mount() auto-wires the /upload-init, /upload-complete, /upload-abort routes. Omit ⇒ chunked is
|
|
79
|
+
* off and clients fall back to the streamed single PUT (disk-only deploys). */
|
|
80
|
+
chunkedUpload?: {
|
|
81
|
+
mintChunkUpload: MintChunkUpload;
|
|
82
|
+
/** Byte-backend label recorded on each ledger op. Default 'binary'. */
|
|
83
|
+
backend?: string;
|
|
84
|
+
/** Per-chunk size. Default DEFAULT_UPLOAD_CHUNK_SIZE (8 MiB), always < MAX_SINGLE_UPLOAD_BYTES. */
|
|
85
|
+
chunkSize?: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The auth seams `mount()` needs from the host — the only pieces the stack cannot own. */
|
|
89
|
+
export interface FilesStackMountAuth {
|
|
90
|
+
/** Session auth for uploads + listings (same guard mountMediaRoutes/mountFileRoutes take).
|
|
91
|
+
* Return a MediaCtx to allow, null to reject. */
|
|
92
|
+
authorizeUpload: (c: Context) => Promise<MediaCtx | null> | MediaCtx | null;
|
|
93
|
+
/** Session auth for the private layer → {userId, userSalt}. REQUIRED when `privateMedia` is
|
|
94
|
+
* configured (mount() throws otherwise); ignored when it isn't. */
|
|
95
|
+
authorizePrivate?: (c: Context) => Promise<{
|
|
96
|
+
userId: string;
|
|
97
|
+
userSalt: string;
|
|
98
|
+
} | null>;
|
|
99
|
+
/** Wire large-file chunked uploads (mountFileRoutes' `chunked` passthrough). */
|
|
100
|
+
mintChunkUpload?: ChunkedUploadService;
|
|
101
|
+
/** Private-upload password header (default 'x-private-password'). */
|
|
102
|
+
passwordHeader?: string;
|
|
103
|
+
/** Private-upload unlock-token header (default 'x-unlock-token'). */
|
|
104
|
+
tokenHeader?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The assembled stack. `catalogue` / `fileUrls` / `mediaUrls` / `videoSource` are intentionally
|
|
108
|
+
* named so a cursedshell host can pass the stack straight into `createFilesPlugin(stack)`
|
|
109
|
+
* structurally, and `{ registry }` into `createGalleryPlugin` — the stack does not (and must
|
|
110
|
+
* not, layering) import cursedshell.
|
|
111
|
+
*/
|
|
112
|
+
export interface FilesStack {
|
|
113
|
+
catalogue: FileCatalogue & PrivateFileCatalogue;
|
|
114
|
+
token: StreamTokenService;
|
|
115
|
+
mediaUrls: MediaUrls;
|
|
116
|
+
fileUrls: FileUrls;
|
|
117
|
+
videoSource: MediaSource;
|
|
118
|
+
/** The image-filtered sibling view (photo gallery data source). */
|
|
119
|
+
photoSource: PhotoSource;
|
|
120
|
+
/** The MIXED view — photos + videos in one feed (media gallery data source). */
|
|
121
|
+
mediaSource: MediaGallerySource;
|
|
122
|
+
registry: SourceRegistry;
|
|
123
|
+
/** Null unless `privateMedia` was configured. */
|
|
124
|
+
unlock: UnlockService | null;
|
|
125
|
+
/** The large-file chunked-upload service, or null unless `chunkedUpload` was configured. mount()
|
|
126
|
+
* auto-wires it into the file routes; exposed for observability (pending()) + tests. */
|
|
127
|
+
chunked: ChunkedUploadService | null;
|
|
128
|
+
/**
|
|
129
|
+
* Mount every byte/upload route the stack owns onto a Hono app: mountMediaRoutes
|
|
130
|
+
* (mediaBasePath) + mountFileRoutes (filesBasePath, incl. privateUpload when unlock exists) +
|
|
131
|
+
* mountPrivateRoutes (privateBasePath, when unlock exists). Auth stays host-injected.
|
|
132
|
+
*/
|
|
133
|
+
mount(app: Hono, auth: FilesStackMountAuth): void;
|
|
134
|
+
}
|
|
135
|
+
/** Assemble the whole files/media server stack from one options bag. See {@link FilesStack}. */
|
|
136
|
+
export declare function createFilesStack(opts: FilesStackOptions): FilesStack;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { runMigrations } from '../../migration/runner';
|
|
2
|
+
import { mountPrivateRoutes } from '../../private-media/mountPrivateRoutes';
|
|
3
|
+
import { createUnlockService, } from '../../private-media/unlock';
|
|
4
|
+
import { mountMediaRoutes } from '../mountMediaRoutes';
|
|
5
|
+
import { createMediaUrls, SourceRegistry } from '../sources';
|
|
6
|
+
import { createStreamTokenService } from '../streamToken';
|
|
7
|
+
import { createCatalogueMediaSource, createCataloguePhotoSource, createCatalogueVideoSource, } from './adapter';
|
|
8
|
+
import { createSqliteFileCatalogue } from './catalogue';
|
|
9
|
+
import { createChunkedUploadService, } from './chunkedUpload';
|
|
10
|
+
import { createFileUrls, mountFileRoutes } from './fileRoutes';
|
|
11
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
12
|
+
import { createStorageOpsLedger } from './storageOps';
|
|
13
|
+
/** Assemble the whole files/media server stack from one options bag. See {@link FilesStack}. */
|
|
14
|
+
export function createFilesStack(opts) {
|
|
15
|
+
const owner = opts.owner ?? ((c) => c.userId);
|
|
16
|
+
const sourceId = opts.sourceId ?? 'files';
|
|
17
|
+
const mediaBasePath = opts.mediaBasePath ?? '/_media';
|
|
18
|
+
const filesBasePath = opts.filesBasePath ?? '/_files';
|
|
19
|
+
const { db, store, workDir } = opts;
|
|
20
|
+
const token = createStreamTokenService({ secret: opts.streamSecret });
|
|
21
|
+
const mediaUrls = createMediaUrls({
|
|
22
|
+
baseUrl: opts.mediaPublicBase ?? mediaBasePath,
|
|
23
|
+
token,
|
|
24
|
+
owner,
|
|
25
|
+
});
|
|
26
|
+
const fileUrls = createFileUrls({ baseUrl: opts.filesPublicBase ?? filesBasePath, token, owner });
|
|
27
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
28
|
+
const catalogue = createSqliteFileCatalogue({
|
|
29
|
+
db,
|
|
30
|
+
store,
|
|
31
|
+
workDir,
|
|
32
|
+
owner,
|
|
33
|
+
ladderHeights: opts.ladderHeights,
|
|
34
|
+
maxProcessingAttempts: opts.maxProcessingAttempts,
|
|
35
|
+
transcodeVideos: opts.transcodeVideos,
|
|
36
|
+
videoProcessor: opts.videoProcessor,
|
|
37
|
+
imageDerivatives: opts.imageDerivatives,
|
|
38
|
+
});
|
|
39
|
+
if (opts.recoverProcessing !== false)
|
|
40
|
+
catalogue.recoverProcessing();
|
|
41
|
+
if (opts.backfillPosters) {
|
|
42
|
+
catalogue.backfillVideoPosters();
|
|
43
|
+
catalogue.backfillImageThumbnails();
|
|
44
|
+
}
|
|
45
|
+
const videoSource = createCatalogueVideoSource({
|
|
46
|
+
id: sourceId,
|
|
47
|
+
title: opts.sourceTitle ?? 'Files',
|
|
48
|
+
catalogue,
|
|
49
|
+
urls: mediaUrls,
|
|
50
|
+
});
|
|
51
|
+
const photoSource = createCataloguePhotoSource({
|
|
52
|
+
id: opts.photoSourceId ?? 'photos',
|
|
53
|
+
title: opts.photoSourceTitle ?? 'Photos',
|
|
54
|
+
catalogue,
|
|
55
|
+
fileUrls,
|
|
56
|
+
mediaUrls,
|
|
57
|
+
});
|
|
58
|
+
const mediaSource = createCatalogueMediaSource({
|
|
59
|
+
id: opts.photoSourceId ?? 'photos',
|
|
60
|
+
title: 'Media',
|
|
61
|
+
catalogue,
|
|
62
|
+
urls: mediaUrls,
|
|
63
|
+
fileUrls,
|
|
64
|
+
// Videos in the mixed feed stream through the registered video source's routes.
|
|
65
|
+
videoSourceId: sourceId,
|
|
66
|
+
});
|
|
67
|
+
const registry = new SourceRegistry().register(videoSource);
|
|
68
|
+
const unlock = opts.privateMedia ? createUnlockService(opts.privateMedia) : null;
|
|
69
|
+
const chunked = opts.chunkedUpload
|
|
70
|
+
? createChunkedUploadService({
|
|
71
|
+
catalogue,
|
|
72
|
+
ledger: createStorageOpsLedger(db),
|
|
73
|
+
backend: opts.chunkedUpload.backend ?? 'binary',
|
|
74
|
+
mintChunkUpload: opts.chunkedUpload.mintChunkUpload,
|
|
75
|
+
chunkSize: opts.chunkedUpload.chunkSize,
|
|
76
|
+
owner,
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
79
|
+
return {
|
|
80
|
+
catalogue,
|
|
81
|
+
token,
|
|
82
|
+
mediaUrls,
|
|
83
|
+
fileUrls,
|
|
84
|
+
videoSource,
|
|
85
|
+
photoSource,
|
|
86
|
+
mediaSource,
|
|
87
|
+
registry,
|
|
88
|
+
unlock,
|
|
89
|
+
chunked,
|
|
90
|
+
mount(app, auth) {
|
|
91
|
+
if (unlock && !auth.authorizePrivate) {
|
|
92
|
+
throw new Error('createFilesStack: privateMedia is configured but mount() got no authorizePrivate — ' +
|
|
93
|
+
'the private routes need a session→{userId, userSalt} resolver.');
|
|
94
|
+
}
|
|
95
|
+
mountMediaRoutes(app, {
|
|
96
|
+
store,
|
|
97
|
+
token,
|
|
98
|
+
basePath: mediaBasePath,
|
|
99
|
+
registry,
|
|
100
|
+
authorizeUpload: auth.authorizeUpload,
|
|
101
|
+
});
|
|
102
|
+
mountFileRoutes(app, {
|
|
103
|
+
store,
|
|
104
|
+
token,
|
|
105
|
+
basePath: filesBasePath,
|
|
106
|
+
catalogue,
|
|
107
|
+
authorizeUpload: auth.authorizeUpload,
|
|
108
|
+
// Prefer a mount-time override, else the service the stack built from `chunkedUpload` opts.
|
|
109
|
+
chunked: auth.mintChunkUpload ?? chunked ?? undefined,
|
|
110
|
+
privateUpload: unlock && auth.authorizePrivate
|
|
111
|
+
? {
|
|
112
|
+
unlock,
|
|
113
|
+
authorize: auth.authorizePrivate,
|
|
114
|
+
passwordHeader: auth.passwordHeader,
|
|
115
|
+
tokenHeader: auth.tokenHeader,
|
|
116
|
+
passwordPolicy: opts.privateMedia?.passwordPolicy,
|
|
117
|
+
}
|
|
118
|
+
: undefined,
|
|
119
|
+
});
|
|
120
|
+
if (unlock && auth.authorizePrivate) {
|
|
121
|
+
mountPrivateRoutes(app, {
|
|
122
|
+
store,
|
|
123
|
+
index: catalogue.asPrivateIndex(),
|
|
124
|
+
unlock,
|
|
125
|
+
basePath: opts.privateBasePath,
|
|
126
|
+
authorize: auth.authorizePrivate,
|
|
127
|
+
passwordPolicy: opts.privateMedia?.passwordPolicy,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type { MediaStore } from '../types';
|
|
3
|
+
export type StorageAction = 'upload' | 'delete' | 'delete_prefix';
|
|
4
|
+
export type StorageOpStatus = 'pending' | 'done' | 'failed';
|
|
5
|
+
export type StorageOpRow = {
|
|
6
|
+
opId: string;
|
|
7
|
+
owner: string;
|
|
8
|
+
fileId: string | null;
|
|
9
|
+
action: StorageAction;
|
|
10
|
+
key: string;
|
|
11
|
+
backend: string;
|
|
12
|
+
status: StorageOpStatus;
|
|
13
|
+
attempts: number;
|
|
14
|
+
error: string | null;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
};
|
|
18
|
+
export interface StorageOpsLedger {
|
|
19
|
+
/** Record a pending op and return its id (caller may pass one, e.g. to share with binary-server
|
|
20
|
+
* as `x-op-id`; otherwise a uuid is minted). */
|
|
21
|
+
record(op: {
|
|
22
|
+
owner: string;
|
|
23
|
+
fileId?: string | null;
|
|
24
|
+
action: StorageAction;
|
|
25
|
+
key: string;
|
|
26
|
+
backend: string;
|
|
27
|
+
opId?: string;
|
|
28
|
+
}): string;
|
|
29
|
+
markDone(opId: string): void;
|
|
30
|
+
markFailed(opId: string, error: string): void;
|
|
31
|
+
get(opId: string): StorageOpRow | null;
|
|
32
|
+
/** Ops still needing work: `pending`, or `failed` under the attempt cap, created before `before`
|
|
33
|
+
* (default: everything). Oldest first. */
|
|
34
|
+
outstanding(opts?: {
|
|
35
|
+
maxAttempts?: number;
|
|
36
|
+
before?: number;
|
|
37
|
+
}): StorageOpRow[];
|
|
38
|
+
}
|
|
39
|
+
export declare function createStorageOpsLedger(db: Database, now?: () => number): StorageOpsLedger;
|
|
40
|
+
export declare function createLedgeredDeletes(store: MediaStore, ledger: StorageOpsLedger, backend: string): Pick<MediaStore, 'remove' | 'removePrefix'> & {
|
|
41
|
+
removeFor(owner: string, fileId: string | null, key: string): Promise<void>;
|
|
42
|
+
removePrefixFor(owner: string, fileId: string | null, prefix: string): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
export interface StorageReconciler {
|
|
45
|
+
/** Re-drive every outstanding op to completion (idempotent). Returns how many healed vs. still
|
|
46
|
+
* failing. Safe to call on boot and on an interval. */
|
|
47
|
+
reconcile(opts?: {
|
|
48
|
+
maxAttempts?: number;
|
|
49
|
+
}): Promise<{
|
|
50
|
+
healed: number;
|
|
51
|
+
failed: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createStorageReconciler(store: MediaStore, ledger: StorageOpsLedger): StorageReconciler;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const toRow = (r) => ({
|
|
2
|
+
opId: r.op_id,
|
|
3
|
+
owner: r.owner,
|
|
4
|
+
fileId: r.file_id,
|
|
5
|
+
action: r.action,
|
|
6
|
+
key: r.key,
|
|
7
|
+
backend: r.backend,
|
|
8
|
+
status: r.status,
|
|
9
|
+
attempts: r.attempts,
|
|
10
|
+
error: r.error,
|
|
11
|
+
createdAt: r.created_at,
|
|
12
|
+
updatedAt: r.updated_at,
|
|
13
|
+
});
|
|
14
|
+
export function createStorageOpsLedger(db, now = () => Date.now()) {
|
|
15
|
+
const insert = db.query(`INSERT INTO storage_operations (op_id, owner, file_id, action, key, backend, status, attempts, created_at, updated_at)
|
|
16
|
+
VALUES ($op, $owner, $file, $action, $key, $backend, 'pending', 0, $now, $now)`);
|
|
17
|
+
// MONOTONIC terminal state: once an op is `done` it can never flip back (a duplicate completion
|
|
18
|
+
// webhook racing the reconciler, or a late failure, must not corrupt a settled success). `failed`
|
|
19
|
+
// is NOT terminal — the reconciler retries a failed delete and can heal it to `done`, or bump its
|
|
20
|
+
// attempts — so both writes guard only against overwriting `done`. (Ported from ca's resolveOp
|
|
21
|
+
// `CASE WHEN status='pending'` invariant, adapted to our two-writer model.)
|
|
22
|
+
const setDone = db.query(`UPDATE storage_operations SET status = 'done', updated_at = $now WHERE op_id = $op AND status != 'done'`);
|
|
23
|
+
const setFailed = db.query(`UPDATE storage_operations SET status = 'failed', attempts = attempts + 1, error = $error, updated_at = $now WHERE op_id = $op AND status != 'done'`);
|
|
24
|
+
const getOne = db.query(`SELECT * FROM storage_operations WHERE op_id = $op`);
|
|
25
|
+
return {
|
|
26
|
+
record(op) {
|
|
27
|
+
const opId = op.opId ?? crypto.randomUUID();
|
|
28
|
+
insert.run({
|
|
29
|
+
$op: opId,
|
|
30
|
+
$owner: op.owner,
|
|
31
|
+
$file: op.fileId ?? null,
|
|
32
|
+
$action: op.action,
|
|
33
|
+
$key: op.key,
|
|
34
|
+
$backend: op.backend,
|
|
35
|
+
$now: now(),
|
|
36
|
+
});
|
|
37
|
+
return opId;
|
|
38
|
+
},
|
|
39
|
+
markDone(opId) {
|
|
40
|
+
setDone.run({ $op: opId, $now: now() });
|
|
41
|
+
},
|
|
42
|
+
markFailed(opId, error) {
|
|
43
|
+
setFailed.run({ $op: opId, $error: error.slice(0, 500), $now: now() });
|
|
44
|
+
},
|
|
45
|
+
get(opId) {
|
|
46
|
+
const r = getOne.get({ $op: opId });
|
|
47
|
+
return r ? toRow(r) : null;
|
|
48
|
+
},
|
|
49
|
+
outstanding(opts = {}) {
|
|
50
|
+
const maxAttempts = opts.maxAttempts ?? 5;
|
|
51
|
+
const before = opts.before ?? now() + 1;
|
|
52
|
+
return db
|
|
53
|
+
.query(`SELECT * FROM storage_operations
|
|
54
|
+
WHERE status != 'done' AND attempts < $max AND created_at < $before
|
|
55
|
+
ORDER BY created_at ASC`)
|
|
56
|
+
.all({ $max: maxAttempts, $before: before })
|
|
57
|
+
.map(toRow);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Wrap a MediaStore so DELETE / DELETE-PREFIX are ledgered: a `pending` row is written first, the
|
|
62
|
+
// op attempted, then marked `done`; a failure marks it `failed` and rethrows (the row survives for
|
|
63
|
+
// the reconciler). Reads/writes pass straight through — only the leak-prone deletes are logged.
|
|
64
|
+
export function createLedgeredDeletes(store, ledger, backend) {
|
|
65
|
+
const run = async (action, owner, fileId, key, exec) => {
|
|
66
|
+
const opId = ledger.record({ owner, fileId, action, key, backend });
|
|
67
|
+
try {
|
|
68
|
+
await exec();
|
|
69
|
+
ledger.markDone(opId);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
ledger.markFailed(opId, e instanceof Error ? e.message : String(e));
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
remove: (key) => run('delete', '', null, key, () => store.remove(key)),
|
|
78
|
+
removePrefix: (prefix) => run('delete_prefix', '', null, prefix, () => store.removePrefix(prefix)),
|
|
79
|
+
removeFor: (owner, fileId, key) => run('delete', owner, fileId, key, () => store.remove(key)),
|
|
80
|
+
removePrefixFor: (owner, fileId, prefix) => run('delete_prefix', owner, fileId, prefix, () => store.removePrefix(prefix)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function createStorageReconciler(store, ledger) {
|
|
84
|
+
return {
|
|
85
|
+
async reconcile(opts = {}) {
|
|
86
|
+
let healed = 0;
|
|
87
|
+
let failed = 0;
|
|
88
|
+
for (const op of ledger.outstanding({ maxAttempts: opts.maxAttempts })) {
|
|
89
|
+
// Only DELETES are re-driven by re-issuing the store call (idempotent). An 'upload' op is
|
|
90
|
+
// healed by its completion webhook (or an app-level check against binary-server's own event
|
|
91
|
+
// ledger) — re-issuing store.remove here would wrongly delete the just-uploaded bytes.
|
|
92
|
+
if (op.action === 'upload')
|
|
93
|
+
continue;
|
|
94
|
+
try {
|
|
95
|
+
if (op.action === 'delete')
|
|
96
|
+
await store.remove(op.key);
|
|
97
|
+
else
|
|
98
|
+
await store.removePrefix(op.key);
|
|
99
|
+
ledger.markDone(op.opId);
|
|
100
|
+
healed++;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
ledger.markFailed(op.opId, e instanceof Error ? e.message : String(e));
|
|
104
|
+
failed++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { healed, failed };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|