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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/dropzone` โ the reusable drop-folder scraper.
|
|
3
|
+
*
|
|
4
|
+
* Drop a tree of files into a folder; a sweep walks it, hands each file to the app's handler,
|
|
5
|
+
* deletes only what the handler confirmed was filed somewhere else, and prunes the folders it
|
|
6
|
+
* empties. Nothing app-specific lives here: what a file MEANS is the app's, what happens to a
|
|
7
|
+
* file on disk is this module's.
|
|
8
|
+
*
|
|
9
|
+
* Consumers (2026-08-13): `apps/music`, `apps/family`, `apps/collections` in
|
|
10
|
+
* cursed-satellites. Each supplies a `DropHandler` and nothing else.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
describeSweep,
|
|
14
|
+
isJunkName,
|
|
15
|
+
isStable,
|
|
16
|
+
pruneEmptyDirs,
|
|
17
|
+
scanDropzone,
|
|
18
|
+
sweepDropzone,
|
|
19
|
+
type DropHandler,
|
|
20
|
+
type DropOutcome,
|
|
21
|
+
type DroppedFile,
|
|
22
|
+
type ScanOptions,
|
|
23
|
+
type SweepReport,
|
|
24
|
+
} from "./scan";
|
|
25
|
+
export {
|
|
26
|
+
askBinaryServerAboutDuplicate,
|
|
27
|
+
hashFile,
|
|
28
|
+
type DuplicateVerdict,
|
|
29
|
+
type DuplicateQuestion,
|
|
30
|
+
} from "./duplicates";
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
describeSweep,
|
|
7
|
+
isJunkName,
|
|
8
|
+
pruneEmptyDirs,
|
|
9
|
+
scanDropzone,
|
|
10
|
+
sweepDropzone,
|
|
11
|
+
type DropOutcome,
|
|
12
|
+
type DroppedFile,
|
|
13
|
+
} from "./scan";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The drop-folder scraper.
|
|
17
|
+
*
|
|
18
|
+
* Almost every test here is about DESTRUCTION โ what the sweep deletes and, more importantly,
|
|
19
|
+
* what it must not. The dropzone is the one place the owner's copy may be the only copy: they
|
|
20
|
+
* dragged the file in expecting it to be taken, and if the sweep is wrong the original is
|
|
21
|
+
* gone with nothing to restore from.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
let root: string;
|
|
25
|
+
|
|
26
|
+
const write = (rel: string, bytes = 8): void => {
|
|
27
|
+
const full = join(root, rel);
|
|
28
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
29
|
+
writeFileSync(full, Buffer.alloc(bytes, 7));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
root = mkdtempSync(join(tmpdir(), "dropzone-spec-"));
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
36
|
+
|
|
37
|
+
// A stability window of 0 keeps these tests fast; rule 2 gets its own test below.
|
|
38
|
+
const sweep = (handle: (f: DroppedFile) => Promise<DropOutcome>, opts = {}) =>
|
|
39
|
+
sweepDropzone(root, handle, { stabilityMs: 0, ...opts });
|
|
40
|
+
|
|
41
|
+
describe("scanDropzone", () => {
|
|
42
|
+
test("finds files at every depth, with their path relative to the root", async () => {
|
|
43
|
+
write("a.mp3");
|
|
44
|
+
write("Album/b.mp3");
|
|
45
|
+
write("Album/Disc 2/c.mp3");
|
|
46
|
+
const found = await scanDropzone(root);
|
|
47
|
+
expect(found.map((f) => f.relativePath).sort()).toEqual([
|
|
48
|
+
"Album/Disc 2/c.mp3",
|
|
49
|
+
"Album/b.mp3",
|
|
50
|
+
"a.mp3",
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("๐ด returns junk too, because a sweep that must DELETE junk has to see it", async () => {
|
|
55
|
+
// "if it's a wrong type of file delete it" โ a scanner that filtered these out would
|
|
56
|
+
// leave them in the folder forever, and the owner's requirement is an EMPTY folder.
|
|
57
|
+
write(".DS_Store");
|
|
58
|
+
write("track.mp3");
|
|
59
|
+
expect((await scanDropzone(root)).map((f) => f.name).sort()).toEqual([".DS_Store", "track.mp3"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("captureSiblings records each directory's file list on the ONE walk", async () => {
|
|
63
|
+
// A folder's cover.jpg is only reachable while the directory still exists, and filing
|
|
64
|
+
// the audio out of it removes the neighbours. Re-reading later finds nothing.
|
|
65
|
+
write("Album/track.mp3");
|
|
66
|
+
write("Album/cover.jpg");
|
|
67
|
+
const found = await scanDropzone(root, { captureSiblings: true });
|
|
68
|
+
const track = found.find((f) => f.name === "track.mp3");
|
|
69
|
+
expect(track?.siblings.sort()).toEqual(["cover.jpg", "track.mp3"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("skipDirs keeps the sweep out of another app's queue", async () => {
|
|
73
|
+
write("collections/video.mp4");
|
|
74
|
+
write("mine.mp3");
|
|
75
|
+
const found = await scanDropzone(root, { skipDirs: ["collections"] });
|
|
76
|
+
expect(found.map((f) => f.name)).toEqual(["mine.mp3"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("the extension is lowercased and includes the dot; a dotless name has none", async () => {
|
|
80
|
+
write("LOUD.MP3");
|
|
81
|
+
write("README");
|
|
82
|
+
const byName = new Map((await scanDropzone(root)).map((f) => [f.name, f.ext]));
|
|
83
|
+
expect(byName.get("LOUD.MP3")).toBe(".mp3");
|
|
84
|
+
expect(byName.get("README")).toBe("");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("isJunkName", () => {
|
|
89
|
+
test("OS bookkeeping, resource forks and in-flight downloads", () => {
|
|
90
|
+
for (const name of [".DS_Store", "Thumbs.db", "._track.mp3", "movie.mp4.part", ".hidden"])
|
|
91
|
+
expect(isJunkName(name)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
test("real content is not junk, whatever it is called", () => {
|
|
94
|
+
for (const name of ["track.mp3", "cover.jpg", "Holiday.MP4", "notes"])
|
|
95
|
+
expect(isJunkName(name)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("sweepDropzone โ what gets deleted", () => {
|
|
100
|
+
test("๐ด a CONFIRMED file is deleted; a FAILED one is left exactly where it is", async () => {
|
|
101
|
+
// Rule 1, and the whole reason this module exists. Deleting on "the upload didn't throw"
|
|
102
|
+
// turns any catalog failure into permanent data loss.
|
|
103
|
+
write("good.mp3");
|
|
104
|
+
write("bad.mp3");
|
|
105
|
+
const report = await sweep(async (f) =>
|
|
106
|
+
f.name === "good.mp3"
|
|
107
|
+
? { kind: "stored" }
|
|
108
|
+
: { kind: "failed", detail: "the catalog said no" },
|
|
109
|
+
);
|
|
110
|
+
expect(existsSync(join(root, "good.mp3"))).toBe(false);
|
|
111
|
+
expect(existsSync(join(root, "bad.mp3"))).toBe(true);
|
|
112
|
+
expect(report.stored).toBe(1);
|
|
113
|
+
expect(report.failed).toHaveLength(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("๐ด a DUPLICATE is deleted โ that is the point of recognising one", async () => {
|
|
117
|
+
// "if it's a dup skip it then delete it". The bytes are already safe at the destination,
|
|
118
|
+
// so keeping the local copy is exactly the two-copies problem the owner is solving.
|
|
119
|
+
write("already-have.mp3");
|
|
120
|
+
await sweep(async () => ({ kind: "duplicate" }));
|
|
121
|
+
expect(existsSync(join(root, "already-have.mp3"))).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("๐ด a HELD file survives โ it is a question, not a failure", async () => {
|
|
125
|
+
write("maybe-dup.mp3");
|
|
126
|
+
const report = await sweep(async () => ({ kind: "held", detail: "waiting on the owner" }));
|
|
127
|
+
expect(existsSync(join(root, "maybe-dup.mp3"))).toBe(true);
|
|
128
|
+
expect(report.held).toBe(1);
|
|
129
|
+
expect(report.failed).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a DISCARD is deleted, so junk cannot accumulate", async () => {
|
|
133
|
+
write("junk.txt");
|
|
134
|
+
await sweep(async () => ({ kind: "discard", detail: "not audio" }));
|
|
135
|
+
expect(existsSync(join(root, "junk.txt"))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("๐ด a handler that THROWS costs one file, never the sweep", async () => {
|
|
139
|
+
// "This should not stall uploading the rest of the in progress uploads" โ stated by the
|
|
140
|
+
// owner about duplicates, and just as true of a corrupt file in the middle of 4,000.
|
|
141
|
+
write("one.mp3");
|
|
142
|
+
write("two.mp3");
|
|
143
|
+
write("three.mp3");
|
|
144
|
+
const report = await sweep(async (f) => {
|
|
145
|
+
if (f.name === "two.mp3") throw new Error("kaboom");
|
|
146
|
+
return { kind: "stored" };
|
|
147
|
+
});
|
|
148
|
+
expect(report.stored).toBe(2);
|
|
149
|
+
expect(report.failed).toHaveLength(1);
|
|
150
|
+
expect(report.failed[0]?.error).toContain("kaboom");
|
|
151
|
+
expect(existsSync(join(root, "two.mp3"))).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("dryRun deletes nothing at all", async () => {
|
|
155
|
+
write("a.mp3");
|
|
156
|
+
write("b.mp3");
|
|
157
|
+
await sweep(async () => ({ kind: "stored" }), { dryRun: true });
|
|
158
|
+
expect(existsSync(join(root, "a.mp3"))).toBe(true);
|
|
159
|
+
expect(existsSync(join(root, "b.mp3"))).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("๐ด a genuinely EMPTY file is handled, not stranded as forever-copying", async () => {
|
|
163
|
+
// The obvious "same size twice AND non-empty" test treats a real 0-byte file as
|
|
164
|
+
// permanently mid-copy: never handled, never deleted, so the folder never empties โ
|
|
165
|
+
// which is the one thing the owner's brief measures success by. Measured on a first
|
|
166
|
+
// run: three real files, all 0 bytes, reported "still copying" on every sweep.
|
|
167
|
+
writeFileSync(join(root, "empty.txt"), "");
|
|
168
|
+
const old = new Date(Date.now() - 5 * 60_000);
|
|
169
|
+
utimesSync(join(root, "empty.txt"), old, old);
|
|
170
|
+
const report = await sweep(async () => ({ kind: "discard" }));
|
|
171
|
+
expect(report.unstable).toEqual([]);
|
|
172
|
+
expect(report.discarded).toBe(1);
|
|
173
|
+
expect(existsSync(join(root, "empty.txt"))).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("a BRAND-NEW empty file is still treated as arriving", async () => {
|
|
177
|
+
// Emptiness is evidence of an unfinished copy only while the file is new.
|
|
178
|
+
writeFileSync(join(root, "arriving.bin"), "");
|
|
179
|
+
const report = await sweep(async () => ({ kind: "discard" }));
|
|
180
|
+
expect(report.unstable).toEqual(["arriving.bin"]);
|
|
181
|
+
expect(existsSync(join(root, "arriving.bin"))).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("๐ด an unstable file is skipped and reported as still-copying, not as failed", async () => {
|
|
185
|
+
// Dropping a 4 GB video is not atomic. Calling this a failure would send the owner
|
|
186
|
+
// looking for a bug in a file that is arriving perfectly.
|
|
187
|
+
write("growing.mp4", 10);
|
|
188
|
+
let handled = 0;
|
|
189
|
+
const report = await sweepDropzone(
|
|
190
|
+
root,
|
|
191
|
+
async () => {
|
|
192
|
+
handled++;
|
|
193
|
+
return { kind: "stored" };
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
// A real gap, and the file grows during it โ exactly the live case.
|
|
197
|
+
stabilityMs: 30,
|
|
198
|
+
onProgress: () => writeFileSync(join(root, "growing.mp4"), Buffer.alloc(9999)),
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
expect(handled).toBe(0);
|
|
202
|
+
expect(report.unstable).toEqual(["growing.mp4"]);
|
|
203
|
+
expect(existsSync(join(root, "growing.mp4"))).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("sweepDropzone โ leaving nothing behind", () => {
|
|
208
|
+
test("๐ด an emptied tree is removed and the report says the folder is empty", async () => {
|
|
209
|
+
// The owner's actual acceptance test: "In the end there should be nothing left in the
|
|
210
|
+
// myMusic dir."
|
|
211
|
+
write("Album/Disc 1/a.mp3");
|
|
212
|
+
write("Album/Disc 1/b.mp3");
|
|
213
|
+
write("Album/cover.jpg");
|
|
214
|
+
const report = await sweep(async () => ({ kind: "stored" }));
|
|
215
|
+
expect(existsSync(join(root, "Album"))).toBe(false);
|
|
216
|
+
expect(existsSync(root)).toBe(true); // never the root itself
|
|
217
|
+
expect(report.remaining).toBe(0);
|
|
218
|
+
expect(describeSweep(report)).toContain("folder is empty");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("๐ด a directory holding a HELD or FAILED file is NOT removed", async () => {
|
|
222
|
+
// rmdir refusing a non-empty directory is the safety property. A recursive delete here
|
|
223
|
+
// would take exactly the files the sweep decided to keep.
|
|
224
|
+
write("Keep/held.mp3");
|
|
225
|
+
write("Gone/stored.mp3");
|
|
226
|
+
await sweep(async (f) => (f.name === "held.mp3" ? { kind: "held" } : { kind: "stored" }));
|
|
227
|
+
expect(existsSync(join(root, "Keep/held.mp3"))).toBe(true);
|
|
228
|
+
expect(existsSync(join(root, "Gone"))).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("the report counts the bytes reclaimed from the queue", async () => {
|
|
232
|
+
write("a.mp3", 1000);
|
|
233
|
+
write("b.mp3", 2000);
|
|
234
|
+
const report = await sweep(async () => ({ kind: "stored" }));
|
|
235
|
+
expect(report.bytesRemoved).toBe(3000);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("pruneEmptyDirs", () => {
|
|
240
|
+
test("collapses a deep empty chain in ONE pass and never removes the root", async () => {
|
|
241
|
+
mkdirSync(join(root, "a/b/c/d"), { recursive: true });
|
|
242
|
+
const removed = await pruneEmptyDirs(root);
|
|
243
|
+
expect(removed).toBe(4);
|
|
244
|
+
expect(existsSync(join(root, "a"))).toBe(false);
|
|
245
|
+
expect(existsSync(root)).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("stops at a directory that still holds something", async () => {
|
|
249
|
+
write("a/b/keep.txt");
|
|
250
|
+
mkdirSync(join(root, "a/b/empty"), { recursive: true });
|
|
251
|
+
await pruneEmptyDirs(root);
|
|
252
|
+
expect(existsSync(join(root, "a/b/empty"))).toBe(false);
|
|
253
|
+
expect(existsSync(join(root, "a/b/keep.txt"))).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drop-folder scraper โ the reusable half.
|
|
3
|
+
*
|
|
4
|
+
* Owner's ask, 2026-08-13: *"Do this to prepare a reusable strategy for multiple apps to get
|
|
5
|
+
* intelligent processing for binary-server. Some functionality may be reusable in multiple
|
|
6
|
+
* apps in different ways and can go in cb where standardized app functionality is shared. The
|
|
7
|
+
* specific ingestion logic that only binary-server or one app needs can go to the specific
|
|
8
|
+
* app."* โฆ *"Make this tool reusable in case we need something like it on other folders
|
|
9
|
+
* later."*
|
|
10
|
+
*
|
|
11
|
+
* So: WALKING a drop folder, deciding a file has finished copying, sorting content from junk,
|
|
12
|
+
* deleting only what has been confirmed elsewhere, and leaving nothing behind โ all of that is
|
|
13
|
+
* the same for music, family photographs, collections videos and notes documents, and all of
|
|
14
|
+
* it is here. What a file MEANS โ which album, which artist, which collection โ is the app's,
|
|
15
|
+
* and none of it is here.
|
|
16
|
+
*
|
|
17
|
+
* โโ ๐ด Three rules that look like paranoia and are not โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
18
|
+
*
|
|
19
|
+
* 1. **A file is deleted only after the destination CONFIRMS it.** The dropzone is the one
|
|
20
|
+
* place the owner's copy may be the only copy: they dragged it in expecting it to be taken.
|
|
21
|
+
* A sweep that deletes on "the upload didn't throw" turns any catalog failure into data
|
|
22
|
+
* loss. The handler returns `stored`/`duplicate` and only then does the original go.
|
|
23
|
+
*
|
|
24
|
+
* 2. **A file must measure the same size twice before it is touched.** Dropping a 4 GB video
|
|
25
|
+
* into a folder is not atomic. Without this the sweep ingests a truncated file, reports
|
|
26
|
+
* success, and deletes the real one as it is still being written. notes' ingester and
|
|
27
|
+
* collections' both learned this the same way.
|
|
28
|
+
*
|
|
29
|
+
* 3. **One bad file never stops the sweep.** Every handler outcome is per-file, and a throw is
|
|
30
|
+
* caught and recorded. The owner's instruction is explicit about the duplicate case โ *"This
|
|
31
|
+
* should not stall uploading the rest of the in progress uploads"* โ and it generalizes: a
|
|
32
|
+
* dropzone with one unreadable file in it must still drain the other 4,000.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { Dirent } from "node:fs";
|
|
36
|
+
import { readdir, rm, rmdir, stat } from "node:fs/promises";
|
|
37
|
+
import { join, relative } from "node:path";
|
|
38
|
+
|
|
39
|
+
/** A file the sweep found, with everything a handler needs to decide about it. */
|
|
40
|
+
export interface DroppedFile {
|
|
41
|
+
/** Absolute path on disk. */
|
|
42
|
+
path: string;
|
|
43
|
+
/** Path relative to the dropzone root โ the app's only clue about where it was filed. */
|
|
44
|
+
relativePath: string;
|
|
45
|
+
/** The filename, no directories. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** Lowercased extension INCLUDING the dot, or `""` for a file that has none. */
|
|
48
|
+
ext: string;
|
|
49
|
+
size: number;
|
|
50
|
+
mtimeMs: number;
|
|
51
|
+
/** Every other FILE name in the same directory โ see {@link ScanOptions.captureSiblings}. */
|
|
52
|
+
siblings: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type DropOutcome =
|
|
56
|
+
/** Filed successfully. The original is deleted. */
|
|
57
|
+
| { kind: "stored"; detail?: string }
|
|
58
|
+
/** Already present at the destination. The original is deleted โ that is the point. */
|
|
59
|
+
| { kind: "duplicate"; detail?: string }
|
|
60
|
+
/** Not content: junk, an unusable sidecar, a format this app does not take. DELETED. */
|
|
61
|
+
| { kind: "discard"; detail?: string }
|
|
62
|
+
/** Deliberately parked (a duplicate awaiting a decision). LEFT ALONE, not retried loudly. */
|
|
63
|
+
| { kind: "held"; detail?: string }
|
|
64
|
+
/** Could not be filed. LEFT IN PLACE and reported. */
|
|
65
|
+
| { kind: "failed"; detail: string };
|
|
66
|
+
|
|
67
|
+
export interface SweepReport {
|
|
68
|
+
scanned: number;
|
|
69
|
+
stored: number;
|
|
70
|
+
duplicate: number;
|
|
71
|
+
discarded: number;
|
|
72
|
+
held: number;
|
|
73
|
+
failed: { relativePath: string; error: string }[];
|
|
74
|
+
/** Files still being copied in โ they are picked up by the next sweep, not an error. */
|
|
75
|
+
unstable: string[];
|
|
76
|
+
bytesRemoved: number;
|
|
77
|
+
emptyDirsRemoved: number;
|
|
78
|
+
/** Anything left in the root when the sweep finished. Empty is the goal. */
|
|
79
|
+
remaining: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ScanOptions {
|
|
83
|
+
/** Two size samples this far apart prove a copy finished. */
|
|
84
|
+
stabilityMs?: number;
|
|
85
|
+
/** Names that are never content, on top of the built-in OS list. */
|
|
86
|
+
reservedNames?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Capture each directory's file listing on the ONE walk.
|
|
89
|
+
*
|
|
90
|
+
* ๐ด Load-bearing for sidecar art, and it cannot be done later. A folder's `cover.jpg` is
|
|
91
|
+
* only reachable while the directory still exists, and filing the audio out of it removes
|
|
92
|
+
* the neighbours โ by the time anything else could look, they are gone. Capturing on the
|
|
93
|
+
* walk also costs one `readdir` per directory instead of one per file, which on a 6,000
|
|
94
|
+
* track library is the difference between seconds and minutes.
|
|
95
|
+
*/
|
|
96
|
+
captureSiblings?: boolean;
|
|
97
|
+
/** Directory names to skip entirely at any depth (another app's queue, our own state). */
|
|
98
|
+
skipDirs?: string[];
|
|
99
|
+
/** Stop after this many files. Absent means everything. */
|
|
100
|
+
limit?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** OS bookkeeping and editor scratch. Never content, in any app. */
|
|
104
|
+
const OS_JUNK = new Set([
|
|
105
|
+
".DS_Store",
|
|
106
|
+
".localized",
|
|
107
|
+
"Icon\r",
|
|
108
|
+
"Thumbs.db",
|
|
109
|
+
"desktop.ini",
|
|
110
|
+
"README.md",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const PARTIAL_SUFFIXES = [".download", ".crdownload", ".part", ".partial", ".tmp"];
|
|
114
|
+
|
|
115
|
+
/** A name that is OS noise, editor scratch, or a copy still in flight. */
|
|
116
|
+
export function isJunkName(name: string): boolean {
|
|
117
|
+
if (OS_JUNK.has(name)) return true;
|
|
118
|
+
// A dotfile in a drop folder is the operating system's or another tool's state. The owner
|
|
119
|
+
// drops content, and content does not begin with a dot.
|
|
120
|
+
if (name.startsWith(".")) return true;
|
|
121
|
+
if (name.startsWith("._")) return true; // AppleDouble resource forks
|
|
122
|
+
return PARTIAL_SUFFIXES.some((s) => name.toLowerCase().endsWith(s));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const extOf = (name: string): string => {
|
|
126
|
+
const dot = name.lastIndexOf(".");
|
|
127
|
+
return dot > 0 ? name.slice(dot).toLowerCase() : "";
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Every candidate file under `root`, deepest-last, with each one's sibling listing.
|
|
132
|
+
*
|
|
133
|
+
* Junk is NOT filtered out here โ it is returned like anything else, because a sweep whose job
|
|
134
|
+
* includes *"if it's a wrong type of file delete it"* has to see it in order to delete it.
|
|
135
|
+
* {@link isJunkName} is exported so a handler can classify without re-deriving the list.
|
|
136
|
+
*/
|
|
137
|
+
export async function scanDropzone(root: string, opts: ScanOptions = {}): Promise<DroppedFile[]> {
|
|
138
|
+
const skip = new Set(opts.skipDirs ?? []);
|
|
139
|
+
const out: DroppedFile[] = [];
|
|
140
|
+
|
|
141
|
+
const walk = async (dir: string): Promise<void> => {
|
|
142
|
+
if (opts.limit !== undefined && out.length >= opts.limit) return;
|
|
143
|
+
let entries: Dirent[];
|
|
144
|
+
try {
|
|
145
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
146
|
+
} catch {
|
|
147
|
+
return; // vanished mid-walk โ a dropzone is a moving target by nature
|
|
148
|
+
}
|
|
149
|
+
const siblings = opts.captureSiblings
|
|
150
|
+
? entries.filter((e) => e.isFile()).map((e) => e.name)
|
|
151
|
+
: [];
|
|
152
|
+
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (opts.limit !== undefined && out.length >= opts.limit) return;
|
|
155
|
+
const full = join(dir, entry.name);
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
if (skip.has(entry.name)) continue;
|
|
158
|
+
await walk(full);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!entry.isFile()) continue;
|
|
162
|
+
try {
|
|
163
|
+
const info = await stat(full);
|
|
164
|
+
out.push({
|
|
165
|
+
path: full,
|
|
166
|
+
relativePath: relative(root, full),
|
|
167
|
+
name: entry.name,
|
|
168
|
+
ext: extOf(entry.name),
|
|
169
|
+
size: info.size,
|
|
170
|
+
mtimeMs: info.mtimeMs,
|
|
171
|
+
siblings,
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
/* gone between readdir and stat */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await walk(root);
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* How old an EMPTY file has to be before it is believed. See {@link isStable}.
|
|
185
|
+
*
|
|
186
|
+
* A file that has just been created and has no bytes yet is almost certainly mid-copy; the
|
|
187
|
+
* same file a minute later really is empty.
|
|
188
|
+
*/
|
|
189
|
+
export const EMPTY_FILE_GRACE_MS = 60_000;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Has this file finished being copied in? See rule 2 in the header.
|
|
193
|
+
*
|
|
194
|
+
* ๐ด A zero-byte file needs its own answer, and getting it wrong strands the folder forever.
|
|
195
|
+
* The obvious test โ "same size twice AND non-empty" โ treats a genuinely empty file as
|
|
196
|
+
* permanently mid-copy: it is never handled, never deleted, and so the dropzone never empties,
|
|
197
|
+
* which is the one outcome the owner's brief measures success by (*"in the end there should be
|
|
198
|
+
* nothing left"*). Measured on a first run: three real files, all 0 bytes, reported as "still
|
|
199
|
+
* copying" on every sweep for as long as they sat there.
|
|
200
|
+
*
|
|
201
|
+
* Emptiness is only evidence of an unfinished copy while the file is NEW, so age decides. A
|
|
202
|
+
* file created a minute ago with no bytes in it is empty, not arriving.
|
|
203
|
+
*/
|
|
204
|
+
export async function isStable(file: DroppedFile, stabilityMs = 750): Promise<boolean> {
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, stabilityMs));
|
|
206
|
+
try {
|
|
207
|
+
const now = await stat(file.path);
|
|
208
|
+
if (now.size !== file.size) return false; // still growing
|
|
209
|
+
if (now.size > 0) return true;
|
|
210
|
+
return Date.now() - now.mtimeMs > EMPTY_FILE_GRACE_MS;
|
|
211
|
+
} catch {
|
|
212
|
+
return false; // taken by something else, or never really there
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remove directories left empty by a sweep, deepest first โ and never the root.
|
|
218
|
+
*
|
|
219
|
+
* ๐ด `rmdir` refuses a non-empty directory, and that refusal IS the safety property. A
|
|
220
|
+
* recursive delete here would take a file the sweep deliberately chose not to ingest โ a held
|
|
221
|
+
* duplicate, a failure left in place for the owner to look at โ which is precisely the set of
|
|
222
|
+
* files that must survive.
|
|
223
|
+
*/
|
|
224
|
+
export async function pruneEmptyDirs(root: string, dir = root): Promise<number> {
|
|
225
|
+
let removed = 0;
|
|
226
|
+
let entries: string[];
|
|
227
|
+
try {
|
|
228
|
+
entries = await readdir(dir);
|
|
229
|
+
} catch {
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
for (const name of entries) {
|
|
233
|
+
const full = join(dir, name);
|
|
234
|
+
try {
|
|
235
|
+
if ((await stat(full)).isDirectory()) removed += await pruneEmptyDirs(root, full);
|
|
236
|
+
} catch {
|
|
237
|
+
/* gone */
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (dir === root) return removed;
|
|
241
|
+
try {
|
|
242
|
+
await rmdir(dir);
|
|
243
|
+
return removed + 1;
|
|
244
|
+
} catch {
|
|
245
|
+
return removed; // not empty โ the right outcome, and not an error
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** What the app supplies: what to do with one file. */
|
|
250
|
+
export type DropHandler = (file: DroppedFile) => Promise<DropOutcome>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Walk a dropzone, hand every file to `handle`, and leave nothing behind that was dealt with.
|
|
254
|
+
*
|
|
255
|
+
* ๐ด The DELETION is here, and it is deliberately not the handler's job. Every app would
|
|
256
|
+
* otherwise reimplement rule 1, and the one that got it wrong would lose the owner's only copy
|
|
257
|
+
* silently. A handler says what happened; this decides what that means for the file on disk.
|
|
258
|
+
*/
|
|
259
|
+
export async function sweepDropzone(
|
|
260
|
+
root: string,
|
|
261
|
+
handle: DropHandler,
|
|
262
|
+
opts: ScanOptions & { dryRun?: boolean; onProgress?: (done: number, total: number) => void } = {},
|
|
263
|
+
): Promise<SweepReport> {
|
|
264
|
+
const report: SweepReport = {
|
|
265
|
+
scanned: 0,
|
|
266
|
+
stored: 0,
|
|
267
|
+
duplicate: 0,
|
|
268
|
+
discarded: 0,
|
|
269
|
+
held: 0,
|
|
270
|
+
failed: [],
|
|
271
|
+
unstable: [],
|
|
272
|
+
bytesRemoved: 0,
|
|
273
|
+
emptyDirsRemoved: 0,
|
|
274
|
+
remaining: 0,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const files = await scanDropzone(root, opts);
|
|
278
|
+
for (const [index, file] of files.entries()) {
|
|
279
|
+
report.scanned++;
|
|
280
|
+
opts.onProgress?.(index + 1, files.length);
|
|
281
|
+
|
|
282
|
+
if (!(await isStable(file, opts.stabilityMs))) {
|
|
283
|
+
report.unstable.push(file.relativePath);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let outcome: DropOutcome;
|
|
288
|
+
try {
|
|
289
|
+
outcome = opts.dryRun ? { kind: "held", detail: "dry run" } : await handle(file);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// Rule 3: a handler that throws costs one file, never the sweep.
|
|
292
|
+
outcome = {
|
|
293
|
+
kind: "failed",
|
|
294
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
switch (outcome.kind) {
|
|
299
|
+
case "stored":
|
|
300
|
+
case "duplicate":
|
|
301
|
+
case "discard": {
|
|
302
|
+
if (outcome.kind === "stored") report.stored++;
|
|
303
|
+
else if (outcome.kind === "duplicate") report.duplicate++;
|
|
304
|
+
else report.discarded++;
|
|
305
|
+
if (opts.dryRun) break;
|
|
306
|
+
try {
|
|
307
|
+
await rm(file.path);
|
|
308
|
+
report.bytesRemoved += file.size;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
// The bytes are safe at the destination; only the tidying failed. Reporting it
|
|
311
|
+
// as a FAILURE would send the owner looking for a file that arrived perfectly.
|
|
312
|
+
report.failed.push({
|
|
313
|
+
relativePath: file.relativePath,
|
|
314
|
+
error: `filed, but the original could not be removed: ${
|
|
315
|
+
error instanceof Error ? error.message : String(error)
|
|
316
|
+
}`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "held":
|
|
322
|
+
report.held++;
|
|
323
|
+
break;
|
|
324
|
+
case "failed":
|
|
325
|
+
report.failed.push({ relativePath: file.relativePath, error: outcome.detail });
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!opts.dryRun) report.emptyDirsRemoved = await pruneEmptyDirs(root);
|
|
331
|
+
report.remaining = (await scanDropzone(root, { skipDirs: opts.skipDirs })).length;
|
|
332
|
+
return report;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A one-line summary โ every consumer prints one, and they should read alike. */
|
|
336
|
+
export const describeSweep = (report: SweepReport): string =>
|
|
337
|
+
`${report.stored} added, ${report.duplicate} already had, ${report.discarded} discarded, ` +
|
|
338
|
+
`${report.held} held, ${report.failed.length} failed, ${report.unstable.length} still copying` +
|
|
339
|
+
(report.bytesRemoved > 0
|
|
340
|
+
? ` ยท ${(report.bytesRemoved / 1024 ** 3).toFixed(2)} GB freed from the queue`
|
|
341
|
+
: "") +
|
|
342
|
+
(report.remaining > 0 ? ` ยท ${report.remaining} left in the folder` : " ยท folder is empty");
|