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,106 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { derivePasswordSetKey, generateUserSalt, openMeta } from '../../private-media/crypto';
|
|
8
|
+
import type { PrivateIndex } from '../../private-media/index-store';
|
|
9
|
+
import { createDiskStore } from '../diskStore';
|
|
10
|
+
import type { MediaCtx } from '../types';
|
|
11
|
+
import { createSqliteFileCatalogue } from './catalogue';
|
|
12
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
13
|
+
import type { PrivateFileCatalogue } from './types';
|
|
14
|
+
|
|
15
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 't1' };
|
|
16
|
+
const stream = (s: string) =>
|
|
17
|
+
new Response(new TextEncoder().encode(s)).body as ReadableStream<Uint8Array>;
|
|
18
|
+
const OWNER = CTX.userId; // private owner axis = userId (matches mountPrivateRoutes)
|
|
19
|
+
|
|
20
|
+
// Seal/reveal/deniability is ffmpeg-independent: the descriptor is sealed regardless of transcode
|
|
21
|
+
// success, so these run everywhere. (Content-key unwrap needs a real transcode → ffmpeg-gated e2e.)
|
|
22
|
+
describe('FileCatalogue private media — ingest + PrivateIndex view + deniability', () => {
|
|
23
|
+
let db: Database;
|
|
24
|
+
let root: string;
|
|
25
|
+
let cat: PrivateFileCatalogue;
|
|
26
|
+
let index: PrivateIndex;
|
|
27
|
+
const salt = generateUserSalt();
|
|
28
|
+
const keyA = derivePasswordSetKey('alpha-pass', salt);
|
|
29
|
+
const keyB = derivePasswordSetKey('bravo-pass', salt);
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
db = new Database(':memory:');
|
|
33
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
34
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-priv-'));
|
|
35
|
+
let clock = 1;
|
|
36
|
+
// owner = userId so private rows + mountPrivateRoutes agree on the scope.
|
|
37
|
+
cat = createSqliteFileCatalogue({
|
|
38
|
+
db,
|
|
39
|
+
store: createDiskStore(root),
|
|
40
|
+
workDir: path.join(root, 'w'),
|
|
41
|
+
owner: (c) => c.userId,
|
|
42
|
+
now: () => ++clock,
|
|
43
|
+
});
|
|
44
|
+
index = cat.asPrivateIndex();
|
|
45
|
+
});
|
|
46
|
+
afterAll(async () => {
|
|
47
|
+
db.close();
|
|
48
|
+
await rm(root, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('ingests private rows under two disjoint passwords', async () => {
|
|
52
|
+
await cat.ingestPrivate(
|
|
53
|
+
CTX,
|
|
54
|
+
{ name: 'secret-a.mp4', mime: 'video/mp4', body: stream('a-bytes'), tags: ['setA'] },
|
|
55
|
+
{ setKey: keyA, keyMode: 'deniable' },
|
|
56
|
+
);
|
|
57
|
+
await cat.ingestPrivate(
|
|
58
|
+
CTX,
|
|
59
|
+
{ name: 'secret-b.mp4', mime: 'video/mp4', body: stream('b-bytes'), tags: ['setB'] },
|
|
60
|
+
{ setKey: keyB, keyMode: 'deniable' },
|
|
61
|
+
);
|
|
62
|
+
expect((await index.list(OWNER)).length).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a set key reveals only its own items (deniability) via the index envelopes', () => {
|
|
66
|
+
const reveal = async (key: Buffer) => {
|
|
67
|
+
const names: string[] = [];
|
|
68
|
+
for (const item of await index.list(OWNER)) {
|
|
69
|
+
const meta = openMeta<{ name: string }>(key, item.sealedMeta);
|
|
70
|
+
if (meta) names.push(meta.name);
|
|
71
|
+
}
|
|
72
|
+
return names.sort();
|
|
73
|
+
};
|
|
74
|
+
return Promise.all([
|
|
75
|
+
reveal(keyA),
|
|
76
|
+
reveal(keyB),
|
|
77
|
+
reveal(derivePasswordSetKey('nope', salt)),
|
|
78
|
+
]).then(([a, b, wrong]) => {
|
|
79
|
+
expect(a).toEqual(['secret-a.mp4']);
|
|
80
|
+
expect(b).toEqual(['secret-b.mp4']);
|
|
81
|
+
expect(wrong).toEqual([]); // an unused password shows nothing — no hint the sets exist
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the clear index exposes no set identifier or key mode difference', async () => {
|
|
86
|
+
// Both rows are keyMode 'deniable' here; the public projection (toPublic) that the /index route
|
|
87
|
+
// returns strips keyMode entirely — asserted by mountPrivateRoutes' own tests. Here we just
|
|
88
|
+
// confirm the raw items carry the wrapped key + sealed meta (opaque without a key).
|
|
89
|
+
for (const item of await index.list(OWNER)) {
|
|
90
|
+
expect(item.sealedMeta.length).toBeGreaterThan(0);
|
|
91
|
+
expect(openMeta(Buffer.alloc(32), item.sealedMeta)).toBeNull(); // a random key opens nothing
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('index.remove drops the row', async () => {
|
|
96
|
+
const items = await index.list(OWNER);
|
|
97
|
+
await index.remove(OWNER, items[0].id);
|
|
98
|
+
expect((await index.list(OWNER)).length).toBe(1);
|
|
99
|
+
expect(await index.get(OWNER, items[0].id)).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('private rows never appear in the public feed', async () => {
|
|
103
|
+
const publicCat = cat as unknown as { list: (c: MediaCtx) => Promise<{ items: unknown[] }> };
|
|
104
|
+
expect((await publicCat.list(CTX)).items).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { createDiskStore } from '../diskStore';
|
|
8
|
+
import { createSqliteFileCatalogue } from './catalogue';
|
|
9
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
10
|
+
import type { FileCatalogue } from './types';
|
|
11
|
+
|
|
12
|
+
// recoverProcessing (ported from ca's recoverOnBoot): a restart mid-transcode leaves video rows
|
|
13
|
+
// stuck in processing/pending; public rows get re-kicked (source persists), private rows are failed.
|
|
14
|
+
describe('FileCatalogue.recoverProcessing', () => {
|
|
15
|
+
let db: Database;
|
|
16
|
+
let root: string;
|
|
17
|
+
let cat: FileCatalogue;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
db = new Database(':memory:');
|
|
21
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
22
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-recover-'));
|
|
23
|
+
cat = createSqliteFileCatalogue({
|
|
24
|
+
db,
|
|
25
|
+
store: createDiskStore(root),
|
|
26
|
+
workDir: path.join(root, 'w'),
|
|
27
|
+
});
|
|
28
|
+
// Simulate two rows a crash left mid-transcode: one public, one private.
|
|
29
|
+
const insert = db.query(
|
|
30
|
+
`INSERT INTO files (id, owner, name, mime, size, file_type, is_private, processing_status, processing_progress, storage_backend, created_at, updated_at)
|
|
31
|
+
VALUES ($id, 'u1', $name, 'video/mp4', 10, 'video', $priv, 'processing', 40, 'disk', 1, 1)`,
|
|
32
|
+
);
|
|
33
|
+
insert.run({ $id: 'pub1', $name: 'a.mp4', $priv: 0 });
|
|
34
|
+
insert.run({ $id: 'prv1', $name: '', $priv: 1 });
|
|
35
|
+
});
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await Bun.sleep(150); // let the re-kicked public job settle before closing the db
|
|
38
|
+
db.close();
|
|
39
|
+
await rm(root, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('re-queues stuck public videos and fails stuck private ones', () => {
|
|
43
|
+
const res = cat.recoverProcessing();
|
|
44
|
+
expect(res.requeued).toBe(1); // pub1 re-kicked
|
|
45
|
+
expect(res.failed).toBe(1); // prv1 can't be re-kicked at boot → failed
|
|
46
|
+
|
|
47
|
+
const status = (id: string) =>
|
|
48
|
+
db
|
|
49
|
+
.query<{ processing_status: string }, { $id: string }>(
|
|
50
|
+
'SELECT processing_status FROM files WHERE id = $id',
|
|
51
|
+
)
|
|
52
|
+
.get({ $id: id })?.processing_status;
|
|
53
|
+
expect(status('prv1')).toBe('failed');
|
|
54
|
+
// pub1 was re-kicked (its final status is settled by the async pipeline; here we just assert it
|
|
55
|
+
// left the recover call counted as requeued, not failed).
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('re-kick bumps processing_attempts', () => {
|
|
59
|
+
const attempts = db
|
|
60
|
+
.query<{ processing_attempts: number }, { $id: string }>(
|
|
61
|
+
'SELECT processing_attempts FROM files WHERE id = $id',
|
|
62
|
+
)
|
|
63
|
+
.get({ $id: 'pub1' })?.processing_attempts;
|
|
64
|
+
expect(attempts).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// The boot-loop guard: when the transcode itself keeps killing the process (ffmpeg OOM →
|
|
69
|
+
// restart → re-kick), the attempt cap must land the row on `failed` instead of re-kicking forever.
|
|
70
|
+
describe('FileCatalogue.recoverProcessing attempt cap', () => {
|
|
71
|
+
test('marks a public video failed once processing_attempts reaches the cap', async () => {
|
|
72
|
+
const db = new Database(':memory:');
|
|
73
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
74
|
+
const root = await mkdtemp(path.join(tmpdir(), 'fc-recover-cap-'));
|
|
75
|
+
const cat = createSqliteFileCatalogue({
|
|
76
|
+
db,
|
|
77
|
+
store: createDiskStore(root),
|
|
78
|
+
workDir: path.join(root, 'w'),
|
|
79
|
+
maxProcessingAttempts: 2,
|
|
80
|
+
});
|
|
81
|
+
db.query(
|
|
82
|
+
`INSERT INTO files (id, owner, name, mime, size, file_type, is_private, processing_status, processing_progress, storage_backend, processing_attempts, created_at, updated_at)
|
|
83
|
+
VALUES ('loop1', 'u1', 'l.mp4', 'video/mp4', 10, 'video', 0, 'processing', 40, 'disk', 2, 1, 1)`,
|
|
84
|
+
).run();
|
|
85
|
+
|
|
86
|
+
const res = cat.recoverProcessing();
|
|
87
|
+
expect(res.requeued).toBe(0);
|
|
88
|
+
expect(res.failed).toBe(1);
|
|
89
|
+
const row = db
|
|
90
|
+
.query<{ processing_status: string }, []>(
|
|
91
|
+
`SELECT processing_status FROM files WHERE id = 'loop1'`,
|
|
92
|
+
)
|
|
93
|
+
.get();
|
|
94
|
+
expect(row?.processing_status).toBe('failed');
|
|
95
|
+
db.close();
|
|
96
|
+
await rm(root, { recursive: true, force: true });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// transcodeVideos: false — the memory-constrained-host mode: no ffmpeg, videos go straight to
|
|
101
|
+
// ready and play raw. Recovery under this mode must not re-enter the transcode pipeline either.
|
|
102
|
+
describe('FileCatalogue transcodeVideos: false', () => {
|
|
103
|
+
test('video rows go ready with no derivatives and no attempt bump', async () => {
|
|
104
|
+
const db = new Database(':memory:');
|
|
105
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
106
|
+
const root = await mkdtemp(path.join(tmpdir(), 'fc-notranscode-'));
|
|
107
|
+
const cat = createSqliteFileCatalogue({
|
|
108
|
+
db,
|
|
109
|
+
store: createDiskStore(root),
|
|
110
|
+
workDir: path.join(root, 'w'),
|
|
111
|
+
transcodeVideos: false,
|
|
112
|
+
});
|
|
113
|
+
db.query(
|
|
114
|
+
`INSERT INTO files (id, owner, name, mime, size, file_type, is_private, processing_status, processing_progress, storage_backend, created_at, updated_at)
|
|
115
|
+
VALUES ('raw1', 'u1', 'r.mp4', 'video/mp4', 10, 'video', 0, 'processing', 40, 'disk', 1, 1)`,
|
|
116
|
+
).run();
|
|
117
|
+
|
|
118
|
+
const res = cat.recoverProcessing();
|
|
119
|
+
expect(res.requeued).toBe(1); // counted as requeued, but the "kick" resolves synchronously
|
|
120
|
+
const row = db
|
|
121
|
+
.query<{ processing_status: string; processing_attempts: number; has_hls: number }, []>(
|
|
122
|
+
`SELECT processing_status, processing_attempts, has_hls FROM files WHERE id = 'raw1'`,
|
|
123
|
+
)
|
|
124
|
+
.get();
|
|
125
|
+
expect(row?.processing_status).toBe('ready');
|
|
126
|
+
expect(row?.processing_attempts).toBe(0);
|
|
127
|
+
expect(row?.has_hls).toBe(0);
|
|
128
|
+
db.close();
|
|
129
|
+
await rm(root, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { createDiskStore } from '../diskStore';
|
|
8
|
+
import type { MediaCtx } from '../types';
|
|
9
|
+
import { createSqliteFileCatalogue } from './catalogue';
|
|
10
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
11
|
+
import type { FileCatalogue, FileSort } from './types';
|
|
12
|
+
|
|
13
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 'sorter' };
|
|
14
|
+
|
|
15
|
+
describe('FileCatalogue — sort orders + pagination', () => {
|
|
16
|
+
let db: Database;
|
|
17
|
+
let root: string;
|
|
18
|
+
let cat: FileCatalogue;
|
|
19
|
+
let clock = 5000;
|
|
20
|
+
|
|
21
|
+
// Names/sizes chosen so each sort produces a DIFFERENT order, with a duplicate size to
|
|
22
|
+
// exercise the id tie-break. Bodies of length n ⇒ size n.
|
|
23
|
+
const FILES: Array<{ name: string; size: number }> = [
|
|
24
|
+
{ name: 'delta.png', size: 10 },
|
|
25
|
+
{ name: 'alpha.png', size: 30 },
|
|
26
|
+
{ name: 'echo.png', size: 20 },
|
|
27
|
+
{ name: 'bravo.png', size: 20 }, // duplicate size — tie-break path
|
|
28
|
+
{ name: 'charlie.png', size: 50 },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
db = new Database(':memory:');
|
|
33
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
34
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-sort-'));
|
|
35
|
+
cat = createSqliteFileCatalogue({
|
|
36
|
+
db,
|
|
37
|
+
store: createDiskStore(root),
|
|
38
|
+
workDir: path.join(root, 'work'),
|
|
39
|
+
now: () => ++clock,
|
|
40
|
+
});
|
|
41
|
+
for (const f of FILES) {
|
|
42
|
+
const body = new Response('x'.repeat(f.size)).body as ReadableStream<Uint8Array>;
|
|
43
|
+
await cat.ingest(CTX, { name: f.name, mime: 'image/png', body });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
afterAll(async () => {
|
|
47
|
+
db.close();
|
|
48
|
+
await rm(root, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("default / 'recent' sorts newest-first (unchanged behavior)", async () => {
|
|
52
|
+
const expected = [...FILES].reverse().map((f) => f.name); // insertion order reversed
|
|
53
|
+
expect((await cat.list(CTX)).items.map((f) => f.name)).toEqual(expected);
|
|
54
|
+
expect((await cat.list(CTX, { sort: 'recent' })).items.map((f) => f.name)).toEqual(expected);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("'oldest' sorts oldest-first (insertion order)", async () => {
|
|
58
|
+
const page = await cat.list(CTX, { sort: 'oldest' });
|
|
59
|
+
expect(page.items.map((f) => f.name)).toEqual(FILES.map((f) => f.name));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("'name' sorts A→Z", async () => {
|
|
63
|
+
const page = await cat.list(CTX, { sort: 'name' });
|
|
64
|
+
expect(page.items.map((f) => f.name)).toEqual([
|
|
65
|
+
'alpha.png',
|
|
66
|
+
'bravo.png',
|
|
67
|
+
'charlie.png',
|
|
68
|
+
'delta.png',
|
|
69
|
+
'echo.png',
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("'size' sorts largest-first", async () => {
|
|
74
|
+
const page = await cat.list(CTX, { sort: 'size' });
|
|
75
|
+
expect(page.items.map((f) => f.size)).toEqual([50, 30, 20, 20, 10]);
|
|
76
|
+
expect(page.items[0].name).toBe('charlie.png');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Walk the whole pool page-by-page under a sort; assert no gaps, no repeats, and that the
|
|
80
|
+
// concatenated pages equal the single-page order.
|
|
81
|
+
const paginate = async (sort: FileSort, limit: number): Promise<string[]> => {
|
|
82
|
+
const all: string[] = [];
|
|
83
|
+
const seen = new Set<string>();
|
|
84
|
+
let cursor: string | undefined;
|
|
85
|
+
let guard = 0;
|
|
86
|
+
do {
|
|
87
|
+
const page = await cat.list(CTX, { sort, limit, cursor });
|
|
88
|
+
for (const f of page.items) {
|
|
89
|
+
expect(seen.has(f.id)).toBe(false); // no repeats across pages
|
|
90
|
+
seen.add(f.id);
|
|
91
|
+
all.push(f.name);
|
|
92
|
+
}
|
|
93
|
+
cursor = page.nextCursor;
|
|
94
|
+
guard++;
|
|
95
|
+
} while (cursor && guard < 10);
|
|
96
|
+
return all;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
test.each([
|
|
100
|
+
'recent',
|
|
101
|
+
'oldest',
|
|
102
|
+
'name',
|
|
103
|
+
'size',
|
|
104
|
+
] as const)("cursor pagination under sort '%s' walks the pool without gaps or repeats", async (sort) => {
|
|
105
|
+
const whole = (await cat.list(CTX, { sort })).items.map((f) => f.name);
|
|
106
|
+
const paged = await paginate(sort, 2);
|
|
107
|
+
expect(paged).toEqual(whole); // page-by-page equals the one-shot order
|
|
108
|
+
expect(paged.length).toBe(FILES.length);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('size pagination splits the duplicate-size pair across pages correctly (tie-break)', async () => {
|
|
112
|
+
// limit 3 puts the boundary INSIDE the size=20 duplicate pair.
|
|
113
|
+
const p1 = await cat.list(CTX, { sort: 'size', limit: 3 });
|
|
114
|
+
expect(p1.items.map((f) => f.size)).toEqual([50, 30, 20]);
|
|
115
|
+
expect(p1.nextCursor).toBeDefined();
|
|
116
|
+
const p2 = await cat.list(CTX, { sort: 'size', limit: 3, cursor: p1.nextCursor });
|
|
117
|
+
expect(p2.items.map((f) => f.size)).toEqual([20, 10]);
|
|
118
|
+
// The two size-20 files are different files.
|
|
119
|
+
expect(p1.items[2].id).not.toBe(p2.items[0].id);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a name containing the cursor separator paginates safely', async () => {
|
|
123
|
+
const CTX2: MediaCtx = { userId: 'u2', tenantId: 'sorter2' };
|
|
124
|
+
const mk = (name: string) =>
|
|
125
|
+
cat.ingest(CTX2, {
|
|
126
|
+
name,
|
|
127
|
+
mime: 'image/png',
|
|
128
|
+
body: new Response('x').body as ReadableStream<Uint8Array>,
|
|
129
|
+
});
|
|
130
|
+
await mk('a|weird|name.png');
|
|
131
|
+
await mk('b|also|weird.png');
|
|
132
|
+
await mk('c.png');
|
|
133
|
+
const p1 = await cat.list(CTX2, { sort: 'name', limit: 1 });
|
|
134
|
+
expect(p1.items[0].name).toBe('a|weird|name.png');
|
|
135
|
+
const p2 = await cat.list(CTX2, { sort: 'name', limit: 1, cursor: p1.nextCursor });
|
|
136
|
+
expect(p2.items[0].name).toBe('b|also|weird.png');
|
|
137
|
+
const p3 = await cat.list(CTX2, { sort: 'name', limit: 1, cursor: p2.nextCursor });
|
|
138
|
+
expect(p3.items[0].name).toBe('c.png');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('sort composes with filters (collection + sort)', async () => {
|
|
142
|
+
const coll = await cat.createCollection(CTX, { name: 'SortColl' });
|
|
143
|
+
const all = await cat.list(CTX);
|
|
144
|
+
await cat.addToCollection(
|
|
145
|
+
CTX,
|
|
146
|
+
coll.id,
|
|
147
|
+
all.items.slice(0, 3).map((f) => f.id),
|
|
148
|
+
);
|
|
149
|
+
const page = await cat.list(CTX, { collectionId: coll.id, sort: 'name' });
|
|
150
|
+
const names = page.items.map((f) => f.name);
|
|
151
|
+
expect(names).toEqual([...names].sort());
|
|
152
|
+
expect(names.length).toBe(3);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { createDiskStore } from '../diskStore';
|
|
8
|
+
import type { MediaCtx } from '../types';
|
|
9
|
+
import { fsPosterKey } from '../types';
|
|
10
|
+
import { createSqliteFileCatalogue, fileBinaryKey } from './catalogue';
|
|
11
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
12
|
+
import type { FileCatalogue } from './types';
|
|
13
|
+
|
|
14
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 't1' };
|
|
15
|
+
const bytes = (s: string) => new TextEncoder().encode(s);
|
|
16
|
+
const stream = (s: string) => new Response(bytes(s)).body as ReadableStream<Uint8Array>;
|
|
17
|
+
|
|
18
|
+
describe('FileCatalogue (SQLite)', () => {
|
|
19
|
+
let db: Database;
|
|
20
|
+
let root: string;
|
|
21
|
+
let cat: FileCatalogue;
|
|
22
|
+
let store: ReturnType<typeof createDiskStore>;
|
|
23
|
+
let clock = 1000;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
db = new Database(':memory:');
|
|
27
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
28
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-store-'));
|
|
29
|
+
store = createDiskStore(root);
|
|
30
|
+
cat = createSqliteFileCatalogue({
|
|
31
|
+
db,
|
|
32
|
+
store,
|
|
33
|
+
workDir: path.join(root, 'work'),
|
|
34
|
+
now: () => ++clock, // deterministic, increasing createdAt
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
afterAll(async () => {
|
|
38
|
+
db.close();
|
|
39
|
+
await rm(root, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ingest = (name: string, mime: string, tags: string[] = []) =>
|
|
43
|
+
cat.ingest(CTX, { name, mime, body: stream(`bytes-of-${name}`), tags });
|
|
44
|
+
|
|
45
|
+
test('ingests non-video files as ready, classifies + tags them', async () => {
|
|
46
|
+
const a = await ingest('a.png', 'image/png', ['photo']);
|
|
47
|
+
expect(a.fileType).toBe('image');
|
|
48
|
+
expect(a.processingStatus).toBe('ready');
|
|
49
|
+
expect(a.tags).toEqual(['photo']);
|
|
50
|
+
expect(a.size).toBe(bytes('bytes-of-a.png').length);
|
|
51
|
+
|
|
52
|
+
await ingest('b.pdf', 'application/pdf', ['work']);
|
|
53
|
+
await ingest('c.jpg', 'image/jpeg', ['photo', 'work']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('lists newest-first with smart-view + tag + query filters', async () => {
|
|
57
|
+
const all = await cat.list(CTX);
|
|
58
|
+
expect(all.items.map((f) => f.name)).toEqual(['c.jpg', 'b.pdf', 'a.png']); // newest first
|
|
59
|
+
|
|
60
|
+
expect((await cat.list(CTX, { type: 'image' })).items.map((f) => f.name)).toEqual([
|
|
61
|
+
'c.jpg',
|
|
62
|
+
'a.png',
|
|
63
|
+
]);
|
|
64
|
+
expect((await cat.list(CTX, { type: 'doc' })).items.map((f) => f.name)).toEqual(['b.pdf']);
|
|
65
|
+
expect((await cat.list(CTX, { tag: 'photo' })).items.map((f) => f.name)).toEqual([
|
|
66
|
+
'c.jpg',
|
|
67
|
+
'a.png',
|
|
68
|
+
]);
|
|
69
|
+
expect((await cat.list(CTX, { query: 'b.' })).items.map((f) => f.name)).toEqual(['b.pdf']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('tagFacets counts tags across the pool', async () => {
|
|
73
|
+
const facets = await cat.tagFacets(CTX);
|
|
74
|
+
expect(facets).toContainEqual({ tag: 'photo', count: 2 });
|
|
75
|
+
expect(facets).toContainEqual({ tag: 'work', count: 2 });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('cursor pagination walks the whole pool without gaps or repeats', async () => {
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
let cursor: string | undefined;
|
|
81
|
+
let pages = 0;
|
|
82
|
+
do {
|
|
83
|
+
const page = await cat.list(CTX, { limit: 2, cursor });
|
|
84
|
+
for (const f of page.items) {
|
|
85
|
+
expect(seen.has(f.id)).toBe(false); // no repeats
|
|
86
|
+
seen.add(f.id);
|
|
87
|
+
}
|
|
88
|
+
cursor = page.nextCursor;
|
|
89
|
+
pages++;
|
|
90
|
+
} while (cursor && pages < 10);
|
|
91
|
+
expect(seen.size).toBe(3); // all files, paged 2 at a time
|
|
92
|
+
expect(pages).toBe(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('get + update (rename + retag) + remove', async () => {
|
|
96
|
+
const { items } = await cat.list(CTX, { type: 'doc' });
|
|
97
|
+
const id = items[0].id;
|
|
98
|
+
expect((await cat.get(CTX, id))?.name).toBe('b.pdf');
|
|
99
|
+
|
|
100
|
+
const updated = await cat.update(CTX, id, { name: 'renamed.pdf', tags: ['archive'] });
|
|
101
|
+
expect(updated?.name).toBe('renamed.pdf');
|
|
102
|
+
expect(updated?.tags).toEqual(['archive']);
|
|
103
|
+
expect((await cat.tagFacets(CTX)).find((f) => f.tag === 'work')?.count).toBe(1); // 'work' dropped from b
|
|
104
|
+
|
|
105
|
+
await cat.remove(CTX, id);
|
|
106
|
+
expect(await cat.get(CTX, id)).toBeNull();
|
|
107
|
+
expect((await cat.list(CTX)).items.length).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('remove deletes an image’s binary AND its poster thumbnail from the store', async () => {
|
|
111
|
+
const img = await ingest('orphan.png', 'image/png');
|
|
112
|
+
const owner = CTX.tenantId; // catalogue's default ownerOf
|
|
113
|
+
// Simulate the thumbnail the async kickImageThumbnail pipeline writes.
|
|
114
|
+
await store.put(fsPosterKey(owner, img.id), bytes('thumb'), 'image/jpeg');
|
|
115
|
+
expect(await store.stat(fileBinaryKey(owner, img.id))).not.toBeNull();
|
|
116
|
+
|
|
117
|
+
await cat.remove(CTX, img.id);
|
|
118
|
+
expect(await cat.get(CTX, img.id)).toBeNull();
|
|
119
|
+
expect(await store.stat(fileBinaryKey(owner, img.id))).toBeNull();
|
|
120
|
+
expect(await store.stat(fsPosterKey(owner, img.id))).toBeNull(); // was orphaned before the fix
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('one tenant never sees another tenant’s files', async () => {
|
|
124
|
+
await cat.ingest(
|
|
125
|
+
{ userId: 'u2', tenantId: 't2' },
|
|
126
|
+
{ name: 'other.png', mime: 'image/png', body: stream('x') },
|
|
127
|
+
);
|
|
128
|
+
expect((await cat.list(CTX)).items.every((f) => f.name !== 'other.png')).toBe(true);
|
|
129
|
+
expect((await cat.list({ userId: 'u2', tenantId: 't2' })).items.map((f) => f.name)).toEqual([
|
|
130
|
+
'other.png',
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { createDiskStore } from '../diskStore';
|
|
8
|
+
import type { MediaCtx } from '../types';
|
|
9
|
+
import { createSqliteFileCatalogue } from './catalogue';
|
|
10
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
11
|
+
import type { FileCatalogue } from './types';
|
|
12
|
+
|
|
13
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 't1' };
|
|
14
|
+
const bytes = (s: string) => new TextEncoder().encode(s);
|
|
15
|
+
const stream = (s: string) => new Response(bytes(s)).body as ReadableStream<Uint8Array>;
|
|
16
|
+
|
|
17
|
+
describe('FileCatalogue trash (soft-delete)', () => {
|
|
18
|
+
let db: Database;
|
|
19
|
+
let root: string;
|
|
20
|
+
let cat: FileCatalogue;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
db = new Database(':memory:');
|
|
24
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
25
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-trash-'));
|
|
26
|
+
cat = createSqliteFileCatalogue({ db, store: createDiskStore(root), workDir: path.join(root, 'work') });
|
|
27
|
+
});
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
db.close();
|
|
30
|
+
await rm(root, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('trash hides a file from list() but keeps its row + bytes', async () => {
|
|
34
|
+
const f = await cat.ingest(CTX, { name: 'a.png', mime: 'image/png', body: stream('a') });
|
|
35
|
+
await cat.trash(CTX, f.id);
|
|
36
|
+
|
|
37
|
+
expect((await cat.list(CTX)).items.some((r) => r.id === f.id)).toBe(false);
|
|
38
|
+
const row = await cat.get(CTX, f.id);
|
|
39
|
+
expect(row?.trashedAt).toBeGreaterThan(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('trashedOnly lists exactly the trashed set', async () => {
|
|
43
|
+
const live = await cat.ingest(CTX, { name: 'live.png', mime: 'image/png', body: stream('l') });
|
|
44
|
+
const gone = await cat.ingest(CTX, { name: 'gone.png', mime: 'image/png', body: stream('g') });
|
|
45
|
+
await cat.trash(CTX, gone.id);
|
|
46
|
+
|
|
47
|
+
const trashPage = await cat.list(CTX, { trashedOnly: true });
|
|
48
|
+
expect(trashPage.items.map((r) => r.id)).toContain(gone.id);
|
|
49
|
+
expect(trashPage.items.map((r) => r.id)).not.toContain(live.id);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('restore undoes trash — the file reappears in list()', async () => {
|
|
53
|
+
const f = await cat.ingest(CTX, { name: 'b.png', mime: 'image/png', body: stream('b') });
|
|
54
|
+
await cat.trash(CTX, f.id);
|
|
55
|
+
await cat.restore(CTX, f.id);
|
|
56
|
+
|
|
57
|
+
expect((await cat.list(CTX)).items.some((r) => r.id === f.id)).toBe(true);
|
|
58
|
+
expect((await cat.get(CTX, f.id))?.trashedAt).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('remove() is still a real hard-delete, independent of trash', async () => {
|
|
62
|
+
const f = await cat.ingest(CTX, { name: 'c.png', mime: 'image/png', body: stream('c') });
|
|
63
|
+
await cat.trash(CTX, f.id);
|
|
64
|
+
await cat.remove(CTX, f.id);
|
|
65
|
+
expect(await cat.get(CTX, f.id)).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('FileCatalogue.importFromPath (local-disk import)', () => {
|
|
70
|
+
let db: Database;
|
|
71
|
+
let root: string;
|
|
72
|
+
let importRoot: string;
|
|
73
|
+
let cat: FileCatalogue;
|
|
74
|
+
|
|
75
|
+
beforeAll(async () => {
|
|
76
|
+
db = new Database(':memory:');
|
|
77
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
78
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-import-store-'));
|
|
79
|
+
importRoot = await mkdtemp(path.join(tmpdir(), 'fc-import-src-'));
|
|
80
|
+
cat = createSqliteFileCatalogue({ db, store: createDiskStore(root), workDir: path.join(root, 'work') });
|
|
81
|
+
});
|
|
82
|
+
afterAll(async () => {
|
|
83
|
+
db.close();
|
|
84
|
+
await rm(root, { recursive: true, force: true });
|
|
85
|
+
await rm(importRoot, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('imports a single file', async () => {
|
|
89
|
+
const filePath = path.join(importRoot, 'single.jpg');
|
|
90
|
+
await writeFile(filePath, bytes('single-file-bytes'));
|
|
91
|
+
|
|
92
|
+
const { imported, skipped } = await cat.importFromPath(CTX, filePath);
|
|
93
|
+
expect(skipped).toEqual([]);
|
|
94
|
+
expect(imported).toHaveLength(1);
|
|
95
|
+
expect(imported[0].name).toBe('single.jpg');
|
|
96
|
+
expect(imported[0].mime).toBe('image/jpeg');
|
|
97
|
+
expect(imported[0].fileType).toBe('image');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('imports a mixed-media folder recursively, tagging by containing folder', async () => {
|
|
101
|
+
const folder = path.join(importRoot, 'vacation');
|
|
102
|
+
const sub = path.join(folder, 'day1');
|
|
103
|
+
await mkdir(sub, { recursive: true });
|
|
104
|
+
await writeFile(path.join(folder, 'cover.png'), bytes('cover'));
|
|
105
|
+
await writeFile(path.join(sub, 'clip.mp4'), bytes('clip'));
|
|
106
|
+
|
|
107
|
+
const { imported, skipped } = await cat.importFromPath(CTX, folder, { tags: ['import-batch'] });
|
|
108
|
+
expect(skipped).toEqual([]);
|
|
109
|
+
expect(imported).toHaveLength(2);
|
|
110
|
+
|
|
111
|
+
const cover = imported.find((f) => f.name === 'cover.png');
|
|
112
|
+
const clip = imported.find((f) => f.name === 'clip.mp4');
|
|
113
|
+
expect(cover?.fileType).toBe('image');
|
|
114
|
+
expect(cover?.tags).toEqual(['import-batch']); // top-level entries get no folder tag
|
|
115
|
+
expect(clip?.fileType).toBe('video');
|
|
116
|
+
expect(clip?.tags).toEqual(['import-batch', 'day1']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('reports a missing path as skipped, not a throw', async () => {
|
|
120
|
+
const { imported, skipped } = await cat.importFromPath(CTX, path.join(importRoot, 'nope.png'));
|
|
121
|
+
expect(imported).toEqual([]);
|
|
122
|
+
expect(skipped).toHaveLength(1);
|
|
123
|
+
expect(skipped[0].path).toContain('nope.png');
|
|
124
|
+
});
|
|
125
|
+
});
|