From b451771169305f73e843577e8108b022e0fb983d Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Sat, 21 Mar 2026 19:31:31 +0800 Subject: [PATCH] chore: reset repository history --- .editorconfig | 16 + .env.example | 26 + .gitattributes | 2 + .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/bug_report.yml | 80 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 62 + .github/ISSUE_TEMPLATE/question.yml | 49 + .github/dependabot.yml | 91 + .github/pull_request_template.md | 26 + .github/workflows/auto-merge-dependencies.yml | 314 ++++ .github/workflows/ci.yml | 252 +++ .github/workflows/dependency-review.yml | 22 + .github/workflows/publish-images.yml | 82 + .github/workflows/security-audits.yml | 201 +++ .gitignore | 20 + .prettierignore | 12 + .prettierrc.json | 7 + AGENTS.md | 35 + CODE_OF_CONDUCT.md | 109 ++ CONTRIBUTING.md | 148 ++ Dockerfile | 39 + LICENSE | 661 +++++++ MESSAGING.md | 93 + README.md | 414 +++++ README.zh.md | 374 ++++ SECURITY.md | 46 + SUPPORT.md | 61 + apps/api/cmd/api/app.go | 302 ++++ apps/api/cmd/api/app_test.go | 515 ++++++ apps/api/cmd/api/main.go | 45 + apps/api/cmd/api/main_test.go | 209 +++ apps/api/go.mod | 96 + apps/api/go.sum | 239 +++ apps/api/internal/config/config.go | 190 ++ apps/api/internal/config/config_test.go | 160 ++ .../api/internal/http/api_integration_test.go | 339 ++++ apps/api/internal/http/router.go | 325 ++++ apps/api/internal/http/router_test.go | 689 +++++++ apps/api/internal/jobs/expiry.go | 26 + apps/api/internal/jobs/expiry_test.go | 223 +++ apps/api/internal/models/models.go | 80 + apps/api/internal/ratelimit/limiter.go | 82 + .../ratelimit/limiter_integration_test.go | 82 + apps/api/internal/ratelimit/limiter_test.go | 86 + apps/api/internal/repo/migrate.go | 93 + apps/api/internal/repo/migrate_unit_test.go | 222 +++ .../api/internal/repo/migrations/001_init.sql | 46 + apps/api/internal/repo/postgres.go | 510 ++++++ .../repo/postgres_adapter_integration_test.go | 56 + .../internal/repo/postgres_core_unit_test.go | 526 ++++++ .../repo/postgres_integration_test.go | 272 +++ .../repo/postgres_mutation_unit_test.go | 321 ++++ .../internal/repo/postgres_wrapper_test.go | 17 + apps/api/internal/repo/repository.go | 23 + apps/api/internal/repo/test_helpers_test.go | 205 +++ apps/api/internal/service/transfer_service.go | 716 ++++++++ .../transfer_service_additional_test.go | 916 ++++++++++ .../service/transfer_service_edge_test.go | 408 +++++ .../internal/service/transfer_service_test.go | 374 ++++ apps/api/internal/storage/s3.go | 246 +++ .../internal/storage/s3_integration_test.go | 172 ++ apps/api/internal/storage/s3_unit_test.go | 465 +++++ apps/web/.gitignore | 23 + apps/web/eslint.config.js | 29 + apps/web/index.html | 48 + apps/web/package.json | 49 + apps/web/public/apple-touch-icon.png | Bin 0 -> 24120 bytes apps/web/public/brand-lockup-horizontal.png | Bin 0 -> 301747 bytes apps/web/public/brand-lockup-horizontal.svg | 54 + apps/web/public/brand-symbol-192.png | Bin 0 -> 26891 bytes apps/web/public/brand-symbol-512.png | Bin 0 -> 142223 bytes apps/web/public/brand-symbol-maskable-512.png | Bin 0 -> 143224 bytes apps/web/public/brand-symbol-maskable.svg | 35 + apps/web/public/brand-symbol.svg | 35 + apps/web/public/favicon.svg | 35 + apps/web/public/icons.svg | 24 + apps/web/public/manifest.webmanifest | 35 + apps/web/public/sw.js | 83 + apps/web/scripts/generate-seo-assets.mjs | 278 +++ apps/web/src/app/App.tsx | 83 + apps/web/src/app/Shell.test.tsx | 38 + apps/web/src/app/Shell.tsx | 55 + apps/web/src/app/routes/HistoryPage.test.tsx | 36 + apps/web/src/app/routes/HistoryPage.tsx | 16 + apps/web/src/app/routes/HomePage.test.tsx | 44 + apps/web/src/app/routes/HomePage.tsx | 14 + apps/web/src/app/routes/NotFoundPage.test.tsx | 19 + apps/web/src/app/routes/NotFoundPage.tsx | 68 + apps/web/src/app/routes/ReceivePage.test.tsx | 41 + apps/web/src/app/routes/ReceivePage.tsx | 14 + apps/web/src/app/routes/SenderRouteLayout.tsx | 12 + apps/web/src/app/routes/SharePage.test.tsx | 54 + apps/web/src/app/routes/SharePage.tsx | 23 + apps/web/src/components/ui/Button.tsx | 20 + apps/web/src/components/ui/Card.tsx | 10 + .../src/components/ui/PageStateCard.test.tsx | 19 + apps/web/src/components/ui/PageStateCard.tsx | 30 + .../src/components/ui/ProgressBar.test.tsx | 28 + apps/web/src/components/ui/ProgressBar.tsx | 26 + .../src/components/ui/StatusBadge.test.tsx | 23 + apps/web/src/components/ui/StatusBadge.tsx | 32 + .../features/history/HistoryBoard.test.tsx | 239 +++ .../web/src/features/history/HistoryBoard.tsx | 257 +++ .../src/features/receive/ReceiveTransfer.tsx | 358 ++++ .../web/src/features/share/ShareCard.test.tsx | 367 ++++ apps/web/src/features/share/ShareCard.tsx | 275 +++ .../upload/TransferContext.actions.test.tsx | 557 ++++++ .../upload/TransferContext.helpers.test.ts | 455 +++++ .../features/upload/TransferContext.test.tsx | 305 ++++ .../src/features/upload/TransferContext.tsx | 879 +++++++++ .../upload/UploadStudio.helpers.test.ts | 151 ++ .../src/features/upload/UploadStudio.test.tsx | 475 +++++ apps/web/src/features/upload/UploadStudio.tsx | 608 +++++++ .../src/features/upload/expiryOptions.test.ts | 31 + .../features/upload/selectionLimits.test.ts | 48 + .../src/features/upload/selectionLimits.ts | 53 + .../upload/uploadSelectionDraft.test.ts | 333 ++++ .../features/upload/uploadSelectionDraft.ts | 224 +++ apps/web/src/index.css | 1602 +++++++++++++++++ apps/web/src/lib/api/client.test.ts | 279 +++ apps/web/src/lib/api/client.ts | 209 +++ apps/web/src/lib/api/types.ts | 117 ++ apps/web/src/lib/crypto/base64.test.ts | 21 + apps/web/src/lib/crypto/base64.ts | 29 + apps/web/src/lib/crypto/envelope.test.ts | 85 + apps/web/src/lib/crypto/envelope.ts | 215 +++ apps/web/src/lib/crypto/urlKey.test.ts | 22 + apps/web/src/lib/crypto/urlKey.ts | 26 + apps/web/src/lib/download/decrypt.ts | 101 ++ apps/web/src/lib/download/zip.test.ts | 104 ++ apps/web/src/lib/download/zip.ts | 103 ++ apps/web/src/lib/files/formatBytes.test.ts | 15 + apps/web/src/lib/files/formatBytes.ts | 13 + apps/web/src/lib/files/metadata.test.ts | 160 ++ apps/web/src/lib/files/metadata.ts | 62 + apps/web/src/lib/files/paths.test.ts | 14 + apps/web/src/lib/files/paths.ts | 19 + .../src/lib/files/persistentSources.test.ts | 354 ++++ apps/web/src/lib/files/persistentSources.ts | 226 +++ apps/web/src/lib/files/save.ts | 114 ++ apps/web/src/lib/i18n/formatDateTime.ts | 36 + apps/web/src/lib/indexeddb/db.test.ts | 107 ++ apps/web/src/lib/indexeddb/db.ts | 149 ++ apps/web/src/lib/seo/site.test.ts | 53 + apps/web/src/lib/seo/site.ts | 96 + apps/web/src/lib/seo/usePageMetadata.test.tsx | 123 ++ apps/web/src/lib/seo/usePageMetadata.ts | 155 ++ apps/web/src/lib/transfers/expiry.test.ts | 55 + apps/web/src/lib/transfers/expiry.ts | 11 + apps/web/src/lib/workers/crypto.worker.ts | 119 ++ apps/web/src/lib/workers/cryptoClient.ts | 101 ++ apps/web/src/main.tsx | 25 + apps/web/src/test/setup.ts | 3 + apps/web/tsconfig.app.json | 19 + apps/web/tsconfig.json | 5 + apps/web/tsconfig.node.json | 17 + apps/web/vite.config.ts | 37 + bun.lock | 776 ++++++++ codecov.yml | 37 + docker-compose.build.yml | 9 + docker-compose.yml | 86 + infra/minio/init.sh | 10 + infra/xdrop/entrypoint.sh | 66 + infra/xdrop/nginx.conf.template | 83 + package.json | 36 + packages/shared/package.json | 8 + packages/shared/src/index.ts | 36 + playwright.config.ts | 21 + scripts/check-go-format.mjs | 79 + tests/e2e/xdrop.spec.ts | 438 +++++ tsconfig.base.json | 19 + 172 files changed, 26281 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/auto-merge-dependencies.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/publish-images.yml create mode 100644 .github/workflows/security-audits.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 AGENTS.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 MESSAGING.md create mode 100644 README.md create mode 100644 README.zh.md create mode 100644 SECURITY.md create mode 100644 SUPPORT.md create mode 100644 apps/api/cmd/api/app.go create mode 100644 apps/api/cmd/api/app_test.go create mode 100644 apps/api/cmd/api/main.go create mode 100644 apps/api/cmd/api/main_test.go create mode 100644 apps/api/go.mod create mode 100644 apps/api/go.sum create mode 100644 apps/api/internal/config/config.go create mode 100644 apps/api/internal/config/config_test.go create mode 100644 apps/api/internal/http/api_integration_test.go create mode 100644 apps/api/internal/http/router.go create mode 100644 apps/api/internal/http/router_test.go create mode 100644 apps/api/internal/jobs/expiry.go create mode 100644 apps/api/internal/jobs/expiry_test.go create mode 100644 apps/api/internal/models/models.go create mode 100644 apps/api/internal/ratelimit/limiter.go create mode 100644 apps/api/internal/ratelimit/limiter_integration_test.go create mode 100644 apps/api/internal/ratelimit/limiter_test.go create mode 100644 apps/api/internal/repo/migrate.go create mode 100644 apps/api/internal/repo/migrate_unit_test.go create mode 100644 apps/api/internal/repo/migrations/001_init.sql create mode 100644 apps/api/internal/repo/postgres.go create mode 100644 apps/api/internal/repo/postgres_adapter_integration_test.go create mode 100644 apps/api/internal/repo/postgres_core_unit_test.go create mode 100644 apps/api/internal/repo/postgres_integration_test.go create mode 100644 apps/api/internal/repo/postgres_mutation_unit_test.go create mode 100644 apps/api/internal/repo/postgres_wrapper_test.go create mode 100644 apps/api/internal/repo/repository.go create mode 100644 apps/api/internal/repo/test_helpers_test.go create mode 100644 apps/api/internal/service/transfer_service.go create mode 100644 apps/api/internal/service/transfer_service_additional_test.go create mode 100644 apps/api/internal/service/transfer_service_edge_test.go create mode 100644 apps/api/internal/service/transfer_service_test.go create mode 100644 apps/api/internal/storage/s3.go create mode 100644 apps/api/internal/storage/s3_integration_test.go create mode 100644 apps/api/internal/storage/s3_unit_test.go create mode 100644 apps/web/.gitignore create mode 100644 apps/web/eslint.config.js create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/public/apple-touch-icon.png create mode 100644 apps/web/public/brand-lockup-horizontal.png create mode 100644 apps/web/public/brand-lockup-horizontal.svg create mode 100644 apps/web/public/brand-symbol-192.png create mode 100644 apps/web/public/brand-symbol-512.png create mode 100644 apps/web/public/brand-symbol-maskable-512.png create mode 100644 apps/web/public/brand-symbol-maskable.svg create mode 100644 apps/web/public/brand-symbol.svg create mode 100644 apps/web/public/favicon.svg create mode 100644 apps/web/public/icons.svg create mode 100644 apps/web/public/manifest.webmanifest create mode 100644 apps/web/public/sw.js create mode 100644 apps/web/scripts/generate-seo-assets.mjs create mode 100644 apps/web/src/app/App.tsx create mode 100644 apps/web/src/app/Shell.test.tsx create mode 100644 apps/web/src/app/Shell.tsx create mode 100644 apps/web/src/app/routes/HistoryPage.test.tsx create mode 100644 apps/web/src/app/routes/HistoryPage.tsx create mode 100644 apps/web/src/app/routes/HomePage.test.tsx create mode 100644 apps/web/src/app/routes/HomePage.tsx create mode 100644 apps/web/src/app/routes/NotFoundPage.test.tsx create mode 100644 apps/web/src/app/routes/NotFoundPage.tsx create mode 100644 apps/web/src/app/routes/ReceivePage.test.tsx create mode 100644 apps/web/src/app/routes/ReceivePage.tsx create mode 100644 apps/web/src/app/routes/SenderRouteLayout.tsx create mode 100644 apps/web/src/app/routes/SharePage.test.tsx create mode 100644 apps/web/src/app/routes/SharePage.tsx create mode 100644 apps/web/src/components/ui/Button.tsx create mode 100644 apps/web/src/components/ui/Card.tsx create mode 100644 apps/web/src/components/ui/PageStateCard.test.tsx create mode 100644 apps/web/src/components/ui/PageStateCard.tsx create mode 100644 apps/web/src/components/ui/ProgressBar.test.tsx create mode 100644 apps/web/src/components/ui/ProgressBar.tsx create mode 100644 apps/web/src/components/ui/StatusBadge.test.tsx create mode 100644 apps/web/src/components/ui/StatusBadge.tsx create mode 100644 apps/web/src/features/history/HistoryBoard.test.tsx create mode 100644 apps/web/src/features/history/HistoryBoard.tsx create mode 100644 apps/web/src/features/receive/ReceiveTransfer.tsx create mode 100644 apps/web/src/features/share/ShareCard.test.tsx create mode 100644 apps/web/src/features/share/ShareCard.tsx create mode 100644 apps/web/src/features/upload/TransferContext.actions.test.tsx create mode 100644 apps/web/src/features/upload/TransferContext.helpers.test.ts create mode 100644 apps/web/src/features/upload/TransferContext.test.tsx create mode 100644 apps/web/src/features/upload/TransferContext.tsx create mode 100644 apps/web/src/features/upload/UploadStudio.helpers.test.ts create mode 100644 apps/web/src/features/upload/UploadStudio.test.tsx create mode 100644 apps/web/src/features/upload/UploadStudio.tsx create mode 100644 apps/web/src/features/upload/expiryOptions.test.ts create mode 100644 apps/web/src/features/upload/selectionLimits.test.ts create mode 100644 apps/web/src/features/upload/selectionLimits.ts create mode 100644 apps/web/src/features/upload/uploadSelectionDraft.test.ts create mode 100644 apps/web/src/features/upload/uploadSelectionDraft.ts create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/lib/api/client.test.ts create mode 100644 apps/web/src/lib/api/client.ts create mode 100644 apps/web/src/lib/api/types.ts create mode 100644 apps/web/src/lib/crypto/base64.test.ts create mode 100644 apps/web/src/lib/crypto/base64.ts create mode 100644 apps/web/src/lib/crypto/envelope.test.ts create mode 100644 apps/web/src/lib/crypto/envelope.ts create mode 100644 apps/web/src/lib/crypto/urlKey.test.ts create mode 100644 apps/web/src/lib/crypto/urlKey.ts create mode 100644 apps/web/src/lib/download/decrypt.ts create mode 100644 apps/web/src/lib/download/zip.test.ts create mode 100644 apps/web/src/lib/download/zip.ts create mode 100644 apps/web/src/lib/files/formatBytes.test.ts create mode 100644 apps/web/src/lib/files/formatBytes.ts create mode 100644 apps/web/src/lib/files/metadata.test.ts create mode 100644 apps/web/src/lib/files/metadata.ts create mode 100644 apps/web/src/lib/files/paths.test.ts create mode 100644 apps/web/src/lib/files/paths.ts create mode 100644 apps/web/src/lib/files/persistentSources.test.ts create mode 100644 apps/web/src/lib/files/persistentSources.ts create mode 100644 apps/web/src/lib/files/save.ts create mode 100644 apps/web/src/lib/i18n/formatDateTime.ts create mode 100644 apps/web/src/lib/indexeddb/db.test.ts create mode 100644 apps/web/src/lib/indexeddb/db.ts create mode 100644 apps/web/src/lib/seo/site.test.ts create mode 100644 apps/web/src/lib/seo/site.ts create mode 100644 apps/web/src/lib/seo/usePageMetadata.test.tsx create mode 100644 apps/web/src/lib/seo/usePageMetadata.ts create mode 100644 apps/web/src/lib/transfers/expiry.test.ts create mode 100644 apps/web/src/lib/transfers/expiry.ts create mode 100644 apps/web/src/lib/workers/crypto.worker.ts create mode 100644 apps/web/src/lib/workers/cryptoClient.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/test/setup.ts create mode 100644 apps/web/tsconfig.app.json create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts create mode 100644 bun.lock create mode 100644 codecov.yml create mode 100644 docker-compose.build.yml create mode 100644 docker-compose.yml create mode 100644 infra/minio/init.sh create mode 100644 infra/xdrop/entrypoint.sh create mode 100644 infra/xdrop/nginx.conf.template create mode 100644 package.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 playwright.config.ts create mode 100644 scripts/check-go-format.mjs create mode 100644 tests/e2e/xdrop.spec.ts create mode 100644 tsconfig.base.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..aac9255 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8555168 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +XDROP_IMAGE=ghcr.io/xixu-me/xdrop:latest +API_ADDR=:8080 +DATABASE_URL=postgres://xdrop:xdrop@localhost:5432/xdrop?sslmode=disable +REDIS_ADDR=localhost:6379 +REDIS_PASSWORD= +REDIS_DB=0 +S3_ENDPOINT=http://localhost:9000 +S3_PUBLIC_ENDPOINT=http://localhost:8080 +S3_REGION=us-east-1 +S3_BUCKET=xdrop +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_USE_SSL=false +PRESIGN_TTL_SECONDS=300 +DEFAULT_EXPIRY_SECONDS=3600 +ALLOWED_ORIGINS=http://localhost:8080 +RATE_LIMIT_CREATE=20 +RATE_LIMIT_PUBLIC_READ=180 +RATE_LIMIT_DOWNLOAD_URLS=120 +CLEANUP_INTERVAL=2m +CHUNK_SIZE_BYTES=8388608 +MAX_FILE_COUNT=100 +MAX_TRANSFER_BYTES=268435456 +VITE_API_BASE_URL=/api/v1 +VITE_SITE_URL=http://localhost:8080 +VITE_ENABLE_VIDEO_METADATA_STRIP=false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f7e2def --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.sh text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b3dada0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: https://xi-xu.me/#sponsorships +buy_me_a_coffee: xixu diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..0b07f8f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,80 @@ +name: Bug report +description: Report a reproducible problem in Xdrop +title: 'bug: ' +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for helping improve Xdrop. + + Please use this form for reproducible defects only. If you are unsure whether this is a + bug or you need help with setup, use the question form instead. + - type: checkboxes + id: preflight + attributes: + label: Preflight + options: + - label: I searched existing issues and pull requests. + required: true + - label: This is not a sensitive security report. + required: true + - label: I removed secrets, tokens, and real share links from the report. + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Web app + - API + - Encryption or share-link flow + - Docker or local environment + - CI or automation + - Documentation + - Other + validations: + required: true + - type: textarea + id: summary + attributes: + label: What happened? + description: Describe the bug and the user-visible impact. + placeholder: Uploads resume incorrectly after refresh and the share page never reaches Ready. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + placeholder: After refresh, the upload should reconnect and eventually reach Ready. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: Be as specific as possible. + placeholder: | + 1. Start the local Docker stack + 2. Open http://localhost:8080 + 3. Upload a large file + 4. Refresh during chunk upload + 5. Observe... + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Browser, OS, commit/tag, and anything relevant about your setup. + placeholder: Windows 11, Chrome 134, local main branch, Docker Desktop 4.x + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs, screenshots, or extra context + description: Paste sanitized logs or add screenshots if they help. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..07ca272 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support guide + url: https://github.com/xixu-me/xdrop/blob/main/SUPPORT.md + about: Use the public support process for setup help, troubleshooting, and questions. + - name: Security policy + url: https://github.com/xixu-me/xdrop/blob/main/SECURITY.md + about: Report vulnerabilities privately. Do not open public security issues. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..7046d4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,62 @@ +name: Feature request +description: Suggest an improvement that fits Xdrop's scope +title: 'feat: ' +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to propose an improvement. + + Xdrop is intentionally focused on privacy-preserving file transfer. Requests that start from + the problem they solve are much easier to evaluate than implementation-first ideas. + - type: checkboxes + id: preflight + attributes: + label: Preflight + options: + - label: I searched existing issues and pull requests. + required: true + - label: This request is not a security report. + required: true + - label: I understand large scope changes may need discussion before implementation. + required: true + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + placeholder: It is hard to tell which transfers are safe to delete after privacy mode clears local controls. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + placeholder: Add a sender-side status hint explaining what metadata remains locally after privacy mode runs. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + placeholder: Documentation update, status copy tweak, or no change. + - type: dropdown + id: area + attributes: + label: Area + options: + - Upload flow + - Receive flow + - Transfer management + - API or storage + - Local development or tooling + - Documentation + - Other + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Mockups, related issues, constraints, or rollout concerns. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..d6ada87 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,49 @@ +name: Question or support request +description: Ask for help with setup, usage, or expected behavior +title: 'question: ' +labels: + - question +body: + - type: markdown + attributes: + value: | + Please keep support questions public unless the topic is security-sensitive. + + If this turns out to be a bug, a maintainer can help convert it into a bug report. + - type: checkboxes + id: preflight + attributes: + label: Preflight + options: + - label: I searched existing issues and pull requests. + required: true + - label: This is not a sensitive security report. + required: true + - type: textarea + id: goal + attributes: + label: What are you trying to do? + placeholder: I want to run the full stack locally and verify the Playwright flow. + validations: + required: true + - type: textarea + id: tried + attributes: + label: What have you tried so far? + placeholder: I ran docker compose up, then bun run test:e2e, but the app never became healthy. + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: OS, browser, Docker version, Bun version, Go version, and branch if relevant. + placeholder: macOS 15, Docker 28.x, Bun 1.3.x, Go 1.26.x, branch main + validations: + required: true + - type: textarea + id: details + attributes: + label: Error output or additional context + description: Paste sanitized logs, screenshots, or command output that would help someone assist. + render: shell diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4bfe3b9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,91 @@ +version: 2 +updates: + - package-ecosystem: bun + directory: / + schedule: + interval: weekly + day: monday + time: '09:00' + timezone: Asia/Shanghai + open-pull-requests-limit: 10 + labels: + - dependencies + - javascript + reviewers: + - xixu-me + commit-message: + prefix: 'deps(bun)' + groups: + web-and-tooling: + update-types: + - minor + - patch + + - package-ecosystem: gomod + directory: /apps/api + schedule: + interval: weekly + day: monday + time: '09:30' + timezone: Asia/Shanghai + open-pull-requests-limit: 5 + labels: + - dependencies + - go + reviewers: + - xixu-me + commit-message: + prefix: 'deps(go)' + groups: + api-runtime: + update-types: + - minor + - patch + + - package-ecosystem: docker + directory: /apps/web + schedule: + interval: weekly + day: tuesday + time: '09:00' + timezone: Asia/Shanghai + labels: + - dependencies + - docker + reviewers: + - xixu-me + open-pull-requests-limit: 2 + commit-message: + prefix: 'deps(docker-web)' + + - package-ecosystem: docker + directory: /apps/api + schedule: + interval: weekly + day: tuesday + time: '09:10' + timezone: Asia/Shanghai + labels: + - dependencies + - docker + reviewers: + - xixu-me + open-pull-requests-limit: 2 + commit-message: + prefix: 'deps(docker-api)' + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: wednesday + time: '09:00' + timezone: Asia/Shanghai + labels: + - dependencies + - github-actions + reviewers: + - xixu-me + open-pull-requests-limit: 3 + commit-message: + prefix: 'deps(ci)' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..f1c7e3e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,26 @@ +## Summary + +Describe the change and the user-visible impact. + +## Linked issues + +Closes # + +## Verification + +- [ ] `bun run format:check` +- [ ] Relevant frontend checks (`bun run lint:web`, `bun run typecheck:web`, `bun run test:web`) +- [ ] Relevant backend checks (`cd apps/api && go test ./... -coverprofile=coverage.out -covermode=atomic`) +- [ ] `bun run test:e2e` for full-stack or user-flow changes + +## Checklist + +- [ ] The change fits the current project scope +- [ ] Tests were added or updated when behavior changed +- [ ] Docs were updated when setup, behavior, or contributor workflow changed +- [ ] Screenshots or recordings are attached for UI changes +- [ ] Config, Docker, API contract, or security-sensitive changes are called out below + +## Notes for reviewers + +Add anything reviewers should pay extra attention to. diff --git a/.github/workflows/auto-merge-dependencies.yml b/.github/workflows/auto-merge-dependencies.yml new file mode 100644 index 0000000..7ab0637 --- /dev/null +++ b/.github/workflows/auto-merge-dependencies.yml @@ -0,0 +1,314 @@ +name: Auto-merge Dependencies + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - ready_for_review + workflow_run: + workflows: + - CI + types: + - completed + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + checks: read + +jobs: + mark-safe-updates: + name: Mark safe Dependabot PRs + if: ${{ github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Label updates that can auto-merge + uses: actions/github-script@v8 + env: + AUTO_MERGE_LABEL: dependencies:auto-merge + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const issue_number = context.issue.number + const label = process.env.AUTO_MERGE_LABEL + const pr = context.payload.pull_request + const isDraft = pr?.draft === true + const safeToMerge = !isDraft + + try { + await github.rest.issues.getLabel({ owner, repo, name: label }) + } catch (error) { + if (error.status !== 404) { + throw error + } + + await github.rest.issues.createLabel({ + owner, + repo, + name: label, + color: '0e8a16', + description: 'Dependabot updates that can merge after checks pass', + }) + } + + if (safeToMerge) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [label], + }) + core.info(`Added ${label} to PR #${issue_number}`) + return + } + + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name: label, + }) + core.info(`Removed ${label} from PR #${issue_number}`) + } catch (error) { + if (error.status !== 404) { + throw error + } + core.info(`Label ${label} not present on PR #${issue_number}`) + } + + merge-when-green: + name: Merge labeled dependency PRs + if: >- + ${{ + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.pull_requests[0] && + contains(fromJSON('["pull_request","push"]'), github.event.workflow_run.event) + }} + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Merge safe updates once all checks pass + uses: actions/github-script@v8 + env: + AUTO_MERGE_LABEL: dependencies:auto-merge + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const run = context.payload.workflow_run + const pullRequest = run.pull_requests?.[0] + + if (!pullRequest) { + core.info('No pull request is attached to this workflow run.') + return + } + + const prNumber = pullRequest.number + const label = process.env.AUTO_MERGE_LABEL + + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: prNumber, + }) + + if (pr.state !== 'open') { + core.info(`PR #${prNumber} is already ${pr.state}.`) + return + } + + if (pr.draft) { + core.info(`PR #${prNumber} is still a draft.`) + return + } + + if (pr.user?.login !== 'dependabot[bot]') { + core.info(`PR #${prNumber} was not opened by Dependabot.`) + return + } + + if (!pr.labels.some((item) => item.name === label)) { + core.info(`PR #${prNumber} is not marked with ${label}.`) + return + } + + if (pr.head.sha !== run.head_sha) { + core.info(`PR #${prNumber} has moved on from ${run.head_sha}; skipping stale run.`) + return + } + + const { data: checkRuns } = await github.rest.checks.listForRef({ + owner, + repo, + ref: pr.head.sha, + per_page: 100, + }) + + const unfinished = checkRuns.check_runs.filter((check) => check.status !== 'completed') + if (unfinished.length > 0) { + core.info(`PR #${prNumber} still has ${unfinished.length} check run(s) in progress.`) + return + } + + const failing = checkRuns.check_runs.filter( + (check) => !['success', 'neutral', 'skipped'].includes(check.conclusion ?? ''), + ) + + if (failing.length > 0) { + core.info( + `PR #${prNumber} has failing check runs: ${failing.map((check) => check.name).join(', ')}`, + ) + return + } + + await github.rest.pulls.merge({ + owner, + repo, + pull_number: prNumber, + sha: pr.head.sha, + merge_method: 'squash', + }) + + core.info(`Merged Dependabot PR #${prNumber}.`) + + label-and-merge-all: + name: Label and merge all open Dependabot PRs + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Label all open Dependabot PRs + uses: actions/github-script@v8 + env: + AUTO_MERGE_LABEL: dependencies:auto-merge + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const label = process.env.AUTO_MERGE_LABEL + + try { + await github.rest.issues.getLabel({ owner, repo, name: label }) + } catch (error) { + if (error.status !== 404) { + throw error + } + + await github.rest.issues.createLabel({ + owner, + repo, + name: label, + color: '0e8a16', + description: 'Dependabot updates that can merge after checks pass', + }) + } + + const { data: pulls } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + per_page: 100, + }) + + const dependabotPRs = pulls.filter( + (pr) => pr.user?.login === 'dependabot[bot]' && !pr.draft, + ) + + if (dependabotPRs.length === 0) { + core.info('No open non-draft Dependabot PRs found.') + return + } + + for (const pr of dependabotPRs) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr.number, + labels: [label], + }) + core.info(`Added ${label} to PR #${pr.number} ("${pr.title}")`) + } + + - name: Merge labeled Dependabot PRs with passing checks + uses: actions/github-script@v8 + env: + AUTO_MERGE_LABEL: dependencies:auto-merge + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const label = process.env.AUTO_MERGE_LABEL + + const { data: pulls } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + per_page: 100, + }) + + const candidates = pulls.filter( + (pr) => + pr.user?.login === 'dependabot[bot]' && + !pr.draft && + pr.labels.some((l) => l.name === label), + ) + + if (candidates.length === 0) { + core.info('No labeled Dependabot PRs found to merge.') + return + } + + for (const pr of candidates) { + const { data: checkRuns } = await github.rest.checks.listForRef({ + owner, + repo, + ref: pr.head.sha, + per_page: 100, + }) + + const unfinished = checkRuns.check_runs.filter( + (check) => check.status !== 'completed', + ) + + if (unfinished.length > 0) { + core.info( + `PR #${pr.number} still has ${unfinished.length} check run(s) in progress — skipping.`, + ) + continue + } + + const failing = checkRuns.check_runs.filter( + (check) => !['success', 'neutral', 'skipped'].includes(check.conclusion ?? ''), + ) + + if (failing.length > 0) { + core.info( + `PR #${pr.number} has failing check runs: ${failing.map((c) => c.name).join(', ')} — skipping.`, + ) + continue + } + + try { + await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + sha: pr.head.sha, + merge_method: 'squash', + }) + core.info(`Merged Dependabot PR #${pr.number} ("${pr.title}").`) + } catch (error) { + core.warning(`Failed to merge PR #${pr.number}: ${error.message}`) + } + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..792b075 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,252 @@ +name: CI + +on: + pull_request: + paths-ignore: + - '**/*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - 'AGENTS.md' + - 'CODE_OF_CONDUCT.md' + - 'CONTRIBUTING.md' + - 'LICENSE' + - 'SECURITY.md' + - 'SUPPORT.md' + push: + branches: + - main + paths-ignore: + - '**/*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - 'AGENTS.md' + - 'CODE_OF_CONDUCT.md' + - 'CONTRIBUTING.md' + - 'LICENSE' + - 'SECURITY.md' + - 'SUPPORT.md' + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + COMPOSE_FILES: -f docker-compose.yml -f docker-compose.build.yml + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + +jobs: + web: + name: Web Checks + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Check Prettier formatting + run: bun run format:check:prettier + + - name: Lint web app + run: bun run lint:web + + - name: Type-check web app + run: bun run typecheck:web + + - name: Run web unit tests with coverage + run: bun run test:web:coverage + + - name: Normalize web coverage paths for Codecov + run: | + python - <<'PY' + from pathlib import Path + + source = Path('apps/web/coverage/lcov.info') + target = Path('apps/web/coverage/lcov.codecov.info') + + normalized_lines = [] + for line in source.read_text(encoding='utf-8').splitlines(): + if line.startswith('SF:'): + path = line[3:].replace('\\', '/') + marker = '/apps/web/' + if marker in path: + path = 'apps/web/' + path.split(marker, 1)[1] + elif not path.startswith('apps/web/'): + path = 'apps/web/' + path.lstrip('./') + line = f'SF:{path}' + normalized_lines.append(line) + + target.write_text('\n'.join(normalized_lines) + '\n', encoding='utf-8') + PY + + - name: Preview web coverage source paths + shell: bash + run: | + grep '^SF:' apps/web/coverage/lcov.codecov.info | head -n 20 + + - name: Upload web coverage artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: web-coverage + path: | + apps/web/coverage/lcov.info + apps/web/coverage/lcov.codecov.info + if-no-files-found: error + + - name: Upload web coverage to Codecov + if: ${{ env.CODECOV_TOKEN != '' }} + uses: codecov/codecov-action@v5 + with: + files: apps/web/coverage/lcov.codecov.info + flags: frontend + disable_search: true + token: ${{ env.CODECOV_TOKEN }} + fail_ci_if_error: false + + - name: Build web app + run: bun run build:web + + api: + name: API Checks + runs-on: ubuntu-latest + timeout-minutes: 20 + + defaults: + run: + working-directory: apps/api + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: apps/api/go.mod + cache-dependency-path: apps/api/go.sum + + - name: Check gofmt formatting + shell: bash + run: | + unformatted="$(gofmt -l .)" + if [ -n "$unformatted" ]; then + echo "The following Go files are not gofmt-formatted:" + echo "$unformatted" + exit 1 + fi + + - name: Run API tests with coverage + run: go test ./... -coverprofile=coverage.out -covermode=atomic + + - name: Print API coverage summary + run: go tool cover -func=coverage.out + + - name: Rewrite API coverage paths for Codecov + shell: bash + run: | + sed 's#^github.com/xdrop/monorepo/#apps/api/#' coverage.out > coverage.codecov.out + + - name: Upload API coverage artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: api-coverage + path: | + apps/api/coverage.out + apps/api/coverage.codecov.out + if-no-files-found: error + + - name: Upload API coverage to Codecov + if: ${{ env.CODECOV_TOKEN != '' }} + uses: codecov/codecov-action@v5 + with: + files: apps/api/coverage.codecov.out + flags: backend + disable_search: true + token: ${{ env.CODECOV_TOKEN }} + fail_ci_if_error: false + + - name: Build API binary + run: go build -o /tmp/xdrop-api ./cmd/api + + e2e: + name: Compose Smoke And E2E + runs-on: ubuntu-latest + timeout-minutes: 35 + needs: + - web + - api + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Playwright browser + run: bunx playwright install --with-deps chromium + + - name: Start Docker stack + run: docker compose ${{ env.COMPOSE_FILES }} up -d --build + + - name: Wait for Xdrop endpoints + shell: bash + run: | + for _ in {1..60}; do + if curl -fsS http://localhost:8080/healthz >/dev/null && curl -fsS http://localhost:8080/ >/dev/null; then + exit 0 + fi + sleep 5 + done + echo "Xdrop endpoints did not become ready in time" >&2 + exit 1 + + - name: Run browser E2E suite + run: bun run test:e2e + + - name: Capture Docker logs + if: always() + run: docker compose ${{ env.COMPOSE_FILES }} logs --no-color > compose-logs.txt + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: playwright-report + if-no-files-found: ignore + + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-test-results + path: test-results + if-no-files-found: ignore + + - name: Upload Docker logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: compose-logs + path: compose-logs.txt + if-no-files-found: ignore + + - name: Shut down Docker stack + if: always() + run: docker compose ${{ env.COMPOSE_FILES }} down -v diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..21a525e --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,22 @@ +name: Dependency Review + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + name: Review dependency changes + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Run dependency review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml new file mode 100644 index 0000000..6c5a275 --- /dev/null +++ b/.github/workflows/publish-images.yml @@ -0,0 +1,82 @@ +name: Publish Images + +on: + workflow_run: + workflows: + - CI + types: + - completed + branches: + - main + workflow_dispatch: + inputs: + confirm_publish: + description: 'Type publish to confirm a manual image release' + required: true + type: string + +concurrency: + group: publish-images-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + packages: write + id-token: write + +jobs: + publish: + name: Publish Xdrop + runs-on: ubuntu-latest + timeout-minutes: 30 + if: >- + ${{ + ( + github.event_name == 'workflow_dispatch' && + github.event.inputs.confirm_publish == 'publish' + ) || + github.event.workflow_run.conclusion == 'success' + }} + + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository_owner }}/xdrop + tags: | + type=raw,value=main + type=sha + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push image + uses: docker/build-push-action@v7 + with: + context: . + file: Dockerfile + push: true + platforms: linux/amd64 + build-args: | + VITE_SITE_URL=${{ vars.VITE_SITE_URL }} + VITE_API_BASE_URL=/api/v1 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true diff --git a/.github/workflows/security-audits.yml b/.github/workflows/security-audits.yml new file mode 100644 index 0000000..f496d4a --- /dev/null +++ b/.github/workflows/security-audits.yml @@ -0,0 +1,201 @@ +name: Security Audits + +on: + schedule: + - cron: '43 2 * * 1' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + bun-audit: + name: bun audit + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run bun audit + shell: bash + run: | + set -o pipefail + bun audit --audit-level=high --json | tee bun-audit.json + + - name: Upload bun audit report + if: always() + uses: actions/upload-artifact@v7 + with: + name: bun-audit-report + path: bun-audit.json + if-no-files-found: ignore + + govulncheck: + name: govulncheck + runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + working-directory: apps/api + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: apps/api/go.mod + cache-dependency-path: apps/api/go.sum + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Run govulncheck + shell: bash + run: | + set -o pipefail + govulncheck ./... | tee ../../govulncheck.txt + + - name: Upload govulncheck report + if: always() + uses: actions/upload-artifact@v7 + with: + name: govulncheck-report + path: govulncheck.txt + if-no-files-found: ignore + + report-status: + name: Report security audit status + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: + - bun-audit + - govulncheck + if: always() + + steps: + - name: Create or update failure issue + if: ${{ needs.bun-audit.result != 'success' || needs.govulncheck.result != 'success' }} + uses: actions/github-script@v8 + env: + BUN_AUDIT_RESULT: ${{ needs.bun-audit.result }} + GOVULNCHECK_RESULT: ${{ needs.govulncheck.result }} + ISSUE_LABEL: security-audit + ISSUE_TITLE: Security audits failing + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const label = process.env.ISSUE_LABEL + const title = process.env.ISSUE_TITLE + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}` + const body = [ + 'One or more scheduled security audit jobs failed.', + '', + `- Workflow run: ${runUrl}`, + `- bun audit: ${process.env.BUN_AUDIT_RESULT}`, + `- govulncheck: ${process.env.GOVULNCHECK_RESULT}`, + `- Trigger: ${context.eventName}`, + `- Commit: ${context.sha}`, + ].join('\n') + + try { + await github.rest.issues.getLabel({ owner, repo, name: label }) + } catch (error) { + if (error.status !== 404) { + throw error + } + + await github.rest.issues.createLabel({ + owner, + repo, + name: label, + color: 'b60205', + description: 'Failures from the scheduled security audits workflow', + }) + } + + const { data: issues } = await github.rest.issues.listForRepo({ + owner, + repo, + state: 'open', + labels: label, + per_page: 100, + }) + + const existing = issues.find((issue) => issue.title === title) + + if (existing) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: existing.number, + body, + }) + core.info(`Updated existing issue #${existing.number}.`) + return + } + + const { data: created } = await github.rest.issues.create({ + owner, + repo, + title, + body, + labels: [label], + }) + + core.info(`Created issue #${created.number}.`) + + - name: Close resolved failure issue + if: ${{ needs.bun-audit.result == 'success' && needs.govulncheck.result == 'success' }} + uses: actions/github-script@v8 + env: + ISSUE_LABEL: security-audit + ISSUE_TITLE: Security audits failing + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const label = process.env.ISSUE_LABEL + const title = process.env.ISSUE_TITLE + + const { data: issues } = await github.rest.issues.listForRepo({ + owner, + repo, + state: 'open', + labels: label, + per_page: 100, + }) + + const existing = issues.find((issue) => issue.title === title) + if (!existing) { + core.info('No open security audit failure issue to close.') + return + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: existing.number, + body: `Security audits recovered in ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}. Closing this alert.`, + }) + + await github.rest.issues.update({ + owner, + repo, + issue_number: existing.number, + state: 'closed', + }) + + core.info(`Closed issue #${existing.number}.`) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93bb52d --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules/ +dist/ +coverage/ +.env +.env.* +!.env.example +*.log +.DS_Store +Thumbs.db +apps/web/dist/ +apps/web/node_modules/ +apps/api/bin/ +apps/api/.cache/ +apps/api/coverage.out +tmp/ +output/ +.playwright-cli/ +playwright-report/ +test-results/ +.codex/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..92a93b5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +.codex +.git +coverage +dist +node_modules +output +playwright-report +test-results +tmp +apps/api/bin +apps/web/dist +apps/web/node_modules diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..10a4fd2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 100, + "proseWrap": "preserve", + "endOfLine": "lf" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..43067ad --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +`Xdrop` is a monorepo with a React frontend and Go API. Put browser code in `apps/web/src` (`app`, `components`, `features`, `lib`), public assets in `apps/web/public`, shared TypeScript helpers in `packages/shared/src`, and backend code in `apps/api` with the entrypoint at `apps/api/cmd/api` and domain packages under `apps/api/internal`. End-to-end tests live in `tests/e2e`, infra files in `infra`, and repo automation in `scripts` and `.github/workflows`. + +## Build, Test, and Development Commands + +Install JS dependencies once with `bun install --frozen-lockfile`. + +- `bun run dev:web`: start the Vite dev server for the frontend. +- `bun run build:web`: type-check and build the web app, then generate SEO assets. +- `bun run lint:web`: run ESLint on `apps/web`. +- `bun run test:web` / `bun run test:web:coverage`: run Vitest, with or without coverage output. +- `bun run test:e2e`: run the Playwright suite in `tests/e2e`. +- `bun run format` / `bun run format:check`: run Prettier plus Go formatting checks. +- `go test ./... -coverprofile=coverage.out -covermode=atomic` from `apps/api`: run API tests the same way CI does. +- `go run ./cmd/api` from `apps/api`: run the API locally. +- `docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build`: boot the full stack on `http://localhost:8080`. + +## Coding Style & Naming Conventions + +Follow `.editorconfig`: 2 spaces for most files, tabs for `.go`, UTF-8, LF endings. Prettier enforces no semicolons, single quotes, and a 100-column wrap. Use PascalCase for React components, `useX` for hooks, camelCase for TS utilities, and lowercase Go package names. Keep tests beside the code they cover. + +## Testing Guidelines + +Frontend unit tests use Vitest, Testing Library, and `src/test/setup.ts`; name them `*.test.ts` or `*.test.tsx`. Browser flows use Playwright in `tests/e2e/*.spec.ts`. Backend tests use Go’s `testing` package, with integration coverage already present in `*_integration_test.go`. CI uploads frontend and backend coverage to Codecov; avoid reducing backend coverage below the current 90% target. + +## Commit & Pull Request Guidelines + +Recent history follows Conventional Commit style such as `feat:`, `fix:`, `test:`, and `docs:`. Keep commit subjects short and imperative. PRs should explain user-visible impact, link the relevant issue, and list verification steps. Include screenshots or short recordings for UI changes, and call out config, Docker, or API contract changes explicitly. + +## Security & Configuration Tips + +Start from `.env.example` for local configuration and never commit real secrets. The web dev server proxies `/api` to `localhost:8080` and `/xdrop` to MinIO on `localhost:9000`, so keep those endpoints aligned when changing local setup. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3eebb00 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,109 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and maintainers pledge to make participation in the Xdrop community a +harassment-free experience for everyone, regardless of age, body size, visible or invisible +disability, ethnicity, sex characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, +and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- demonstrating empathy and kindness toward other people +- being respectful of differing opinions, viewpoints, and experiences +- giving and gracefully accepting constructive feedback +- taking responsibility, apologizing to those affected by our mistakes, and learning from the + experience +- focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- the use of sexualized language or imagery, and sexual attention or advances of any kind +- trolling, insulting or derogatory comments, and personal or political attacks +- public or private harassment +- publishing others' private information, such as a physical or email address, without their + explicit permission +- other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior +and will take appropriate and fair corrective action in response to any behavior that they deem +inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, +code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and +will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is +officially representing the community in public spaces. Examples of representing our community +include using an official email address, posting via an official social media account, or acting as +an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project +maintainer at [hi@xi-xu.me](mailto:hi@xi-xu.me). All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any +incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for +any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community impact:** Use of inappropriate language or other behavior deemed unprofessional or +unwelcome in the community. + +**Consequence:** A private, written warning from community leaders, providing clarity around the +nature of the violation and an explanation of why the behavior was inappropriate. A public apology +may be requested. + +### 2. Warning + +**Community impact:** A violation through a single incident or series of actions. + +**Consequence:** A warning with consequences for continued behavior. No interaction with the people +involved, including unsolicited interaction with those enforcing the Code of Conduct, for a +specified period of time. This includes avoiding interactions in community spaces as well as +external channels like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community impact:** A serious violation of community standards, including sustained inappropriate +behavior. + +**Consequence:** A temporary ban from any sort of interaction or public communication with the +community for a specified period of time. No public or private interaction with the people involved, +including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this +period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community impact:** Demonstrating a pattern of violation of community standards, including +sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement +of classes of individuals. + +**Consequence:** A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fec0951 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,148 @@ +# Contributing to Xdrop + +Thank you for helping improve Xdrop. + +This repository follows the guidance from the Open Source Guides: we try to be explicit about +scope, expectations, and how to participate so contributors do not waste time on changes that are +unlikely to land. + +## Project focus + +Xdrop is focused on private file transfer with browser-side encryption. + +Changes are most likely to be accepted when they improve one or more of these areas: + +- privacy or security of the transfer flow +- reliability of uploads, downloads, expiry, or cleanup +- accessibility, usability, or performance of the current product surface +- test coverage, documentation, or developer experience +- operational hardening of the existing React + Go + Docker stack + +Changes are less likely to be accepted if they significantly broaden the project into unrelated +product areas, add accounts/social features, or increase complexity without a clear privacy or +usability win. + +## Before you start + +- Read [README.md](README.md) for the current project status and local setup. +- Follow [MESSAGING.md](MESSAGING.md) when you touch README copy, homepage copy, SEO text, or + other product-facing messaging. +- Search existing issues and pull requests before opening a new one. +- For substantial features, architecture changes, or API contract changes, open an issue first. +- For vulnerabilities or sensitive security concerns, follow [SECURITY.md](SECURITY.md) instead of + filing a public issue. +- Keep general questions and troubleshooting in public channels when possible. See + [SUPPORT.md](SUPPORT.md). + +## Development setup + +### Prerequisites + +- Bun +- Go 1.26+ +- Docker with Compose + +### Install dependencies + +```bash +bun install --frozen-lockfile +``` + +Use `.env.example` as the reference list of supported settings. Creating a local `.env` is +optional for the default dev stack; only set variables when you want to override Docker or API +defaults. + +### Start the full stack + +```bash +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +This brings up Xdrop, Postgres, Redis, and MinIO at `http://localhost:8080`. + +### Run the frontend only + +```bash +bun run dev:web +``` + +### Run the API only + +```bash +cd apps/api +go run ./cmd/api +``` + +## What to run before opening a PR + +Choose the checks that match your change: + +### Frontend changes + +```bash +bun run lint:web +bun run typecheck:web +bun run test:web +``` + +### Backend changes + +```bash +cd apps/api +go test ./... -coverprofile=coverage.out -covermode=atomic +``` + +### Full-stack or user-flow changes + +```bash +bun run test:e2e +``` + +### Formatting + +```bash +bun run format:check +``` + +## Coding conventions + +- Follow `.editorconfig`, Prettier, and Go formatting defaults. +- Use PascalCase for React components, `useX` for hooks, camelCase for TypeScript utilities, and + lowercase package names in Go. +- Keep tests close to the code they cover. +- Prefer focused pull requests over broad drive-by refactors. +- Add or update tests when behavior changes. + +## Pull request expectations + +Please make it easy to review your change: + +- Use a short imperative commit message, ideally in Conventional Commit style (`feat:`, `fix:`, + `docs:`, `test:`). +- Explain the user-visible impact and any API, config, Docker, or data-model changes. +- List the verification steps you ran locally. +- Include screenshots or short recordings for UI changes. +- Update documentation when behavior, setup, or contributor workflows change. + +If a change is large, split it into smaller reviewable steps whenever possible. + +## Review and maintainer expectations + +- Maintainers make the final call on whether a change fits the project vision. +- We aim to acknowledge new issues and pull requests within 7 days, but this is a part-time + project, so response times can vary. +- If you have not heard back after a week, a polite follow-up in the same thread is welcome. +- A closed issue or PR is not a judgment on the contributor. It usually means the change does not + currently fit scope, timing, or maintenance capacity. + +## Good first contributions + +If you are new to the project, good places to help include: + +- improving docs and setup clarity +- adding or tightening frontend and API test coverage +- fixing accessibility issues +- polishing error states and recovery flows +- cleaning up rough edges in local development and CI + +Thanks again for taking the time to contribute. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11f09cf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM oven/bun:alpine AS web-build + +WORKDIR /workspace + +ARG VITE_SITE_URL="" +ARG VITE_API_BASE_URL="/api/v1" +ENV VITE_SITE_URL=${VITE_SITE_URL} +ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} + +COPY package.json bun.lock tsconfig.base.json ./ +COPY packages/shared ./packages/shared +COPY apps/web ./apps/web + +RUN bun install --frozen-lockfile +RUN bun run build:web + +FROM golang:1.26-alpine AS api-build + +WORKDIR /src/apps/api + +COPY apps/api/go.mod apps/api/go.sum ./ +RUN go mod download + +COPY apps/api ./ + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/xdrop-api ./cmd/api + +FROM nginx:1.29-alpine + +COPY infra/xdrop/nginx.conf.template /etc/nginx/templates/default.conf.template +COPY infra/xdrop/entrypoint.sh /usr/local/bin/xdrop-entrypoint +COPY --from=web-build /workspace/apps/web/dist /usr/share/nginx/html +COPY --from=api-build /out/xdrop-api /usr/local/bin/xdrop-api + +RUN chmod +x /usr/local/bin/xdrop-entrypoint + +EXPOSE 80 + +ENTRYPOINT ["/usr/local/bin/xdrop-entrypoint"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e20b431 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/MESSAGING.md b/MESSAGING.md new file mode 100644 index 0000000..46e154e --- /dev/null +++ b/MESSAGING.md @@ -0,0 +1,93 @@ +# Xdrop Messaging Guide + +Use this guide to keep README copy, homepage copy, SEO metadata, Open Graph assets, package +metadata, and social profiles aligned. + +## Core Positioning + +Canonical one-liner: + +`Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.` + +This is the default introduction for Xdrop. If a surface only gets one sentence, use this one. + +## Canonical Copy By Surface + +README first sentence: + +`Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.` + +Homepage H1: + +`Encrypted file transfer.` + +Homepage body: + +`Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.` + +Homepage and SEO title: + +`Open Source Encrypted File Transfer in the Browser | Xdrop` + +Meta description: + +`Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.` + +OG card headline: + +`Open source encrypted` + +`file transfer in your browser.` + +OG card support line: + +`Plaintext file names, contents, and keys stay off the server.` + +Short social bio: + +`Open source file transfer with in-browser encryption. Plaintext file names, contents, and keys stay off the server.` + +Short technical summary: + +`Browser-encrypted file transfer with plaintext kept off the server.` + +## Messaging Priorities + +When space is limited, keep these ideas in this order: + +1. Xdrop is open source. +2. Encryption happens in the browser. +3. Plaintext file names, contents, and keys stay off the server. +4. `No account required` is a useful supporting point, but not the main definition. + +## Preferred Language + +- Prefer `encrypts files in your browser` over `browser-side encryption` in user-facing copy. +- Prefer `keeps plaintext ... off the server` over `ciphertext-only storage` unless the audience is technical. +- Prefer `open source file transfer app` when introducing Xdrop for the first time. +- Prefer `in-browser encryption` as the compact form when space is tight. +- Use `AES-256-GCM` in technical docs, threat-model explanations, and implementation notes, not as the default marketing hook. + +## Avoid + +- Avoid using `private file transfer` as the only product summary. +- Avoid mixing `private`, `secure`, `browser-side`, and `ciphertext-only` as interchangeable main taglines. +- Avoid making `no account required` the primary headline. It is a benefit, not the core definition. +- Avoid shortening the promise to just `secure uploads` because it removes the browser and server model that makes Xdrop distinct. + +## Tone + +- Clear and specific over clever. +- Technical enough to be accurate, but readable for non-specialists. +- Calm and factual instead of hype-heavy. +- Confident about the architecture, careful about broader security claims. + +## Copy Review Checklist + +Before shipping new product-facing copy, check: + +- Does it describe Xdrop as open source? +- Does it say encryption happens in the browser? +- Does it make clear that plaintext names, contents, and keys stay off the server? +- Is `no account required` used as support rather than the core identity? +- Does it avoid introducing a new summary line that conflicts with the canonical one-liner? diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cfe4d8 --- /dev/null +++ b/README.md @@ -0,0 +1,414 @@ +

+ Xdrop +

+ +

+ + Codecov coverage + + + GitHub Actions CI status + + + CodeQL code scanning status + + + Container image publish status + +

+ +

+ English | 汉语 +

+ +Xdrop is an open source file transfer app that encrypts files in your browser and keeps +plaintext file names, contents, and keys off the server. + +## Highlights + +- End-to-end encryption in the browser before upload. +- Single-file and folder transfers, including local ZIP downloads for received folders. +- Resumable uploads with browser-local state for interrupted transfers. +- Expiring links, sender-side management, and optional privacy mode after upload. +- S3-compatible object storage support with PostgreSQL and Redis on the backend. + +## How It Works + +1. A sender creates a transfer in the browser. Xdrop generates a random transfer root key and a + separate link key, optionally strips removable image metadata, and prepares resumable local + state before upload begins. +2. The API creates the transfer record and returns a manage token plus upload limits. The browser + registers encrypted file metadata, then requests presigned chunk upload URLs in batches. + PostgreSQL stores transfer/file/chunk metadata, Redis enforces rate limits, and S3-compatible + storage keeps only encrypted blobs. +3. The sender shares a full link such as `/t/:transferId#k=...`. The `#k=...` fragment stays in + the browser and is used to unwrap the transfer root key locally. +4. During upload, file chunks are encrypted in a dedicated Web Worker and streamed to storage. + After every chunk is uploaded, the browser encrypts the manifest, uploads it, and finalizes the + transfer with the wrapped root key. +5. A recipient opens the link, fetches the encrypted manifest and chunk URLs, and decrypts the + transfer entirely in the browser. Folder downloads can be re-packed into a ZIP locally. +6. Background cleanup periodically removes expired or deleted transfer objects from storage. + +Xdrop keeps plaintext file names, paths, contents, and decryption keys off the server. The server +still sees operational metadata such as transfer timestamps, file counts, chunk counts, file +sizes, and rate-limit identifiers. + +Key technical details: + +- **Crypto model:** The browser generates 32-byte random secrets for the transfer root key and the + share-link key. HKDF-SHA-256 derives separate AES-256-GCM keys for the manifest and for each + file, and chunk encryption binds `transferId`, `fileId`, `chunkIndex`, size, and protocol + version as authenticated data. +- **Chunked uploads:** The server advertises chunk size, file-count, and transfer-size limits to + the browser. This repo defaults to 8 MiB chunks, up to 100 files, and a 256 MiB encrypted + transfer size cap. +- **Resume behavior:** Xdrop persists source files locally in OPFS when available and falls back + to IndexedDB-backed blobs when the staged data is still within the fallback storage limit. + Resume requests ask the API which chunks already exist so the browser only uploads missing work + after a refresh or reopen. +- **Sender controls:** The manage token is returned once on creation and stored as a SHA-256 hash + on the server. Privacy mode can scrub sender-side local controls after upload. +- **Backend responsibilities:** The API never decrypts payloads. It validates transfer state, + rate-limits endpoints, issues presigned URLs, stores metadata, and cleans up expired or deleted + objects from storage. + +## System Architecture + +```mermaid +flowchart LR + subgraph Sender["Sender browser"] + Select["Choose files or a folder"] + Worker["Crypto worker
AES-256-GCM + HKDF-SHA-256"] + Local["OPFS / IndexedDB
resume state and local controls"] + Browser["Browser app
React + upload/download runtime"] + Select --> Worker + Worker <--> Local + Browser <--> Worker + Browser <--> Local + end + + subgraph Edge["Default Xdrop deployment"] + nginx["nginx
serves SPA and proxies /api + /xdrop"] + API["Go API
transfer lifecycle, presigning, cleanup"] + nginx --> API + end + + Postgres["PostgreSQL
transfers, files, chunks, hashed manage tokens"] + Redis["Redis
rate limiting"] + Storage["S3-compatible storage
encrypted manifest and chunk objects"] + Receiver["Receiver browser
opens /t/:id#k=..."] + + Browser -->|create/register/finalize| nginx + Browser -->|presigned PUT uploads| nginx + API --> Postgres + API --> Redis + API -->|presigned PUT/GET URLs| Storage + nginx -->|/xdrop proxy| Storage + nginx -->|web app + public API| Receiver + Receiver -->|presigned GET downloads| nginx + Receiver -->|decrypts locally with #k fragment| Receiver +``` + +In the default Docker deployment, nginx serves the built frontend and proxies both `/api` and +`/xdrop`. If `S3_PUBLIC_ENDPOINT` points at a different public object-storage endpoint, presigned +upload and download requests can bypass the nginx proxy while the rest of the architecture stays +the same. + +## Deployment + +### Recommended Production Topology + +For a public deployment, run Xdrop behind a reverse proxy such as Caddy or nginx: + +- The reverse proxy terminates HTTPS for your public domain. +- The `xdrop` container listens on a loopback-only host port such as `127.0.0.1:8080`. +- MinIO should not be exposed publicly. Bind MinIO ports to `127.0.0.1` only unless you have + a specific reason to expose them. +- Set `S3_PUBLIC_ENDPOINT` and `ALLOWED_ORIGINS` to your public site URL, for example + `https://xdrop.example.com`. + +### Step 1: Get the Files + +If you only want to run the published image, you do not need to clone the whole repository on the +server. + +Download the required deployment files: + +```bash +mkdir -p xdrop/infra/minio +cd xdrop +curl -fsSL -o docker-compose.yml \ + https://github.com/xixu-me/xdrop/raw/refs/heads/main/docker-compose.yml +curl -fsSL -o infra/minio/init.sh \ + https://github.com/xixu-me/xdrop/raw/refs/heads/main/infra/minio/init.sh +chmod +x infra/minio/init.sh +``` + +Optionally, download [`.env.example`](./.env.example) as a reference for supported settings: + +```bash +curl -fsSL -o .env.example \ + https://github.com/xixu-me/xdrop/raw/refs/heads/main/.env.example +``` + +If you want to build your own image, clone the repository instead so Docker can use the full build +context. In most cases, it is better to build in CI or on a separate machine and only pull the +final image onto the server. + +### Step 2: Review Configuration + +Install Docker and Docker Compose on the server, then review the `xdrop` service environment in +`docker-compose.yml`. + +At minimum, update these values for your real deployment: + +- `S3_PUBLIC_ENDPOINT` +- `ALLOWED_ORIGINS` + +Typical production values look like this: + +```yaml +services: + minio: + ports: + - '127.0.0.1:9000:9000' + - '127.0.0.1:9001:9001' + + xdrop: + ports: + - '127.0.0.1:8080:80' + environment: + S3_PUBLIC_ENDPOINT: https://xdrop.example.com + ALLOWED_ORIGINS: https://xdrop.example.com +``` + +Treat `.env.example` as the reference list of supported settings. Changing `.env.example` alone +does not affect the running stack because the provided Compose file uses inline environment values. + +### Step 3: Use the Published Image + +```bash +docker compose up -d +``` + +This uses [`ghcr.io/xixu-me/xdrop:latest`](https://ghcr.io/xixu-me/xdrop). + +This is enough when the published image already matches the frontend settings you want. + +Important caveats: + +- Frontend build-time values such as `VITE_SITE_URL` are baked into the image. +- If your deployment uses a different public domain and you care about canonical URLs, Open Graph + metadata, JSON-LD, or sitemap generation, use your own rebuilt image instead of the published + one. + +### Step 4: Optional: Use Your Own Prebuilt Image + +```bash +XDROP_IMAGE=ghcr.io/your-org/xdrop:latest docker compose up -d +``` + +### Step 5: Optional: Build Your Own Image + +Build your own image when you need different frontend build-time settings: + +```bash +git clone https://github.com/xixu-me/xdrop.git +cd xdrop +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +Edit the build args in `docker-compose.build.yml` before you run that command. + +Example: + +```yaml +services: + xdrop: + build: + args: + VITE_SITE_URL: https://xdrop.example.com + VITE_API_BASE_URL: /api/v1 +``` + +On low-memory servers, building directly on the host may be slow or fail. In that case, build +elsewhere, push the image to a registry, and deploy it with `XDROP_IMAGE`. + +### Step 6: Put Xdrop Behind a Reverse Proxy + +Example `Caddyfile`: + +```caddyfile +xdrop.example.com { + encode gzip zstd + reverse_proxy 127.0.0.1:8080 +} +``` + +Then reload Caddy: + +```bash +systemctl reload caddy +``` + +After the stack starts, open . + +### Production Notes + +- The final container serves the built frontend with nginx and runs the Go API in the same + container. +- The stack includes `xdrop`, `postgres`, `redis`, `minio`, and the bucket bootstrap container. +- MinIO is intended to be private in the default single-host deployment. +- Public traffic should normally hit only the reverse proxy on ports `80` and `443`. + +## Development + +### Prerequisites + +- Bun +- Go 1.26+ +- Docker / Docker Compose + +### Step 1: Install Dependencies + +```bash +bun install --frozen-lockfile +``` + +### Step 2: Start Backing Services + +For local development, start PostgreSQL, Redis, and MinIO with Docker: + +```bash +docker compose up -d postgres redis minio minio-setup +``` + +### Step 3: Run the API + +```bash +cd apps/api +go run ./cmd/api +``` + +### Step 4: Run the Web App + +From the repo root in a second terminal: + +```bash +bun run dev:web +``` + +Open [http://localhost:5173](http://localhost:5173). During local development, the Vite dev +server proxies: + +- `/api` to `http://localhost:8080` +- `/xdrop` to `http://localhost:9000` + +This keeps frontend hot reload while talking to the local Go API and MinIO. + +## Testing + +### Web + +```bash +bun run lint:web +bun run typecheck:web +bun run test:web +bun run test:web:coverage +bun run build:web +``` + +### End-to-End + +Install Playwright browsers once if needed: + +```bash +bun run test:e2e:install +``` + +The E2E suite expects Xdrop at `http://localhost:8080` by default and uses the local `postgres` +and `redis` Compose services during the tests. Start the full stack first: + +```bash +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +Then run the suite: + +```bash +bun run test:e2e +``` + +Set `E2E_BASE_URL` and `E2E_API_URL` if you want to target a different environment. + +### API + +From `apps/api`: + +```bash +go test ./... -coverprofile=coverage.out -covermode=atomic +``` + +Some API integration tests use Docker-backed testcontainers. If Docker is unavailable, those tests +are skipped and coverage will be lower than CI. + +### Formatting + +```bash +bun run format +bun run format:check +``` + +## Project Structure + +```text +apps/ + api/ Go API + cmd/api/ API entrypoint + internal/ Domain packages + web/ React frontend + public/ Static assets + src/ App, components, features, and utilities +packages/ + shared/ Shared TypeScript constants and helpers + src/ Shared source files +tests/ + e2e/ Playwright end-to-end tests +infra/ Deployment and container configuration +scripts/ Repository automation and helper scripts +``` + +## Environment Variables + +See [`.env.example`](./.env.example) for the full list. The most important settings are: + +- `API_ADDR` +- `DATABASE_URL` +- `REDIS_ADDR` +- `S3_ENDPOINT` +- `S3_PUBLIC_ENDPOINT` +- `S3_BUCKET` +- `ALLOWED_ORIGINS` +- `VITE_API_BASE_URL` +- `VITE_SITE_URL` + +## License + +AGPL-3.0-only. See [`LICENSE`](./LICENSE). diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..93c8d5e --- /dev/null +++ b/README.zh.md @@ -0,0 +1,374 @@ +

+ Xdrop +

+ +

+ + Codecov coverage + + + GitHub Actions CI status + + + CodeQL code scanning status + + + Container image publish status + +

+ +

+ English | 汉语 +

+ +Xdrop 是一个开源文件传输应用,会在浏览器中先加密文件再上传,确保服务端拿不到明文文件名、文件内容和密钥。 + +## 亮点 + +- 在浏览器中完成端到端加密后再上传。 +- 支持单文件和文件夹传输,接收文件夹时也可在本地重新打包为 ZIP 下载。 +- 上传中断后可依赖浏览器本地状态继续断点续传。 +- 支持到期失效链接、发送方管理,以及上传后的可选隐私模式。 +- 后端支持兼容 S3 的对象存储,并使用 PostgreSQL 和 Redis。 + +## 工作原理 + +1. 发送方在浏览器中创建一次传输。Xdrop 会生成随机的传输根密钥和独立的链接密钥,可选地移除图片中可删除的元数据,并在上传开始前准备好可恢复的本地状态。 +2. API 创建传输记录,并返回管理令牌和上传限制。浏览器先注册加密后的文件元数据,再分批请求分块上传所需的预签名 URL。PostgreSQL 保存传输、文件和分块元数据,Redis 负责限流,兼容 S3 的对象存储只保存密文对象。 +3. 发送方分享完整链接,例如 `/t/:transferId#k=...`。其中 `#k=...` 片段只保留在浏览器端,用于在本地解开传输根密钥。 +4. 上传过程中,文件分块会在独立的 Web Worker 中加密并流式写入存储。每上传完一个分块,浏览器都会加密清单文件、上传清单,并在最后使用封装后的根密钥完成传输。 +5. 接收方打开链接后,会获取加密清单和分块 URL,并在浏览器中完成全部解密。文件夹下载时可以在本地重新打包成 ZIP。 +6. 后台清理任务会定期从存储中删除已过期或已删除的传输对象。 + +Xdrop 不会让服务端接触到明文文件名、路径、内容或解密密钥。服务端仍然能看到运行层面的元数据,例如传输时间戳、文件数量、分块数量、文件大小,以及限流标识符。 + +关键技术细节: + +- **加密模型:** 浏览器会为传输根密钥和分享链接密钥各生成一个 32 字节随机密钥。随后使用 HKDF-SHA-256 派生出清单和每个文件各自的 AES-256-GCM 密钥,并在分块加密时把 `transferId`、`fileId`、`chunkIndex`、大小和协议版本作为认证附加数据绑定进去。 +- **分块上传:** 服务端会向浏览器声明分块大小、文件数限制和传输总大小限制。当前存储库默认使用 8 MiB 分块,最多 100 个文件,加密后的传输总大小上限为 256 MiB。 +- **断点续传:** 当浏览器支持 OPFS 时,Xdrop 会把源文件持久化到本地;如果不可用,则在回退存储限制范围内使用基于 IndexedDB 的 Blob 存储。恢复上传时,浏览器会先询问 API 哪些分块已存在,因此即使刷新页面或重新打开浏览器,也只会补传缺失的部分。 +- **发送方控制:** 管理令牌只会在创建时返回一次,服务端保存的是它的 SHA-256 哈希。开启隐私模式后,发送方本地控制信息可在上传完成后被清除。 +- **后端职责:** API 从不解密载荷。它负责校验传输状态、限制速率、签发预签名 URL、存储元数据,并清理已过期或已删除的对象。 + +## 系统架构 + +```mermaid +flowchart LR + subgraph Sender["发送方浏览器"] + Select["选择文件或文件夹"] + Worker["加密 Worker
AES-256-GCM + HKDF-SHA-256"] + Local["OPFS / IndexedDB
断点状态与本地控制信息"] + Browser["浏览器应用
React + 上传/下载运行时"] + Select --> Worker + Worker <--> Local + Browser <--> Worker + Browser <--> Local + end + + subgraph Edge["默认 Xdrop 部署"] + nginx["nginx
提供 SPA 并代理 /api + /xdrop"] + API["Go API
传输生命周期、预签名、清理"] + nginx --> API + end + + Postgres["PostgreSQL
transfers、files、chunks、管理令牌哈希"] + Redis["Redis
限流"] + Storage["兼容 S3 的对象存储
加密清单与分块对象"] + Receiver["接收方浏览器
打开 /t/:id#k=..."] + + Browser -->|create/register/finalize| nginx + Browser -->|presigned PUT uploads| nginx + API --> Postgres + API --> Redis + API -->|presigned PUT/GET URLs| Storage + nginx -->|/xdrop proxy| Storage + nginx -->|web app + public API| Receiver + Receiver -->|presigned GET downloads| nginx + Receiver -->|decrypts locally with #k fragment| Receiver +``` + +在默认的 Docker 部署中,nginx 会提供构建后的前端资源,同时代理 `/api` 和 `/xdrop`。如果 `S3_PUBLIC_ENDPOINT` 指向另一个公开可访问的对象存储端点,那么预签名上传和下载请求可以绕过 nginx 代理,其余架构保持不变。 + +## 部署 + +### 推荐的生产拓扑 + +面向公网部署时,建议把 Xdrop 放在 Caddy 或 nginx 之类的反向代理后面: + +- 反向代理为你的公网域名终止 HTTPS。 +- `xdrop` 容器只监听回环地址上的主机端口,例如 `127.0.0.1:8080`。 +- 默认不建议公开暴露 MinIO。除非你有明确理由,否则应仅将 MinIO 端口绑定到 `127.0.0.1`。 +- 将 `S3_PUBLIC_ENDPOINT` 和 `ALLOWED_ORIGINS` 设置为你的公开站点 URL,例如 `https://xdrop.example.com`。 + +### 第一步:获取文件 + +如果你只想运行已经发布的镜像,就不需要在服务器上克隆整个存储库。 + +下载部署所需文件: + +```bash +mkdir -p xdrop/infra/minio +cd xdrop +curl -fsSL -o docker-compose.yml \ + https://github.com/xixu-me/xdrop/raw/refs/heads/main/docker-compose.yml +curl -fsSL -o infra/minio/init.sh \ + https://github.com/xixu-me/xdrop/raw/refs/heads/main/infra/minio/init.sh +chmod +x infra/minio/init.sh +``` + +你也可以额外下载 [`.env.example`](./.env.example) 作为支持配置项的参考: + +```bash +curl -fsSL -o .env.example \ + https://github.com/xixu-me/xdrop/raw/refs/heads/main/.env.example +``` + +如果你打算自行构建镜像,请改为克隆存储库,以便 Docker 拿到完整的构建上下文。通常更推荐在 CI 或另一台机器上完成构建,再把最终镜像拉到服务器上部署。 + +### 第二步:检查配置 + +在服务器上安装 Docker 和 Docker Compose,然后检查 `docker-compose.yml` 中 `xdrop` 服务的环境变量。 + +至少需要把这些值替换成真实部署所用配置: + +- `S3_PUBLIC_ENDPOINT` +- `ALLOWED_ORIGINS` + +典型的生产配置如下: + +```yaml +services: + minio: + ports: + - '127.0.0.1:9000:9000' + - '127.0.0.1:9001:9001' + + xdrop: + ports: + - '127.0.0.1:8080:80' + environment: + S3_PUBLIC_ENDPOINT: https://xdrop.example.com + ALLOWED_ORIGINS: https://xdrop.example.com +``` + +请把 `.env.example` 视为支持配置项的参考清单。仅修改 `.env.example` 不会影响运行中的栈,因为存储库附带的 Compose 文件使用的是内联环境变量。 + +### 第三步:使用已发布镜像 + +```bash +docker compose up -d +``` + +该命令会使用 [`ghcr.io/xixu-me/xdrop:latest`](https://ghcr.io/xixu-me/xdrop)。 + +如果已发布镜像已经符合你的前端配置需求,这一步就足够了。 + +需要注意: + +- `VITE_SITE_URL` 这类前端构建期变量会被直接烘焙进镜像。 +- 如果你的部署使用不同的公网域名,并且你在意 canonical URL、Open Graph 元数据、JSON-LD 或 sitemap 生成,请不要直接使用已发布镜像,而应自行重建镜像。 + +### 第四步:可选,使用你自己的预构建镜像 + +```bash +XDROP_IMAGE=ghcr.io/your-org/xdrop:latest docker compose up -d +``` + +### 第五步:可选,自行构建镜像 + +当你需要不同的前端构建期配置时,请自行构建镜像: + +```bash +git clone https://github.com/xixu-me/xdrop.git +cd xdrop +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +运行前请先修改 `docker-compose.build.yml` 中的构建参数。 + +例如: + +```yaml +services: + xdrop: + build: + args: + VITE_SITE_URL: https://xdrop.example.com + VITE_API_BASE_URL: /api/v1 +``` + +在低内存服务器上直接构建可能会很慢甚至失败。这种情况下,建议在其他机器完成构建,把镜像推送到存储库后,再通过 `XDROP_IMAGE` 部署。 + +### 第六步:把 Xdrop 放到反向代理后面 + +`Caddyfile` 示例: + +```caddyfile +xdrop.example.com { + encode gzip zstd + reverse_proxy 127.0.0.1:8080 +} +``` + +然后重载 Caddy: + +```bash +systemctl reload caddy +``` + +栈启动后,访问 。 + +### 生产环境说明 + +- 最终容器会用 nginx 提供构建后的前端,并在同一容器中运行 Go API。 +- 整个栈包含 `xdrop`、`postgres`、`redis`、`minio` 以及初始化 bucket 的容器。 +- 在默认单机部署中,MinIO 设计为私有服务。 +- 公网流量通常只应访问反向代理暴露的 `80` 和 `443` 端口。 + +## 开发 + +### 前置要求 + +- Bun +- Go 1.26+ +- Docker / Docker Compose + +### 第一步:安装依赖 + +```bash +bun install --frozen-lockfile +``` + +### 第二步:启动基础服务 + +本地开发时,用 Docker 启动 PostgreSQL、Redis 和 MinIO: + +```bash +docker compose up -d postgres redis minio minio-setup +``` + +### 第三步:运行 API + +```bash +cd apps/api +go run ./cmd/api +``` + +### 第四步:运行 Web 应用 + +在第二个终端里,从存储库根目录执行: + +```bash +bun run dev:web +``` + +打开 [http://localhost:5173](http://localhost:5173)。本地开发时,Vite 开发服务器会代理: + +- `/api` 到 `http://localhost:8080` +- `/xdrop` 到 `http://localhost:9000` + +这样既能保留前端热更新,也能连接本地 Go API 和 MinIO。 + +## 测试 + +### Web + +```bash +bun run lint:web +bun run typecheck:web +bun run test:web +bun run test:web:coverage +bun run build:web +``` + +### 端到端测试 + +如果还没安装 Playwright 浏览器,先执行一次: + +```bash +bun run test:e2e:install +``` + +E2E 套件默认会访问 `http://localhost:8080` 上的 Xdrop,并在测试期间使用本地 Compose 启动的 `postgres` 和 `redis` 服务。请先启动完整栈: + +```bash +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +然后运行测试: + +```bash +bun run test:e2e +``` + +如果你想指向其他环境,请设置 `E2E_BASE_URL` 和 `E2E_API_URL`。 + +### API + +在 `apps/api` 目录下执行: + +```bash +go test ./... -coverprofile=coverage.out -covermode=atomic +``` + +部分 API 集成测试会使用基于 Docker 的 testcontainers。如果 Docker 不可用,这些测试会被跳过,覆盖率也会低于 CI。 + +### 格式化 + +```bash +bun run format +bun run format:check +``` + +## 项目结构 + +```text +apps/ + api/ Go API + cmd/api/ API 入口 + internal/ 领域包 + web/ React 前端 + public/ 静态资源 + src/ 应用、组件、功能模块与工具代码 +packages/ + shared/ 共享 TypeScript 常量与辅助函数 + src/ 共享源码 +tests/ + e2e/ Playwright 端到端测试 +infra/ 部署与容器配置 +scripts/ 存储库自动化与辅助脚本 +``` + +## 环境变量 + +完整列表请查看 [`.env.example`](./.env.example)。其中最重要的设置包括: + +- `API_ADDR` +- `DATABASE_URL` +- `REDIS_ADDR` +- `S3_ENDPOINT` +- `S3_PUBLIC_ENDPOINT` +- `S3_BUCKET` +- `ALLOWED_ORIGINS` +- `VITE_API_BASE_URL` +- `VITE_SITE_URL` + +## 许可证 + +AGPL-3.0-only。详见 [`LICENSE`](./LICENSE)。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3b8df17 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,46 @@ +# Security Policy + +## Supported versions + +Xdrop is pre-`1.0`, so security fixes are only guaranteed on the latest development line. + +| Version or branch | Supported | +| ----------------------------------------- | --------- | +| `main` | Yes | +| older commits, branches, and ad-hoc forks | No | + +## Reporting a vulnerability + +Please do not report security issues in public GitHub issues, discussions, or pull requests. + +Instead, email [hi@xi-xu.me](mailto:hi@xi-xu.me) with the subject line `Xdrop security report`. + +Include as much of the following as you can: + +- a description of the issue and the impacted component +- reproduction steps or a proof of concept +- the potential impact and any assumptions required for exploitation +- whether the issue affects confidentiality, integrity, availability, or key handling +- any suggested fix or mitigation, if you have one + +If your report involves share links, uploaded files, or secrets, sanitize them before sending. +Never post real `#k=` fragments in public places. + +## What to expect + +- We aim to acknowledge reports within 72 hours. +- We aim to provide an initial assessment within 7 days. +- We may ask for more detail, a smaller reproduction, or time to validate a fix. +- We will coordinate disclosure timing with the reporter when a report is confirmed. + +## Scope notes + +Xdrop is a file transfer system with browser-side encryption. Security-sensitive areas include: + +- key generation, wrapping, and fragment handling +- client-side encryption and decryption flows +- manifest and chunk storage behavior +- share-link lifecycle, expiry, and delete controls +- secrets, environment configuration, and deployment defaults + +Operational or configuration questions that are not vulnerabilities belong in [SUPPORT.md](SUPPORT.md). diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..435404d --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,61 @@ +# Support + +Thanks for using Xdrop. + +To keep answers discoverable and to align with open source best practices, please prefer public +support channels for non-sensitive questions. + +## Where to ask what + +### Bug reports + +Use the bug report issue form when you can reproduce a defect in the current behavior. + +Include: + +- what you expected to happen +- what actually happened +- reproduction steps +- environment details +- logs or screenshots when relevant + +### Feature requests + +Use the feature request issue form for ideas that fit Xdrop's current scope. + +For larger changes, explain the problem first. A proposal is much easier to review when it starts +with the user or maintainer pain it solves. + +### Questions and troubleshooting + +Use the question issue form for setup problems, workflow questions, or help understanding current +behavior. + +Please share enough context for someone else to help: + +- what you are trying to do +- what command or page you used +- what you already tried +- exact error output when available + +### Security issues + +Do not file public issues for vulnerabilities, exposed secrets, or anything that could put users at +risk. + +Follow [SECURITY.md](SECURITY.md) and email [hi@xi-xu.me](mailto:hi@xi-xu.me) instead. + +## Before you open an issue + +- search existing issues and pull requests first +- use the issue template that best matches your request +- keep the conversation in the issue thread instead of moving to private messages +- remove sensitive values, credentials, and full share links from logs and screenshots + +## Response expectations + +Xdrop is maintained on a best-effort basis. Maintainers will try to respond promptly, but there is +no guaranteed SLA. + +If a thread has been quiet for more than 7 days and you have new information, a polite follow-up is +welcome. diff --git a/apps/api/cmd/api/app.go b/apps/api/cmd/api/app.go new file mode 100644 index 0000000..ff8a997 --- /dev/null +++ b/apps/api/cmd/api/app.go @@ -0,0 +1,302 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + nethttp "net/http" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/redis/go-redis/v9" + "github.com/xdrop/monorepo/internal/config" + apihttp "github.com/xdrop/monorepo/internal/http" + "github.com/xdrop/monorepo/internal/jobs" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" + "github.com/xdrop/monorepo/internal/service" + "github.com/xdrop/monorepo/internal/storage" +) + +type appError struct { + message string + err error +} + +func (e *appError) Error() string { + return fmt.Sprintf("%s: %v", e.message, e.err) +} + +func (e *appError) Unwrap() error { + return e.err +} + +type dbHandle interface { + Ping(context.Context) error + Close() +} + +// redisHandle captures the small subset of Redis client behavior the runtime needs. +type redisHandle interface { + Close() error +} + +// httpServer abstracts the concrete HTTP server so tests can replace network IO. +type httpServer interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +// appRuntime groups the constructed service graph and its coordinated cleanup. +type appRuntime struct { + service *service.Service + close func() +} + +// buildHooks packages side-effecting dependencies for runtime construction tests. +type buildHooks struct { + openDB func(context.Context, string) (dbHandle, error) + waitForDB func(context.Context, dbHandle) error + runMigrations func(context.Context, dbHandle) error + openStorage func(context.Context, config.Config) (storage.ObjectStorage, error) + openRedis func(config.Config) redisHandle + newLimiter func(redisHandle) ratelimit.Limiter + newRepository func(dbHandle) repo.Repository +} + +// appHooks packages top-level startup steps for CLI tests. +type appHooks struct { + loadConfig func() (config.Config, error) + buildRuntime func(context.Context, config.Config) (*appRuntime, error) + newServer func(config.Config, *slog.Logger, *service.Service) httpServer + startCleanup func(context.Context, *slog.Logger, time.Duration, *service.Service) +} + +type pgxPoolHandle struct { + *pgxpool.Pool +} + +type redisClientHandle struct { + *redis.Client +} + +// defaultBuildHooks returns the production dependency graph for runtime construction. +func defaultBuildHooks() buildHooks { + return buildHooks{ + openDB: openPostgresDB, + waitForDB: waitForPostgres, + runMigrations: runPostgresMigrations, + openStorage: openObjectStorage, + openRedis: openRedisClient, + newLimiter: newRedisLimiter, + newRepository: newPostgresRepository, + } +} + +// defaultAppHooks returns the production wiring used by the CLI entrypoint. +func defaultAppHooks() appHooks { + return appHooks{ + loadConfig: config.Load, + buildRuntime: buildDefaultRuntime, + newServer: newHTTPServer, + startCleanup: jobs.StartCleanup, + } +} + +// buildDefaultRuntime builds the production service graph with the default dependency set. +func buildDefaultRuntime(ctx context.Context, cfg config.Config) (*appRuntime, error) { + return buildRuntime(ctx, cfg, defaultBuildHooks()) +} + +func openPostgresDB(ctx context.Context, databaseURL string) (dbHandle, error) { + pool, err := pgxpool.New(ctx, databaseURL) + if err != nil { + return nil, err + } + return &pgxPoolHandle{Pool: pool}, nil +} + +func waitForPostgres(ctx context.Context, db dbHandle) error { + return retry(ctx, 30, 2*time.Second, func() error { + return db.Ping(ctx) + }) +} + +func runPostgresMigrations(ctx context.Context, db dbHandle) error { + handle, ok := db.(*pgxPoolHandle) + if !ok { + return fmt.Errorf("unexpected db handle type %T", db) + } + return repo.RunMigrations(ctx, handle.Pool) +} + +func openObjectStorage(ctx context.Context, cfg config.Config) (storage.ObjectStorage, error) { + return storage.NewS3Storage(ctx, storage.Config{ + Endpoint: cfg.S3Endpoint, + PublicEndpoint: cfg.S3PublicEndpoint, + Region: cfg.S3Region, + Bucket: cfg.S3Bucket, + AccessKey: cfg.S3AccessKey, + SecretKey: cfg.S3SecretKey, + UseSSL: cfg.S3UseSSL, + }) +} + +func openRedisClient(cfg config.Config) redisHandle { + return &redisClientHandle{Client: redis.NewClient(&redis.Options{ + Addr: cfg.RedisAddr, + Password: cfg.RedisPassword, + DB: cfg.RedisDB, + })} +} + +func newRedisLimiter(client redisHandle) ratelimit.Limiter { + handle, ok := client.(*redisClientHandle) + if !ok { + return nil + } + return ratelimit.NewRedisLimiter(handle.Client) +} + +func newPostgresRepository(db dbHandle) repo.Repository { + handle, ok := db.(*pgxPoolHandle) + if !ok { + return nil + } + return repo.NewPostgresRepository(handle.Pool) +} + +// buildRuntime connects the database, storage, rate limiter, and service layer. +func buildRuntime(ctx context.Context, cfg config.Config, hooks buildHooks) (*appRuntime, error) { + db, err := hooks.openDB(ctx, cfg.DatabaseURL) + if err != nil { + return nil, &appError{message: "connect postgres", err: err} + } + + closeDB := true + defer func() { + if closeDB { + db.Close() + } + }() + + waitForDB := hooks.waitForDB + if waitForDB == nil { + waitForDB = func(ctx context.Context, db dbHandle) error { + return db.Ping(ctx) + } + } + + if err := waitForDB(ctx, db); err != nil { + return nil, &appError{message: "ping postgres", err: err} + } + + if err := hooks.runMigrations(ctx, db); err != nil { + return nil, &appError{message: "run migrations", err: err} + } + + objectStorage, err := hooks.openStorage(ctx, cfg) + if err != nil { + return nil, &appError{message: "init object storage", err: err} + } + if err := objectStorage.EnsureBucket(ctx); err != nil { + return nil, &appError{message: "ensure bucket", err: err} + } + + redisClient := hooks.openRedis(cfg) + closeDB = false + + return &appRuntime{ + service: service.New(cfg, hooks.newRepository(db), objectStorage, hooks.newLimiter(redisClient)), + close: func() { + _ = redisClient.Close() + db.Close() + }, + }, nil +} + +// runMain loads configuration, builds the runtime, and blocks until the server stops. +func runMain(ctx context.Context, cancel context.CancelFunc, logger *slog.Logger, hooks appHooks) error { + cfg, err := hooks.loadConfig() + if err != nil { + return &appError{message: "load config", err: err} + } + + runtime, err := hooks.buildRuntime(ctx, cfg) + if err != nil { + return err + } + defer runtime.close() + + runServer(ctx, cancel, logger, cfg, runtime.service, hooks.newServer, hooks.startCleanup) + return nil +} + +// runServer starts background cleanup, serves HTTP traffic, and shuts down gracefully. +func runServer( + ctx context.Context, + cancel context.CancelFunc, + logger *slog.Logger, + cfg config.Config, + svc *service.Service, + newServer func(config.Config, *slog.Logger, *service.Service) httpServer, + startCleanup func(context.Context, *slog.Logger, time.Duration, *service.Service), +) { + if startCleanup != nil { + go startCleanup(ctx, logger, cfg.CleanupInterval, svc) + } + + server := newServer(cfg, logger, svc) + serverDone := make(chan error, 1) + + go func() { + logger.Info("api listening", "addr", cfg.Addr) + if err := server.ListenAndServe(); err != nil && err != nethttp.ErrServerClosed { + logger.Error("listen failed", "error", err) + cancel() + serverDone <- err + return + } + + serverDone <- nil + }() + + <-ctx.Done() + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + if err := server.Shutdown(shutdownCtx); err != nil { + logger.Error("shutdown failed", "error", err) + } + + <-serverDone +} + +// newHTTPServer constructs the concrete net/http server used in production. +func newHTTPServer(cfg config.Config, logger *slog.Logger, svc *service.Service) httpServer { + return &nethttp.Server{ + Addr: cfg.Addr, + Handler: apihttp.NewRouter(cfg, logger, svc), + ReadHeaderTimeout: 10 * time.Second, + } +} + +// retry keeps polling a dependency until it succeeds, the context is canceled, or attempts run out. +func retry(ctx context.Context, attempts int, delay time.Duration, fn func() error) error { + var lastErr error + for attempt := 0; attempt < attempts; attempt++ { + if err := fn(); err == nil { + return nil + } else { + lastErr = err + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(delay): + } + } + + return lastErr +} diff --git a/apps/api/cmd/api/app_test.go b/apps/api/cmd/api/app_test.go new file mode 100644 index 0000000..eeb1205 --- /dev/null +++ b/apps/api/cmd/api/app_test.go @@ -0,0 +1,515 @@ +package main + +import ( + "context" + "errors" + "io" + "log/slog" + nethttp "net/http" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" + "github.com/xdrop/monorepo/internal/service" + "github.com/xdrop/monorepo/internal/storage" +) + +func TestRunMainHandlesLoadConfigErrors(t *testing.T) { + t.Parallel() + + err := runMain(context.Background(), func() {}, slog.New(slog.NewTextHandler(io.Discard, nil)), appHooks{ + loadConfig: func() (config.Config, error) { + return config.Config{}, errors.New("broken config") + }, + buildRuntime: func(context.Context, config.Config) (*appRuntime, error) { + t.Fatal("buildRuntime should not be called") + return nil, nil + }, + }) + + require.ErrorContains(t, err, "load config") +} + +func TestRunMainBuildsRuntimeRunsServerAndClosesRuntime(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + cfg := config.Config{Addr: ":8080"} + server := newFakeServer() + closed := false + cleanupStarted := make(chan struct{}, 1) + + err := runMain(ctx, cancel, slog.New(slog.NewTextHandler(io.Discard, nil)), appHooks{ + loadConfig: func() (config.Config, error) { + return cfg, nil + }, + buildRuntime: func(context.Context, config.Config) (*appRuntime, error) { + return &appRuntime{ + service: nil, + close: func() { + closed = true + }, + }, nil + }, + newServer: func(config.Config, *slog.Logger, *service.Service) httpServer { + return server + }, + startCleanup: func(context.Context, *slog.Logger, time.Duration, *service.Service) { + cleanupStarted <- struct{}{} + }, + }) + + require.NoError(t, err) + require.True(t, closed) + require.True(t, server.shutdownCalled) + select { + case <-cleanupStarted: + case <-time.After(time.Second): + t.Fatal("expected cleanup loop to start") + } +} + +func TestDefaultHooksAndHelpers(t *testing.T) { + t.Parallel() + + t.Run("app error unwrap", func(t *testing.T) { + t.Parallel() + + cause := errors.New("root cause") + err := &appError{message: "outer", err: cause} + require.ErrorIs(t, err, cause) + }) + + t.Run("default hooks are wired", func(t *testing.T) { + t.Parallel() + + builder := defaultBuildHooks() + require.NotNil(t, builder.openDB) + require.NotNil(t, builder.waitForDB) + require.NotNil(t, builder.runMigrations) + require.NotNil(t, builder.openStorage) + require.NotNil(t, builder.openRedis) + require.NotNil(t, builder.newLimiter) + require.NotNil(t, builder.newRepository) + + hooks := defaultAppHooks() + require.NotNil(t, hooks.loadConfig) + require.NotNil(t, hooks.buildRuntime) + require.NotNil(t, hooks.newServer) + require.NotNil(t, hooks.startCleanup) + + require.NoError(t, waitForPostgres(context.Background(), &fakeDB{})) + require.ErrorContains(t, runPostgresMigrations(context.Background(), &fakeDB{}), "unexpected db handle type") + + pool, err := pgxpool.New(context.Background(), "postgres://xdrop:xdrop@127.0.0.1:1/xdrop?sslmode=disable&connect_timeout=1") + require.NoError(t, err) + t.Cleanup(pool.Close) + require.Error(t, runPostgresMigrations(context.Background(), &pgxPoolHandle{Pool: pool})) + + store, err := openObjectStorage(context.Background(), config.Config{ + S3Endpoint: "http://localhost:9000", + S3PublicEndpoint: "http://localhost:9000", + S3Region: "us-east-1", + S3Bucket: "xdrop", + S3AccessKey: "minioadmin", + S3SecretKey: "minioadmin", + }) + require.NoError(t, err) + require.NotNil(t, store) + + redisClient := openRedisClient(config.Config{RedisAddr: "localhost:6379"}) + require.NotNil(t, redisClient) + require.NotNil(t, newRedisLimiter(redisClient)) + require.Nil(t, newRedisLimiter(&fakeRedis{})) + + require.NotNil(t, newPostgresRepository(&pgxPoolHandle{})) + require.Nil(t, newPostgresRepository(&fakeDB{})) + + db, err := openPostgresDB(context.Background(), "postgres://xdrop:xdrop@localhost:5432/xdrop?sslmode=disable") + require.NoError(t, err) + db.Close() + + _, err = buildDefaultRuntime(context.Background(), config.Config{DatabaseURL: "postgres://%zz"}) + require.Error(t, err) + }) + + t.Run("logger and server helpers", func(t *testing.T) { + t.Parallel() + + logger := newLogger(io.Discard) + require.NotNil(t, logger) + + server := newHTTPServer(config.Config{Addr: ":8080"}, logger, nil) + httpServer, ok := server.(*nethttp.Server) + require.True(t, ok) + require.Equal(t, ":8080", httpServer.Addr) + require.Equal(t, 10*time.Second, httpServer.ReadHeaderTimeout) + require.NotNil(t, httpServer.Handler) + }) +} + +func TestBuildRuntimeHandlesFailuresAndSuccess(t *testing.T) { + t.Parallel() + + baseConfig := config.Config{ + DatabaseURL: "postgres://example.test/xdrop", + } + + t.Run("connect postgres failure", func(t *testing.T) { + t.Parallel() + + _, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return nil, errors.New("dial failed") + }, + }) + require.ErrorContains(t, err, "connect postgres") + }) + + t.Run("ping failure closes database", func(t *testing.T) { + t.Parallel() + + db := &fakeDB{pingErr: errors.New("ping failed")} + _, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return db, nil + }, + waitForDB: func(context.Context, dbHandle) error { + return db.Ping(context.Background()) + }, + }) + require.ErrorContains(t, err, "ping postgres") + require.True(t, db.closed) + }) + + t.Run("migration and storage failures close database", func(t *testing.T) { + t.Parallel() + + t.Run("migration", func(t *testing.T) { + db := &fakeDB{} + _, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return db, nil + }, + waitForDB: func(context.Context, dbHandle) error { + return nil + }, + runMigrations: func(context.Context, dbHandle) error { + return errors.New("migration failed") + }, + }) + require.ErrorContains(t, err, "run migrations") + require.True(t, db.closed) + }) + + t.Run("ensure bucket", func(t *testing.T) { + db := &fakeDB{} + store := &fakeObjectStorage{ensureBucketErr: errors.New("bucket failed")} + _, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return db, nil + }, + waitForDB: func(context.Context, dbHandle) error { + return nil + }, + runMigrations: func(context.Context, dbHandle) error { + return nil + }, + openStorage: func(context.Context, config.Config) (storage.ObjectStorage, error) { + return store, nil + }, + }) + require.ErrorContains(t, err, "ensure bucket") + require.True(t, db.closed) + require.True(t, store.ensureBucketCalled) + }) + + t.Run("open storage", func(t *testing.T) { + db := &fakeDB{} + _, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return db, nil + }, + waitForDB: func(context.Context, dbHandle) error { + return nil + }, + runMigrations: func(context.Context, dbHandle) error { + return nil + }, + openStorage: func(context.Context, config.Config) (storage.ObjectStorage, error) { + return nil, errors.New("storage failed") + }, + }) + require.ErrorContains(t, err, "init object storage") + require.True(t, db.closed) + }) + }) + + t.Run("nil waitForDB falls back to Ping", func(t *testing.T) { + t.Parallel() + + db := &fakeDB{} + redisClient := &fakeRedis{} + store := &fakeObjectStorage{} + + runtime, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return db, nil + }, + runMigrations: func(context.Context, dbHandle) error { + return nil + }, + openStorage: func(context.Context, config.Config) (storage.ObjectStorage, error) { + return store, nil + }, + openRedis: func(config.Config) redisHandle { + return redisClient + }, + newLimiter: func(redisHandle) ratelimit.Limiter { + return nil + }, + newRepository: func(dbHandle) repo.Repository { + return nil + }, + }) + require.NoError(t, err) + require.Equal(t, 1, db.pingCalls) + + runtime.close() + }) + + t.Run("success closes redis and database", func(t *testing.T) { + t.Parallel() + + db := &fakeDB{} + redisClient := &fakeRedis{} + store := &fakeObjectStorage{} + + runtime, err := buildRuntime(context.Background(), baseConfig, buildHooks{ + openDB: func(context.Context, string) (dbHandle, error) { + return db, nil + }, + waitForDB: func(context.Context, dbHandle) error { + return nil + }, + runMigrations: func(context.Context, dbHandle) error { + return nil + }, + openStorage: func(context.Context, config.Config) (storage.ObjectStorage, error) { + return store, nil + }, + openRedis: func(config.Config) redisHandle { + return redisClient + }, + newLimiter: func(redisHandle) ratelimit.Limiter { + return nil + }, + newRepository: func(dbHandle) repo.Repository { + return nil + }, + }) + require.NoError(t, err) + require.NotNil(t, runtime) + require.True(t, store.ensureBucketCalled) + require.False(t, db.closed) + require.False(t, redisClient.closed) + + runtime.close() + require.True(t, db.closed) + require.True(t, redisClient.closed) + }) +} + +func TestRunServerStartsCleanupAndShutsDownOnCancel(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + server := newFakeServer() + cleanupStarted := make(chan struct{}, 1) + done := make(chan struct{}) + + go func() { + runServer( + ctx, + cancel, + slog.New(slog.NewTextHandler(io.Discard, nil)), + config.Config{Addr: ":8080", CleanupInterval: time.Second}, + nil, + func(config.Config, *slog.Logger, *service.Service) httpServer { return server }, + func(context.Context, *slog.Logger, time.Duration, *service.Service) { cleanupStarted <- struct{}{} }, + ) + close(done) + }() + + <-server.listenStarted + cancel() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("runServer did not return after cancellation") + } + + require.True(t, server.shutdownCalled) + select { + case <-cleanupStarted: + case <-time.After(time.Second): + t.Fatal("expected cleanup loop to start") + } +} + +func TestRunServerCancelsContextWhenListenFails(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + server := &fakeServer{listenErr: errors.New("bind failed"), listenStarted: make(chan struct{})} + done := make(chan struct{}) + + go func() { + runServer( + ctx, + cancel, + slog.New(slog.NewTextHandler(io.Discard, nil)), + config.Config{Addr: ":8080"}, + nil, + func(config.Config, *slog.Logger, *service.Service) httpServer { return server }, + nil, + ) + close(done) + }() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("runServer did not return after listen failure") + } + + require.ErrorIs(t, ctx.Err(), context.Canceled) + require.True(t, server.shutdownCalled) +} + +func TestRunServerContinuesAfterShutdownError(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + server := newFakeServer() + server.shutdownErr = errors.New("shutdown failed") + done := make(chan struct{}) + + go func() { + runServer( + ctx, + cancel, + slog.New(slog.NewTextHandler(io.Discard, nil)), + config.Config{Addr: ":8080"}, + nil, + func(config.Config, *slog.Logger, *service.Service) httpServer { return server }, + nil, + ) + close(done) + }() + + <-server.listenStarted + cancel() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("runServer did not return after shutdown failure") + } + + require.True(t, server.shutdownCalled) +} + +type fakeDB struct { + pingErr error + pingCalls int + closed bool +} + +func (d *fakeDB) Ping(context.Context) error { + d.pingCalls++ + return d.pingErr +} + +func (d *fakeDB) Close() { + d.closed = true +} + +type fakeRedis struct { + closed bool +} + +func (r *fakeRedis) Close() error { + r.closed = true + return nil +} + +type fakeObjectStorage struct { + ensureBucketErr error + ensureBucketCalled bool +} + +func (s *fakeObjectStorage) PresignUpload(context.Context, string, time.Duration) (string, error) { + return "", nil +} + +func (s *fakeObjectStorage) PresignDownload(context.Context, string, time.Duration) (string, error) { + return "", nil +} + +func (s *fakeObjectStorage) PutObject(context.Context, string, []byte, string) error { + return nil +} + +func (s *fakeObjectStorage) DeletePrefix(context.Context, string) error { + return nil +} + +func (s *fakeObjectStorage) EnsureBucket(context.Context) error { + s.ensureBucketCalled = true + return s.ensureBucketErr +} + +type fakeServer struct { + listenErr error + shutdownErr error + shutdownCalled bool + listenStarted chan struct{} + stop chan struct{} + stopClosed bool +} + +func newFakeServer() *fakeServer { + return &fakeServer{ + listenStarted: make(chan struct{}), + stop: make(chan struct{}), + } +} + +func (s *fakeServer) ListenAndServe() error { + if s.listenStarted != nil { + close(s.listenStarted) + s.listenStarted = nil + } + if s.listenErr != nil { + return s.listenErr + } + <-s.stop + return nethttp.ErrServerClosed +} + +func (s *fakeServer) Shutdown(context.Context) error { + s.shutdownCalled = true + if s.stop != nil && !s.stopClosed { + close(s.stop) + s.stopClosed = true + } + return s.shutdownErr +} diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go new file mode 100644 index 0000000..62d8a2b --- /dev/null +++ b/apps/api/cmd/api/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "errors" + "io" + "log/slog" + "os" + "os/signal" + "syscall" +) + +var ( + // These indirections keep process-level side effects swappable in tests. + notifySignalContext = signal.NotifyContext + runCLIEntrypoint = runCLI + exitProcess = os.Exit +) + +func main() { + ctx, cancel := notifySignalContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + exitProcess(runCLIEntrypoint(ctx, cancel, newLogger(os.Stdout), defaultAppHooks())) +} + +// runCLI executes the fully wired application and converts failures into an exit code. +func runCLI(ctx context.Context, cancel context.CancelFunc, logger *slog.Logger, hooks appHooks) int { + if err := runMain(ctx, cancel, logger, hooks); err != nil { + var typedErr *appError + if errors.As(err, &typedErr) { + logger.Error(typedErr.message, "error", typedErr.err) + } else { + logger.Error("run api", "error", err) + } + return 1 + } + + return 0 +} + +// newLogger writes structured JSON logs so local and container output share one format. +func newLogger(writer io.Writer) *slog.Logger { + return slog.New(slog.NewJSONHandler(writer, &slog.HandlerOptions{Level: slog.LevelInfo})) +} diff --git a/apps/api/cmd/api/main_test.go b/apps/api/cmd/api/main_test.go new file mode 100644 index 0000000..eff112d --- /dev/null +++ b/apps/api/cmd/api/main_test.go @@ -0,0 +1,209 @@ +package main + +import ( + "bytes" + "context" + "errors" + "io" + "log/slog" + "net" + nethttp "net/http" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/config" + apihttp "github.com/xdrop/monorepo/internal/http" + "github.com/xdrop/monorepo/internal/service" +) + +func TestRetryReturnsAfterSuccessfulAttempt(t *testing.T) { + t.Parallel() + + attempts := 0 + err := retry(context.Background(), 5, 5*time.Millisecond, func() error { + attempts++ + if attempts < 3 { + return errors.New("try again") + } + return nil + }) + + require.NoError(t, err) + require.Equal(t, 3, attempts) +} + +func TestRetryReturnsContextErrorWhenCancelled(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + attempts := 0 + done := make(chan struct{}) + + go func() { + time.Sleep(15 * time.Millisecond) + cancel() + close(done) + }() + + err := retry(ctx, 5, 50*time.Millisecond, func() error { + attempts++ + return errors.New("still failing") + }) + + <-done + require.ErrorIs(t, err, context.Canceled) + require.Equal(t, 1, attempts) +} + +func TestRetryReturnsLastErrorAfterExhaustingAttempts(t *testing.T) { + t.Parallel() + + expected := errors.New("permanent failure") + attempts := 0 + + err := retry(context.Background(), 3, 5*time.Millisecond, func() error { + attempts++ + return expected + }) + + require.ErrorIs(t, err, expected) + require.Equal(t, 3, attempts) +} + +func TestServerServesHealthzAndShutsDown(t *testing.T) { + t.Parallel() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + + server := &nethttp.Server{ + Addr: listener.Addr().String(), + Handler: apihttp.NewRouter( + config.Config{AllowedOrigins: []string{"http://localhost:5173"}}, + slog.New(slog.NewTextHandler(io.Discard, nil)), + nil, + ), + ReadHeaderTimeout: 10 * time.Second, + } + + serverErr := make(chan error, 1) + go func() { + serverErr <- server.Serve(listener) + }() + + response, err := nethttp.Get("http://" + listener.Addr().String() + "/healthz") + require.NoError(t, err) + defer response.Body.Close() + require.Equal(t, nethttp.StatusOK, response.StatusCode) + + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + require.NoError(t, server.Shutdown(shutdownCtx)) + + err = <-serverErr + require.ErrorIs(t, err, nethttp.ErrServerClosed) +} + +func TestRunCLIReturnsExitCodesAndLogsErrors(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + exitCode := runCLI(ctx, cancel, slog.New(slog.NewTextHandler(io.Discard, nil)), appHooks{ + loadConfig: func() (config.Config, error) { + return config.Config{}, nil + }, + buildRuntime: func(context.Context, config.Config) (*appRuntime, error) { + return &appRuntime{close: func() {}}, nil + }, + newServer: func(config.Config, *slog.Logger, *service.Service) httpServer { + return newFakeServer() + }, + }) + + require.Equal(t, 0, exitCode) + }) + + t.Run("typed application error", func(t *testing.T) { + t.Parallel() + + var buffer bytes.Buffer + exitCode := runCLI(context.Background(), func() {}, newLogger(&buffer), appHooks{ + loadConfig: func() (config.Config, error) { + return config.Config{}, errors.New("config failed") + }, + }) + + require.Equal(t, 1, exitCode) + require.Contains(t, buffer.String(), "load config") + }) + + t.Run("generic error", func(t *testing.T) { + t.Parallel() + + var buffer bytes.Buffer + exitCode := runCLI(context.Background(), func() {}, newLogger(&buffer), appHooks{ + loadConfig: func() (config.Config, error) { + return config.Config{}, nil + }, + buildRuntime: func(context.Context, config.Config) (*appRuntime, error) { + return nil, errors.New("boom") + }, + }) + + require.Equal(t, 1, exitCode) + require.Contains(t, buffer.String(), "run api") + }) +} + +func TestMainExitsWithRunCLIStatus(t *testing.T) { + t.Parallel() + + originalNotify := notifySignalContext + originalRunCLI := runCLIEntrypoint + originalExit := exitProcess + defer func() { + notifySignalContext = originalNotify + runCLIEntrypoint = originalRunCLI + exitProcess = originalExit + }() + + cancelCalled := false + exitCode := -1 + + notifySignalContext = func( + ctx context.Context, + _ ...os.Signal, + ) (context.Context, context.CancelFunc) { + return ctx, func() { + cancelCalled = true + } + } + runCLIEntrypoint = func( + ctx context.Context, + cancel context.CancelFunc, + logger *slog.Logger, + hooks appHooks, + ) int { + require.NotNil(t, ctx) + require.NotNil(t, logger) + require.NotNil(t, hooks.loadConfig) + cancel() + return 7 + } + exitProcess = func(code int) { + exitCode = code + } + + main() + + require.True(t, cancelCalled) + require.Equal(t, 7, exitCode) +} diff --git a/apps/api/go.mod b/apps/api/go.mod new file mode 100644 index 0000000..c6917cd --- /dev/null +++ b/apps/api/go.mod @@ -0,0 +1,96 @@ +module github.com/xdrop/monorepo + +go 1.26.1 + +require ( + github.com/aws/aws-sdk-go-v2 v1.41.4 + github.com/aws/aws-sdk-go-v2/config v1.32.12 + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.8 + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 + github.com/go-chi/chi/v5 v5.2.5 + github.com/go-chi/cors v1.2.2 + github.com/jackc/pgx/v5 v5.8.0 + github.com/redis/go-redis/v9 v9.18.0 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.41.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/shirou/gopsutil/v4 v4.26.2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/apps/api/go.sum b/apps/api/go.sum new file mode 100644 index 0000000..51b5bd7 --- /dev/null +++ b/apps/api/go.sum @@ -0,0 +1,239 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.8 h1:nuc44j+otOY0d1e+CWwB6zul57d2YEGlgCyiq3SL0lI= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.8/go.mod h1:qSFgGCN8fjdhvlLhTPZdWRWXbwfeZZWF2FEaIplYPhE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= +github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go new file mode 100644 index 0000000..89fdbe9 --- /dev/null +++ b/apps/api/internal/config/config.go @@ -0,0 +1,190 @@ +package config + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" +) + +type Config struct { + AppEnv string + Addr string + DatabaseURL string + RedisAddr string + RedisPassword string + RedisDB int + S3Endpoint string + S3PublicEndpoint string + S3Region string + S3Bucket string + S3AccessKey string + S3SecretKey string + S3UseSSL bool + PresignTTL time.Duration + DefaultExpiry time.Duration + AllowedOrigins []string + CreateLimit int + PublicReadLimit int + DownloadURLLimit int + CleanupInterval time.Duration + ChunkSize int64 + MaxFileCount int + MaxTransferBytes int64 +} + +// AllowedExpiryOptions is the canonical list of transfer lifetimes accepted by the API. +var AllowedExpiryOptions = []time.Duration{ + 5 * time.Minute, + 10 * time.Minute, + 30 * time.Minute, + 1 * time.Hour, + 3 * time.Hour, + 6 * time.Hour, + 12 * time.Hour, + 24 * time.Hour, + 72 * time.Hour, + 7 * 24 * time.Hour, +} + +// Load reads environment variables, applies defaults, and validates required settings. +func Load() (Config, error) { + cfg := Config{ + AppEnv: getenv("APP_ENV", "development"), + Addr: getenv("API_ADDR", ":8080"), + DatabaseURL: getenv("DATABASE_URL", "postgres://xdrop:xdrop@localhost:5432/xdrop?sslmode=disable"), + RedisAddr: getenv("REDIS_ADDR", "localhost:6379"), + RedisPassword: getenv("REDIS_PASSWORD", ""), + S3Endpoint: getenv("S3_ENDPOINT", "http://localhost:9000"), + S3PublicEndpoint: getenv("S3_PUBLIC_ENDPOINT", "http://localhost:5173"), + S3Region: getenv("S3_REGION", "us-east-1"), + S3Bucket: getenv("S3_BUCKET", "xdrop"), + S3AccessKey: getenv("S3_ACCESS_KEY", "minioadmin"), + S3SecretKey: getenv("S3_SECRET_KEY", "minioadmin"), + S3UseSSL: getenv("S3_USE_SSL", "false") == "true", + DefaultExpiry: getenvExpiry("DEFAULT_EXPIRY_SECONDS", "DEFAULT_EXPIRY_DAYS", time.Hour), + AllowedOrigins: splitCSV(getenv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080")), + CreateLimit: getenvInt("RATE_LIMIT_CREATE", 20), + PublicReadLimit: getenvInt("RATE_LIMIT_PUBLIC_READ", 180), + DownloadURLLimit: getenvInt("RATE_LIMIT_DOWNLOAD_URLS", 120), + CleanupInterval: getenvDuration("CLEANUP_INTERVAL", 2*time.Minute), + ChunkSize: getenvInt64("CHUNK_SIZE_BYTES", 8*1024*1024), + MaxFileCount: getenvInt("MAX_FILE_COUNT", 100), + MaxTransferBytes: getenvInt64("MAX_TRANSFER_BYTES", 256*1024*1024), + } + + cfg.RedisDB = getenvInt("REDIS_DB", 0) + presignSeconds := getenvInt("PRESIGN_TTL_SECONDS", 300) + cfg.PresignTTL = time.Duration(presignSeconds) * time.Second + + if cfg.DatabaseURL == "" { + return Config{}, fmt.Errorf("DATABASE_URL is required") + } + if cfg.S3Bucket == "" { + return Config{}, fmt.Errorf("S3_BUCKET is required") + } + if !IsAllowedExpiry(cfg.DefaultExpiry) { + return Config{}, fmt.Errorf("DEFAULT_EXPIRY_SECONDS must be one of the supported expiry options") + } + + return cfg, nil +} + +func getenv(key, fallback string) string { + value := strings.TrimSpace(os.Getenv(key)) + if value == "" { + return fallback + } + + return value +} + +func getenvInt(key string, fallback int) int { + value := strings.TrimSpace(os.Getenv(key)) + if value == "" { + return fallback + } + + parsed, err := strconv.Atoi(value) + if err != nil { + return fallback + } + + return parsed +} + +func getenvInt64(key string, fallback int64) int64 { + value := strings.TrimSpace(os.Getenv(key)) + if value == "" { + return fallback + } + + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return fallback + } + + return parsed +} + +func getenvDuration(key string, fallback time.Duration) time.Duration { + value := strings.TrimSpace(os.Getenv(key)) + if value == "" { + return fallback + } + + parsed, err := time.ParseDuration(value) + if err != nil { + return fallback + } + + return parsed +} + +// getenvExpiry preserves support for the previous day-based variable while preferring seconds. +func getenvExpiry(secondsKey string, legacyDaysKey string, fallback time.Duration) time.Duration { + if seconds := strings.TrimSpace(os.Getenv(secondsKey)); seconds != "" { + parsed, err := strconv.Atoi(seconds) + if err == nil && parsed > 0 { + return time.Duration(parsed) * time.Second + } + return fallback + } + + if days := strings.TrimSpace(os.Getenv(legacyDaysKey)); days != "" { + parsed, err := strconv.Atoi(days) + if err == nil && parsed > 0 { + return time.Duration(parsed) * 24 * time.Hour + } + return fallback + } + + return fallback +} + +// IsAllowedExpiry reports whether a duration matches one of the supported public options. +func IsAllowedExpiry(duration time.Duration) bool { + for _, option := range AllowedExpiryOptions { + if duration == option { + return true + } + } + + return false +} + +// splitCSV trims whitespace and drops empty values from comma-separated environment settings. +func splitCSV(value string) []string { + parts := strings.Split(value, ",") + origins := make([]string, 0, len(parts)) + + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + origins = append(origins, trimmed) + } + } + + return origins +} diff --git a/apps/api/internal/config/config_test.go b/apps/api/internal/config/config_test.go new file mode 100644 index 0000000..eaaf46d --- /dev/null +++ b/apps/api/internal/config/config_test.go @@ -0,0 +1,160 @@ +package config + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestLoadUsesDefaultsWhenEnvironmentIsUnset(t *testing.T) { + clearConfigEnv(t) + + cfg, err := Load() + require.NoError(t, err) + require.Equal(t, "development", cfg.AppEnv) + require.Equal(t, ":8080", cfg.Addr) + require.Equal(t, "postgres://xdrop:xdrop@localhost:5432/xdrop?sslmode=disable", cfg.DatabaseURL) + require.Equal(t, "localhost:6379", cfg.RedisAddr) + require.Equal(t, "http://localhost:9000", cfg.S3Endpoint) + require.Equal(t, "http://localhost:5173", cfg.S3PublicEndpoint) + require.Equal(t, time.Hour, cfg.DefaultExpiry) + require.Equal(t, 5*time.Minute, cfg.PresignTTL) + require.Equal(t, []string{"http://localhost:5173", "http://localhost:8080"}, cfg.AllowedOrigins) + require.Equal(t, 20, cfg.CreateLimit) + require.Equal(t, 180, cfg.PublicReadLimit) + require.Equal(t, 120, cfg.DownloadURLLimit) + require.Equal(t, 2*time.Minute, cfg.CleanupInterval) + require.Equal(t, int64(8*1024*1024), cfg.ChunkSize) + require.Equal(t, 100, cfg.MaxFileCount) + require.Equal(t, int64(256*1024*1024), cfg.MaxTransferBytes) +} + +func TestLoadParsesCustomEnvironmentValues(t *testing.T) { + clearConfigEnv(t) + + t.Setenv("APP_ENV", "production") + t.Setenv("API_ADDR", ":9090") + t.Setenv("DATABASE_URL", "postgres://demo:demo@db:5432/demo?sslmode=disable") + t.Setenv("REDIS_ADDR", "redis:6379") + t.Setenv("REDIS_PASSWORD", "secret") + t.Setenv("REDIS_DB", "4") + t.Setenv("S3_ENDPOINT", "http://minio:9000") + t.Setenv("S3_PUBLIC_ENDPOINT", "https://files.example.test") + t.Setenv("S3_REGION", "ap-southeast-1") + t.Setenv("S3_BUCKET", "custom-bucket") + t.Setenv("S3_ACCESS_KEY", "key") + t.Setenv("S3_SECRET_KEY", "secret-key") + t.Setenv("S3_USE_SSL", "true") + t.Setenv("PRESIGN_TTL_SECONDS", "600") + t.Setenv("DEFAULT_EXPIRY_SECONDS", "10800") + t.Setenv("ALLOWED_ORIGINS", " https://app.example.test, https://admin.example.test ") + t.Setenv("RATE_LIMIT_CREATE", "7") + t.Setenv("RATE_LIMIT_PUBLIC_READ", "8") + t.Setenv("RATE_LIMIT_DOWNLOAD_URLS", "9") + t.Setenv("CLEANUP_INTERVAL", "45s") + t.Setenv("CHUNK_SIZE_BYTES", "12345") + t.Setenv("MAX_FILE_COUNT", "12") + t.Setenv("MAX_TRANSFER_BYTES", "34567") + + cfg, err := Load() + require.NoError(t, err) + require.Equal(t, "production", cfg.AppEnv) + require.Equal(t, ":9090", cfg.Addr) + require.Equal(t, "postgres://demo:demo@db:5432/demo?sslmode=disable", cfg.DatabaseURL) + require.Equal(t, "redis:6379", cfg.RedisAddr) + require.Equal(t, "secret", cfg.RedisPassword) + require.Equal(t, 4, cfg.RedisDB) + require.Equal(t, "http://minio:9000", cfg.S3Endpoint) + require.Equal(t, "https://files.example.test", cfg.S3PublicEndpoint) + require.Equal(t, "ap-southeast-1", cfg.S3Region) + require.Equal(t, "custom-bucket", cfg.S3Bucket) + require.Equal(t, "key", cfg.S3AccessKey) + require.Equal(t, "secret-key", cfg.S3SecretKey) + require.True(t, cfg.S3UseSSL) + require.Equal(t, 10*time.Minute, cfg.PresignTTL) + require.Equal(t, 3*time.Hour, cfg.DefaultExpiry) + require.Equal(t, []string{"https://app.example.test", "https://admin.example.test"}, cfg.AllowedOrigins) + require.Equal(t, 7, cfg.CreateLimit) + require.Equal(t, 8, cfg.PublicReadLimit) + require.Equal(t, 9, cfg.DownloadURLLimit) + require.Equal(t, 45*time.Second, cfg.CleanupInterval) + require.Equal(t, int64(12345), cfg.ChunkSize) + require.Equal(t, 12, cfg.MaxFileCount) + require.Equal(t, int64(34567), cfg.MaxTransferBytes) +} + +func TestLoadSupportsLegacyExpiryDays(t *testing.T) { + clearConfigEnv(t) + t.Setenv("DEFAULT_EXPIRY_DAYS", "3") + + cfg, err := Load() + require.NoError(t, err) + require.Equal(t, 72*time.Hour, cfg.DefaultExpiry) +} + +func TestLoadRejectsUnsupportedDefaultExpiry(t *testing.T) { + clearConfigEnv(t) + t.Setenv("DEFAULT_EXPIRY_SECONDS", "42") + + _, err := Load() + require.ErrorContains(t, err, "DEFAULT_EXPIRY_SECONDS must be one of the supported expiry options") +} + +func TestEnvHelpersFallBackOnInvalidValues(t *testing.T) { + t.Setenv("INT_VALUE", "not-a-number") + t.Setenv("INT64_VALUE", "nope") + t.Setenv("DURATION_VALUE", "bad-duration") + t.Setenv("EXPIRY_SECONDS", "-1") + t.Setenv("EXPIRY_DAYS", "still-bad") + + require.Equal(t, "fallback", getenv("MISSING_VALUE", "fallback")) + require.Equal(t, 12, getenvInt("INT_VALUE", 12)) + require.Equal(t, int64(34), getenvInt64("INT64_VALUE", 34)) + require.Equal(t, time.Minute, getenvDuration("DURATION_VALUE", time.Minute)) + require.Equal(t, 2*time.Hour, getenvExpiry("EXPIRY_SECONDS", "EXPIRY_DAYS", 2*time.Hour)) + + t.Setenv("EXPIRY_SECONDS", "") + require.Equal(t, 2*time.Hour, getenvExpiry("EXPIRY_SECONDS", "EXPIRY_DAYS", 2*time.Hour)) +} + +func TestSplitCSVAndAllowedExpiryHelpers(t *testing.T) { + t.Parallel() + + require.Equal(t, []string{"https://a.test", "https://b.test"}, splitCSV(" https://a.test, ,https://b.test ")) + require.True(t, IsAllowedExpiry(time.Hour)) + require.False(t, IsAllowedExpiry(2*time.Hour)) +} + +func clearConfigEnv(t *testing.T) { + t.Helper() + + for _, key := range []string{ + "APP_ENV", + "API_ADDR", + "DATABASE_URL", + "REDIS_ADDR", + "REDIS_PASSWORD", + "REDIS_DB", + "S3_ENDPOINT", + "S3_PUBLIC_ENDPOINT", + "S3_REGION", + "S3_BUCKET", + "S3_ACCESS_KEY", + "S3_SECRET_KEY", + "S3_USE_SSL", + "PRESIGN_TTL_SECONDS", + "DEFAULT_EXPIRY_SECONDS", + "DEFAULT_EXPIRY_DAYS", + "ALLOWED_ORIGINS", + "RATE_LIMIT_CREATE", + "RATE_LIMIT_PUBLIC_READ", + "RATE_LIMIT_DOWNLOAD_URLS", + "CLEANUP_INTERVAL", + "CHUNK_SIZE_BYTES", + "MAX_FILE_COUNT", + "MAX_TRANSFER_BYTES", + } { + t.Setenv(key, "") + } +} diff --git a/apps/api/internal/http/api_integration_test.go b/apps/api/internal/http/api_integration_test.go new file mode 100644 index 0000000..1559d55 --- /dev/null +++ b/apps/api/internal/http/api_integration_test.go @@ -0,0 +1,339 @@ +package http + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + nethttp "net/http" + "net/http/httptest" + "os/exec" + "runtime" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" + "github.com/xdrop/monorepo/internal/service" + "github.com/xdrop/monorepo/internal/storage" +) + +func TestAPITransferLifecycleEndToEnd(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + stack := startHTTPIntegrationStack(t, ctx) + + router := NewRouter( + stack.cfg, + slog.New(slog.NewTextHandler(io.Discard, nil)), + service.New( + stack.cfg, + repo.NewPostgresRepository(stack.db), + stack.objectStorage, + ratelimit.NewRedisLimiter(stack.redisClient), + ), + ) + server := httptest.NewServer(router) + defer server.Close() + + httpClient := server.Client() + + createResponse := struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + }{} + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/transfers/", "", map[string]int{ + "expiresInSeconds": 3600, + }, &createResponse) + require.NotEmpty(t, createResponse.TransferID) + require.NotEmpty(t, createResponse.ManageToken) + + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/transfers/"+createResponse.TransferID+"/files", createResponse.ManageToken, []map[string]any{ + { + "fileId": "file-a", + "totalChunks": 1, + "ciphertextBytes": 5, + "plaintextBytes": 3, + "chunkSize": 3, + }, + }, nil) + + uploadURLs := struct { + Items []struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + ObjectKey string `json:"objectKey"` + URL string `json:"url"` + } `json:"items"` + }{} + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/transfers/"+createResponse.TransferID+"/upload-urls", createResponse.ManageToken, map[string]any{ + "chunks": []map[string]any{{"fileId": "file-a", "chunkIndex": 0}}, + }, &uploadURLs) + require.Len(t, uploadURLs.Items, 1) + + chunkCiphertext := []byte("chunk") + uploadRequest, err := nethttp.NewRequestWithContext(ctx, nethttp.MethodPut, uploadURLs.Items[0].URL, bytes.NewReader(chunkCiphertext)) + require.NoError(t, err) + uploadRequest.Header.Set("Content-Type", "application/octet-stream") + uploadResponse, err := httpClient.Do(uploadRequest) + require.NoError(t, err) + uploadResponse.Body.Close() + require.Equal(t, nethttp.StatusOK, uploadResponse.StatusCode) + + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/transfers/"+createResponse.TransferID+"/chunks/complete", createResponse.ManageToken, []map[string]any{ + { + "fileId": "file-a", + "chunkIndex": 0, + "ciphertextSize": len(chunkCiphertext), + "checksumSha256": "deadbeef", + }, + }, nil) + + manifestCiphertext := []byte(`{"version":1}`) + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/transfers/"+createResponse.TransferID+"/manifest", createResponse.ManageToken, map[string]string{ + "ciphertextBase64": base64.StdEncoding.EncodeToString(manifestCiphertext), + }, nil) + + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/transfers/"+createResponse.TransferID+"/finalize", createResponse.ManageToken, map[string]any{ + "wrappedRootKey": `{"wrapped":true}`, + "totalFiles": 1, + "totalCiphertextBytes": len(chunkCiphertext), + }, nil) + + publicTransfer := struct { + Status string `json:"status"` + WrappedRootKey string `json:"wrappedRootKey"` + ManifestURL string `json:"manifestUrl"` + ManifestCiphertextSize int64 `json:"manifestCiphertextSize"` + }{} + doJSON(t, httpClient, nethttp.MethodGet, server.URL+"/api/v1/public/transfers/"+createResponse.TransferID+"/", "", nil, &publicTransfer) + require.Equal(t, "ready", publicTransfer.Status) + require.Equal(t, `{"wrapped":true}`, publicTransfer.WrappedRootKey) + require.NotEmpty(t, publicTransfer.ManifestURL) + require.Equal(t, int64(len(manifestCiphertext)), publicTransfer.ManifestCiphertextSize) + + manifestResponse, err := httpClient.Get(publicTransfer.ManifestURL) + require.NoError(t, err) + defer manifestResponse.Body.Close() + require.Equal(t, nethttp.StatusOK, manifestResponse.StatusCode) + manifestBody, err := io.ReadAll(manifestResponse.Body) + require.NoError(t, err) + require.Equal(t, manifestCiphertext, manifestBody) + + downloadURLs := struct { + Items []struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + URL string `json:"url"` + } `json:"items"` + }{} + doJSON(t, httpClient, nethttp.MethodPost, server.URL+"/api/v1/public/transfers/"+createResponse.TransferID+"/download-urls", "", map[string]any{ + "chunks": []map[string]any{{"fileId": "file-a", "chunkIndex": 0}}, + }, &downloadURLs) + require.Len(t, downloadURLs.Items, 1) + + downloadResponse, err := httpClient.Get(downloadURLs.Items[0].URL) + require.NoError(t, err) + defer downloadResponse.Body.Close() + require.Equal(t, nethttp.StatusOK, downloadResponse.StatusCode) + downloadedChunk, err := io.ReadAll(downloadResponse.Body) + require.NoError(t, err) + require.Equal(t, chunkCiphertext, downloadedChunk) +} + +type httpIntegrationStack struct { + cfg config.Config + db *pgxpool.Pool + redisClient *redis.Client + objectStorage *storage.S3Storage +} + +func startHTTPIntegrationStack(t *testing.T, ctx context.Context) httpIntegrationStack { + t.Helper() + + db := startHTTPPostgresDB(t, ctx) + require.NoError(t, repo.RunMigrations(ctx, db)) + + redisClient := startHTTPRedisClient(t, ctx) + objectStorage, cfg := startHTTPStorage(t, ctx) + + return httpIntegrationStack{ + cfg: cfg, + db: db, + redisClient: redisClient, + objectStorage: objectStorage, + } +} + +func startHTTPPostgresDB(t *testing.T, ctx context.Context) *pgxpool.Pool { + t.Helper() + + container, err := tcpostgres.Run( + ctx, + "postgres:16-alpine", + tcpostgres.WithDatabase("xdrop"), + tcpostgres.WithUsername("xdrop"), + tcpostgres.WithPassword("xdrop"), + tcpostgres.BasicWaitStrategies(), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, testcontainers.TerminateContainer(container)) + }) + + connectionString, err := container.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := pgxpool.New(ctx, connectionString) + require.NoError(t, err) + require.NoError(t, db.Ping(ctx)) + t.Cleanup(db.Close) + + return db +} + +func startHTTPRedisClient(t *testing.T, ctx context.Context) *redis.Client { + t.Helper() + + container, err := testcontainers.Run( + ctx, + "redis:7-alpine", + testcontainers.WithExposedPorts("6379/tcp"), + testcontainers.WithWaitStrategy( + wait.ForLog("Ready to accept connections").WithStartupTimeout(60*time.Second), + ), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, testcontainers.TerminateContainer(container)) + }) + + host, err := container.Host(ctx) + require.NoError(t, err) + port, err := container.MappedPort(ctx, "6379/tcp") + require.NoError(t, err) + + client := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%s", host, port.Port()), + DB: 0, + }) + require.NoError(t, client.Ping(ctx).Err()) + t.Cleanup(func() { + require.NoError(t, client.Close()) + }) + + return client +} + +func startHTTPStorage(t *testing.T, ctx context.Context) (*storage.S3Storage, config.Config) { + t.Helper() + + container, err := testcontainers.Run( + ctx, + "minio/minio:latest", + testcontainers.WithEnv(map[string]string{ + "MINIO_ROOT_USER": "minioadmin", + "MINIO_ROOT_PASSWORD": "minioadmin", + }), + testcontainers.WithExposedPorts("9000/tcp"), + testcontainers.WithCmd("server", "/data"), + testcontainers.WithWaitStrategy( + wait.ForHTTP("/minio/health/live"). + WithPort("9000/tcp"). + WithStartupTimeout(90*time.Second), + ), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, testcontainers.TerminateContainer(container)) + }) + + host, err := container.Host(ctx) + require.NoError(t, err) + port, err := container.MappedPort(ctx, "9000/tcp") + require.NoError(t, err) + + endpoint := fmt.Sprintf("http://%s:%s", host, port.Port()) + objectStorage, err := storage.NewS3Storage(ctx, storage.Config{ + Endpoint: endpoint, + PublicEndpoint: endpoint, + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "minioadmin", + SecretKey: "minioadmin", + }) + require.NoError(t, err) + require.NoError(t, objectStorage.EnsureBucket(ctx)) + + cfg := config.Config{ + AllowedOrigins: []string{"http://localhost:5173"}, + ChunkSize: 8 * 1024 * 1024, + DefaultExpiry: time.Hour, + CreateLimit: 20, + PublicReadLimit: 120, + DownloadURLLimit: 120, + PresignTTL: 5 * time.Minute, + MaxFileCount: 100, + MaxTransferBytes: 256 * 1024 * 1024, + } + + return objectStorage, cfg +} + +func doJSON(t *testing.T, client *nethttp.Client, method string, url string, bearerToken string, payload any, target any) { + t.Helper() + + var body io.Reader + if payload != nil { + encoded, err := json.Marshal(payload) + require.NoError(t, err) + body = bytes.NewReader(encoded) + } + + request, err := nethttp.NewRequest(method, url, body) + require.NoError(t, err) + if payload != nil { + request.Header.Set("Content-Type", "application/json") + } + if bearerToken != "" { + request.Header.Set("Authorization", "Bearer "+bearerToken) + } + + response, err := client.Do(request) + require.NoError(t, err) + defer response.Body.Close() + + responseBody, err := io.ReadAll(response.Body) + require.NoError(t, err) + require.Less(t, response.StatusCode, 400, string(responseBody)) + + if target != nil { + require.NoError(t, json.Unmarshal(responseBody, target)) + } +} + +func skipIfDockerUnavailable(t *testing.T) { + t.Helper() + + if testing.Short() { + t.Skip("skipping docker-backed integration test in short mode") + } + if runtime.GOOS == "windows" { + t.Skip("skipping docker-backed integration test on windows") + } + + if err := exec.Command("docker", "info").Run(); err != nil { + t.Skipf("skipping docker-backed integration test: %v", err) + } +} diff --git a/apps/api/internal/http/router.go b/apps/api/internal/http/router.go new file mode 100644 index 0000000..172df8d --- /dev/null +++ b/apps/api/internal/http/router.go @@ -0,0 +1,325 @@ +package http + +import ( + "encoding/json" + "errors" + "io" + "log/slog" + "net" + nethttp "net/http" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/service" +) + +type Handler struct { + logger *slog.Logger + service *service.Service +} + +// NewRouter wires the public and manage APIs together with shared middleware. +func NewRouter(cfg config.Config, logger *slog.Logger, svc *service.Service) nethttp.Handler { + handler := Handler{ + logger: logger, + service: svc, + } + + router := chi.NewRouter() + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(middleware.Recoverer) + router.Use(requestLogger(logger)) + router.Use(securityHeaders) + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: cfg.AllowedOrigins, + AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + ExposedHeaders: []string{"Content-Type"}, + AllowCredentials: false, + MaxAge: 300, + })) + + router.Get("/healthz", func(w nethttp.ResponseWriter, _ *nethttp.Request) { + writeJSON(w, nethttp.StatusOK, map[string]string{"status": "ok"}) + }) + + router.Route("/api/v1", func(r chi.Router) { + r.Route("/public/transfers/{transferId}", func(public chi.Router) { + public.Get("/", handler.getPublicTransfer) + public.Post("/download-urls", handler.createDownloadURLs) + }) + + r.Route("/transfers", func(manage chi.Router) { + manage.Post("/", handler.createTransfer) + manage.Route("/{transferId}", func(transfer chi.Router) { + transfer.Get("/", handler.getManageTransfer) + transfer.Patch("/", handler.patchTransfer) + transfer.Delete("/", handler.deleteTransfer) + transfer.Get("/resume", handler.resumeTransfer) + transfer.Post("/files", handler.registerFiles) + transfer.Post("/upload-urls", handler.createUploadURLs) + transfer.Post("/chunks/complete", handler.completeChunks) + transfer.Post("/manifest", handler.putManifest) + transfer.Post("/finalize", handler.finalizeTransfer) + }) + }) + }) + + return router +} + +func (h Handler) createTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + var request service.CreateTransferRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + response, err := h.service.CreateTransfer(r.Context(), clientKey(r), request) + if err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusCreated, response) +} + +func (h Handler) registerFiles(w nethttp.ResponseWriter, r *nethttp.Request) { + var request []service.RegisterFileRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + if err := h.service.RegisterFiles(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r), request); err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]bool{"ok": true}) +} + +func (h Handler) createUploadURLs(w nethttp.ResponseWriter, r *nethttp.Request) { + var request service.UploadURLRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + response, err := h.service.CreateUploadURLs(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r), request) + if err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]any{"items": response}) +} + +func (h Handler) completeChunks(w nethttp.ResponseWriter, r *nethttp.Request) { + var request []service.CompleteChunkRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + if err := h.service.CompleteChunks(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r), request); err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]bool{"ok": true}) +} + +func (h Handler) putManifest(w nethttp.ResponseWriter, r *nethttp.Request) { + var request service.ManifestUploadRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + if err := h.service.PutManifest(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r), request); err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]bool{"ok": true}) +} + +func (h Handler) finalizeTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + var request service.FinalizeTransferRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + if err := h.service.FinalizeTransfer(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r), request); err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]bool{"ok": true}) +} + +func (h Handler) getManageTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + response, err := h.service.GetManageTransfer(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r)) + if err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, response) +} + +func (h Handler) patchTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + var request service.UpdateTransferRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + if err := h.service.UpdateTransfer(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r), request); err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]bool{"ok": true}) +} + +func (h Handler) deleteTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + if err := h.service.DeleteTransfer(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r)); err != nil { + writeError(w, err) + return + } + + w.WriteHeader(nethttp.StatusNoContent) +} + +func (h Handler) resumeTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + response, err := h.service.ResumeTransfer(r.Context(), chi.URLParam(r, "transferId"), bearerToken(r)) + if err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, response) +} + +func (h Handler) getPublicTransfer(w nethttp.ResponseWriter, r *nethttp.Request) { + response, err := h.service.GetPublicTransfer(r.Context(), clientKey(r), chi.URLParam(r, "transferId")) + if err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, response) +} + +func (h Handler) createDownloadURLs(w nethttp.ResponseWriter, r *nethttp.Request) { + var request service.DownloadURLRequest + if err := decodeJSON(r, &request); err != nil { + writeError(w, err) + return + } + + response, err := h.service.CreateDownloadURLs(r.Context(), clientKey(r), chi.URLParam(r, "transferId"), request) + if err != nil { + writeError(w, err) + return + } + + writeJSON(w, nethttp.StatusOK, map[string]any{"items": response}) +} + +// decodeJSON enforces a single JSON value and rejects unknown fields for stricter contracts. +func decodeJSON(r *nethttp.Request, target any) error { + defer r.Body.Close() + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return &service.HTTPError{Status: nethttp.StatusBadRequest, Code: "invalid_json", Message: err.Error()} + } + if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { + return &service.HTTPError{Status: nethttp.StatusBadRequest, Code: "invalid_json", Message: "request body must contain a single JSON value"} + } + + return nil +} + +// writeJSON serializes a response body with the expected JSON content type. +func writeJSON(w nethttp.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(payload) +} + +// writeError translates service-layer HTTP errors into JSON API responses. +func writeError(w nethttp.ResponseWriter, err error) { + var httpErr *service.HTTPError + if errors.As(err, &httpErr) { + writeJSON(w, httpErr.Status, map[string]string{ + "error": httpErr.Code, + "message": httpErr.Message, + }) + return + } + + writeJSON(w, nethttp.StatusInternalServerError, map[string]string{ + "error": "internal_error", + "message": "internal server error", + }) +} + +func bearerToken(r *nethttp.Request) string { + value := strings.TrimSpace(r.Header.Get("Authorization")) + if !strings.HasPrefix(strings.ToLower(value), "bearer ") { + return "" + } + + return strings.TrimSpace(value[7:]) +} + +func clientKey(r *nethttp.Request) string { + value := strings.TrimSpace(r.RemoteAddr) + if value == "" { + return "" + } + host, _, err := net.SplitHostPort(value) + if err == nil { + return strings.TrimSpace(host) + } + + return value +} + +// securityHeaders adds a baseline set of defensive response headers to every request. +func securityHeaders(next nethttp.Handler) nethttp.Handler { + return nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Referrer-Policy", "same-origin") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") + w.Header().Set("Cross-Origin-Resource-Policy", "same-site") + next.ServeHTTP(w, r) + }) +} + +// requestLogger records lightweight request metadata without buffering response bodies. +func requestLogger(logger *slog.Logger) func(nethttp.Handler) nethttp.Handler { + return func(next nethttp.Handler) nethttp.Handler { + return nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) { + startedAt := time.Now() + next.ServeHTTP(w, r) + logger.Info("request complete", + "method", r.Method, + "path", r.URL.Path, + "remote_addr", r.RemoteAddr, + "duration_ms", time.Since(startedAt).Milliseconds(), + ) + }) + } +} diff --git a/apps/api/internal/http/router_test.go b/apps/api/internal/http/router_test.go new file mode 100644 index 0000000..3028f14 --- /dev/null +++ b/apps/api/internal/http/router_test.go @@ -0,0 +1,689 @@ +package http + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "io" + "log/slog" + nethttp "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/models" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" + "github.com/xdrop/monorepo/internal/service" +) + +func TestCreateTransferRateLimitUsesIPAddressWithoutPort(t *testing.T) { + t.Parallel() + + cfg := testRouterConfig() + cfg.CreateLimit = 1 + + router := NewRouter( + cfg, + slog.New(slog.NewTextHandler(io.Discard, nil)), + service.New(cfg, newRouterRepository(), &routerStorage{}, ratelimit.NewMemoryLimiter()), + ) + + first := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusCreated, first.Code) + + second := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40002") + require.Equal(t, nethttp.StatusTooManyRequests, second.Code) + require.Contains(t, second.Body.String(), `"error":"rate_limited"`) +} + +func TestCreateTransferRejectsTrailingJSONPayload(t *testing.T) { + t.Parallel() + + router := newTestRouter() + response := performJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/", + `{"expiresInSeconds":3600}{"ignored":true}`, + "198.51.100.42:40001", + ) + + require.Equal(t, nethttp.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), `"error":"invalid_json"`) +} + +func TestCreateTransferRejectsUnknownFields(t *testing.T) { + t.Parallel() + + router := newTestRouter() + response := performJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/", + `{"expiresInSeconds":3600,"extra":true}`, + "198.51.100.42:40001", + ) + + require.Equal(t, nethttp.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), `"error":"invalid_json"`) +} + +func TestHealthzAppliesSecurityHeaders(t *testing.T) { + t.Parallel() + + router := newTestRouter() + request := httptest.NewRequest(nethttp.MethodGet, "/healthz", nil) + response := httptest.NewRecorder() + + router.ServeHTTP(response, request) + + require.Equal(t, nethttp.StatusOK, response.Code) + require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options")) + require.Equal(t, "DENY", response.Header().Get("X-Frame-Options")) + require.Equal(t, "same-origin", response.Header().Get("Cross-Origin-Opener-Policy")) +} + +func newTestRouter() nethttp.Handler { + cfg := testRouterConfig() + return NewRouter( + cfg, + slog.New(slog.NewTextHandler(io.Discard, nil)), + service.New(cfg, newRouterRepository(), &routerStorage{}, ratelimit.NewMemoryLimiter()), + ) +} + +func testRouterConfig() config.Config { + return config.Config{ + AllowedOrigins: []string{"http://localhost:5173"}, + ChunkSize: 8 * 1024 * 1024, + DefaultExpiry: time.Hour, + CreateLimit: 20, + PublicReadLimit: 120, + DownloadURLLimit: 120, + PresignTTL: 5 * time.Minute, + MaxFileCount: 100, + MaxTransferBytes: 256 * 1024 * 1024, + } +} + +func performJSONRequest(t *testing.T, handler nethttp.Handler, method string, path string, body string, remoteAddr string) *httptest.ResponseRecorder { + t.Helper() + + request := httptest.NewRequest(method, path, bytes.NewBufferString(body)) + request.Header.Set("Content-Type", "application/json") + request.RemoteAddr = remoteAddr + + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + return response +} + +type routerRepository struct { + transfers map[string]models.Transfer + files map[string][]models.TransferFile + chunks map[string][]models.TransferChunk +} + +func newRouterRepository() *routerRepository { + return &routerRepository{ + transfers: map[string]models.Transfer{}, + files: map[string][]models.TransferFile{}, + chunks: map[string][]models.TransferChunk{}, + } +} + +func (r *routerRepository) CreateTransfer(_ context.Context, transfer models.Transfer) error { + r.transfers[transfer.ID] = transfer + return nil +} + +func (r *routerRepository) GetTransfer(_ context.Context, transferID string) (models.Transfer, error) { + transfer, ok := r.transfers[transferID] + if !ok { + return models.Transfer{}, repo.ErrNotFound + } + return transfer, nil +} + +func (r *routerRepository) RegisterFiles(_ context.Context, transferID string, files []models.TransferFile) error { + r.files[transferID] = append([]models.TransferFile{}, files...) + transfer := r.transfers[transferID] + transfer.Status = models.TransferStatusUploading + r.transfers[transferID] = transfer + return nil +} + +func (r *routerRepository) ListFiles(_ context.Context, transferID string) ([]models.TransferFile, error) { + return append([]models.TransferFile{}, r.files[transferID]...), nil +} + +func (r *routerRepository) CompleteChunks(_ context.Context, transferID string, chunks []models.TransferChunk) error { + r.chunks[transferID] = append(r.chunks[transferID], chunks...) + files := r.files[transferID] + for fileIndex := range files { + uploaded := 0 + for _, chunk := range r.chunks[transferID] { + if chunk.OpaqueFileID == files[fileIndex].OpaqueFileID { + uploaded++ + } + } + if uploaded >= files[fileIndex].TotalChunks { + files[fileIndex].UploadStatus = "complete" + } + } + r.files[transferID] = files + return nil +} + +func (r *routerRepository) GetResumeState(ctx context.Context, transferID string) (models.TransferResumeState, error) { + transfer, err := r.GetTransfer(ctx, transferID) + if err != nil { + return models.TransferResumeState{}, err + } + + uploaded := map[string][]int{} + for _, chunk := range r.chunks[transferID] { + uploaded[chunk.OpaqueFileID] = append(uploaded[chunk.OpaqueFileID], chunk.ChunkIndex) + } + + return models.TransferResumeState{ + Transfer: transfer, + Files: append([]models.TransferFile{}, r.files[transferID]...), + UploadedChunks: uploaded, + }, nil +} + +func (r *routerRepository) SetManifest(_ context.Context, transferID string, objectKey string, ciphertextSize int64) error { + transfer := r.transfers[transferID] + transfer.ManifestObjectKey = objectKey + transfer.ManifestCiphertextSize = ciphertextSize + r.transfers[transferID] = transfer + return nil +} + +func (r *routerRepository) FinalizeTransfer(_ context.Context, transferID string, wrappedRootKey string, totalFiles int, totalCiphertextBytes int64) error { + transfer := r.transfers[transferID] + transfer.Status = models.TransferStatusReady + transfer.WrappedRootKey = wrappedRootKey + transfer.TotalFiles = totalFiles + transfer.TotalCiphertextBytes = totalCiphertextBytes + now := time.Now().UTC() + transfer.FinalizedAt = &now + r.transfers[transferID] = transfer + return nil +} + +func (r *routerRepository) UpdateTransfer(_ context.Context, transferID string, params models.UpdateTransferParams) error { + transfer := r.transfers[transferID] + if params.ManifestObjectKey != nil { + transfer.ManifestObjectKey = *params.ManifestObjectKey + } + if params.ExpiresAt != nil { + transfer.ExpiresAt = *params.ExpiresAt + } + if params.ManifestCiphertextSize != nil { + transfer.ManifestCiphertextSize = *params.ManifestCiphertextSize + } + r.transfers[transferID] = transfer + return nil +} + +func (r *routerRepository) MarkDeleted(_ context.Context, transferID string) error { + transfer := r.transfers[transferID] + now := time.Now().UTC() + transfer.Status = models.TransferStatusDeleted + transfer.DeletedAt = &now + r.transfers[transferID] = transfer + return nil +} + +func (r *routerRepository) ListCleanupCandidates(context.Context, int) ([]models.Transfer, error) { + return nil, nil +} + +func (r *routerRepository) MarkPurged(context.Context, string) error { + return nil +} + +type routerStorage struct{} + +func (s *routerStorage) PresignUpload(_ context.Context, objectKey string, _ time.Duration) (string, error) { + return "https://example.test/upload/" + objectKey, nil +} + +func (s *routerStorage) PresignDownload(_ context.Context, objectKey string, _ time.Duration) (string, error) { + return "https://example.test/download/" + objectKey, nil +} + +func (s *routerStorage) PutObject(context.Context, string, []byte, string) error { + return nil +} + +func (s *routerStorage) DeletePrefix(context.Context, string) error { + return nil +} + +func (s *routerStorage) EnsureBucket(context.Context) error { + return nil +} + +func TestManageTransferEndpointsLifecycle(t *testing.T) { + t.Parallel() + + router, repository := newTestRouterWithRepository() + + createResponse := struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + }{} + create := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusCreated, create.Code) + require.NoError(t, json.Unmarshal(create.Body.Bytes(), &createResponse)) + + getResponse := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodGet, + "/api/v1/transfers/"+createResponse.TransferID+"/", + createResponse.ManageToken, + "", + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, getResponse.Code) + require.Contains(t, getResponse.Body.String(), createResponse.TransferID) + + patchBody := `{"ciphertextBase64":"` + base64.StdEncoding.EncodeToString([]byte("updated-manifest")) + `"}` + patchResponse := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPatch, + "/api/v1/transfers/"+createResponse.TransferID+"/", + createResponse.ManageToken, + patchBody, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, patchResponse.Code) + + updatedTransfer, err := repository.GetTransfer(context.Background(), createResponse.TransferID) + require.NoError(t, err) + require.Equal(t, "transfers/"+createResponse.TransferID+"/manifest.bin", updatedTransfer.ManifestObjectKey) + require.Equal(t, int64(len("updated-manifest")), updatedTransfer.ManifestCiphertextSize) + + resumeResponse := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodGet, + "/api/v1/transfers/"+createResponse.TransferID+"/resume", + createResponse.ManageToken, + "", + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, resumeResponse.Code) + require.Contains(t, resumeResponse.Body.String(), createResponse.TransferID) + + deleteResponse := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodDelete, + "/api/v1/transfers/"+createResponse.TransferID+"/", + createResponse.ManageToken, + "", + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusNoContent, deleteResponse.Code) + + deletedTransfer, err := repository.GetTransfer(context.Background(), createResponse.TransferID) + require.NoError(t, err) + require.Equal(t, models.TransferStatusDeleted, deletedTransfer.Status) + require.NotNil(t, deletedTransfer.DeletedAt) +} + +func TestTransferLifecycleEndpointsWithInMemoryDependencies(t *testing.T) { + t.Parallel() + + router := newTestRouter() + + createResponse := struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + }{} + create := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusCreated, create.Code) + require.NoError(t, json.Unmarshal(create.Body.Bytes(), &createResponse)) + + register := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/"+createResponse.TransferID+"/files", + createResponse.ManageToken, + `[{"fileId":"file-a","totalChunks":2,"ciphertextBytes":64,"plaintextBytes":32,"chunkSize":32}]`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, register.Code) + + uploadURLs := struct { + Items []struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + ObjectKey string `json:"objectKey"` + URL string `json:"url"` + } `json:"items"` + }{} + createUploads := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/"+createResponse.TransferID+"/upload-urls", + createResponse.ManageToken, + `{"chunks":[{"fileId":"file-a","chunkIndex":0},{"fileId":"file-a","chunkIndex":1}]}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, createUploads.Code) + require.NoError(t, json.Unmarshal(createUploads.Body.Bytes(), &uploadURLs)) + require.Len(t, uploadURLs.Items, 2) + require.Contains(t, uploadURLs.Items[0].URL, uploadURLs.Items[0].ObjectKey) + + complete := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/"+createResponse.TransferID+"/chunks/complete", + createResponse.ManageToken, + `[{"fileId":"file-a","chunkIndex":0,"ciphertextSize":32,"checksumSha256":"a"},{"fileId":"file-a","chunkIndex":1,"ciphertextSize":32,"checksumSha256":"b"}]`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, complete.Code) + + putManifest := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/"+createResponse.TransferID+"/manifest", + createResponse.ManageToken, + `{"ciphertextBase64":"`+base64.StdEncoding.EncodeToString([]byte("manifest"))+`"}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, putManifest.Code) + + finalize := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/transfers/"+createResponse.TransferID+"/finalize", + createResponse.ManageToken, + `{"wrappedRootKey":"wrapped","totalFiles":1,"totalCiphertextBytes":64}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, finalize.Code) + + publicTransfer := struct { + Status string `json:"status"` + ManifestURL string `json:"manifestUrl"` + WrappedRootKey string `json:"wrappedRootKey"` + }{} + getPublic := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodGet, + "/api/v1/public/transfers/"+createResponse.TransferID+"/", + "", + "", + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, getPublic.Code) + require.NoError(t, json.Unmarshal(getPublic.Body.Bytes(), &publicTransfer)) + require.Equal(t, "ready", publicTransfer.Status) + require.Contains(t, publicTransfer.ManifestURL, "transfers/"+createResponse.TransferID+"/manifest.bin") + require.Equal(t, "wrapped", publicTransfer.WrappedRootKey) + + downloadURLs := struct { + Items []struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + URL string `json:"url"` + } `json:"items"` + }{} + createDownloads := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/public/transfers/"+createResponse.TransferID+"/download-urls", + "", + `{"chunks":[{"fileId":"file-a","chunkIndex":1}]}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusOK, createDownloads.Code) + require.NoError(t, json.Unmarshal(createDownloads.Body.Bytes(), &downloadURLs)) + require.Len(t, downloadURLs.Items, 1) + require.Contains(t, downloadURLs.Items[0].URL, "transfers/"+createResponse.TransferID+"/files/file-a/chunks/00000001.bin") +} + +func TestManageTransferEndpointsRejectInvalidPayloadsAndMissingTokens(t *testing.T) { + t.Parallel() + + router := newTestRouter() + + createResponse := struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + }{} + create := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusCreated, create.Code) + require.NoError(t, json.Unmarshal(create.Body.Bytes(), &createResponse)) + + patchResponse := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPatch, + "/api/v1/transfers/"+createResponse.TransferID+"/", + createResponse.ManageToken, + `{"expiresInSeconds":3600}{"extra":true}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusBadRequest, patchResponse.Code) + require.Contains(t, patchResponse.Body.String(), `"error":"invalid_json"`) + + getResponse := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodGet, + "/api/v1/transfers/"+createResponse.TransferID+"/", + "", + "", + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusUnauthorized, getResponse.Code) + require.Contains(t, getResponse.Body.String(), `"error":"missing_manage_token"`) + + patchUnauthorized := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPatch, + "/api/v1/transfers/"+createResponse.TransferID+"/", + "", + `{"expiresInSeconds":3600}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusUnauthorized, patchUnauthorized.Code) + require.Contains(t, patchUnauthorized.Body.String(), `"error":"missing_manage_token"`) +} + +func TestTransferEndpointsRejectInvalidJSON(t *testing.T) { + t.Parallel() + + router := newTestRouter() + + createResponse := struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + }{} + create := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusCreated, create.Code) + require.NoError(t, json.Unmarshal(create.Body.Bytes(), &createResponse)) + + testCases := []struct { + name string + method string + path string + body string + }{ + {name: "register files", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/files", body: `{"broken":true}`}, + {name: "create upload urls", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/upload-urls", body: `{"chunks":"broken"}`}, + {name: "complete chunks", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/chunks/complete", body: `{"broken":true}`}, + {name: "put manifest", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/manifest", body: `{"ciphertextBase64":123}`}, + {name: "finalize", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/finalize", body: `{"wrappedRootKey":123}`}, + {name: "download urls", method: nethttp.MethodPost, path: "/api/v1/public/transfers/" + createResponse.TransferID + "/download-urls", body: `{"chunks":"broken"}`}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + bearer := createResponse.ManageToken + if tc.name == "download urls" { + bearer = "" + } + response := performAuthorizedJSONRequest(t, router, tc.method, tc.path, bearer, tc.body, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), `"error":"invalid_json"`) + }) + } +} + +func TestTransferEndpointsSurfaceServiceErrors(t *testing.T) { + t.Parallel() + + router := newTestRouter() + + createResponse := struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + }{} + create := performJSONRequest(t, router, nethttp.MethodPost, "/api/v1/transfers/", `{"expiresInSeconds":3600}`, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusCreated, create.Code) + require.NoError(t, json.Unmarshal(create.Body.Bytes(), &createResponse)) + + managePaths := []struct { + name string + method string + path string + body string + }{ + {name: "register files", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/files", body: `[{"fileId":"file-a","totalChunks":1,"ciphertextBytes":32,"chunkSize":32}]`}, + {name: "create upload urls", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/upload-urls", body: `{"chunks":[{"fileId":"file-a","chunkIndex":0}]}`}, + {name: "complete chunks", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/chunks/complete", body: `[{"fileId":"file-a","chunkIndex":0,"ciphertextSize":32,"checksumSha256":"a"}]`}, + {name: "put manifest", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/manifest", body: `{"ciphertextBase64":"` + base64.StdEncoding.EncodeToString([]byte("manifest")) + `"}`}, + {name: "finalize", method: nethttp.MethodPost, path: "/api/v1/transfers/" + createResponse.TransferID + "/finalize", body: `{"wrappedRootKey":"wrapped","totalFiles":1,"totalCiphertextBytes":64}`}, + {name: "delete transfer", method: nethttp.MethodDelete, path: "/api/v1/transfers/" + createResponse.TransferID + "/", body: ``}, + {name: "resume transfer", method: nethttp.MethodGet, path: "/api/v1/transfers/" + createResponse.TransferID + "/resume", body: ``}, + } + + for _, tc := range managePaths { + tc := tc + t.Run(tc.name, func(t *testing.T) { + response := performAuthorizedJSONRequest(t, router, tc.method, tc.path, "", tc.body, "198.51.100.42:40001") + require.Equal(t, nethttp.StatusUnauthorized, response.Code) + require.Contains(t, response.Body.String(), `"error":"missing_manage_token"`) + }) + } + + getPublic := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodGet, + "/api/v1/public/transfers/missing-transfer/", + "", + "", + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusNotFound, getPublic.Code) + require.Contains(t, getPublic.Body.String(), `"error":"not_found"`) + + createDownloads := performAuthorizedJSONRequest( + t, + router, + nethttp.MethodPost, + "/api/v1/public/transfers/missing-transfer/download-urls", + "", + `{"chunks":[{"fileId":"file-a","chunkIndex":0}]}`, + "198.51.100.42:40001", + ) + require.Equal(t, nethttp.StatusNotFound, createDownloads.Code) + require.Contains(t, createDownloads.Body.String(), `"error":"not_found"`) +} + +func TestWriteErrorAndHeaderHelpers(t *testing.T) { + t.Parallel() + + t.Run("write error handles generic failures", func(t *testing.T) { + t.Parallel() + + response := httptest.NewRecorder() + writeError(response, errors.New("boom")) + + require.Equal(t, nethttp.StatusInternalServerError, response.Code) + require.Contains(t, response.Body.String(), `"error":"internal_error"`) + }) + + t.Run("bearer token trims and validates prefixes", func(t *testing.T) { + t.Parallel() + + request := httptest.NewRequest(nethttp.MethodGet, "/healthz", nil) + request.Header.Set("Authorization", " Bearer test-token ") + require.Equal(t, "test-token", bearerToken(request)) + + request.Header.Set("Authorization", "Token test-token") + require.Empty(t, bearerToken(request)) + }) + + t.Run("client key handles empty, hostport and raw values", func(t *testing.T) { + t.Parallel() + + request := httptest.NewRequest(nethttp.MethodGet, "/healthz", nil) + request.RemoteAddr = "198.51.100.42:41000" + require.Equal(t, "198.51.100.42", clientKey(request)) + + request.RemoteAddr = "198.51.100.42" + require.Equal(t, "198.51.100.42", clientKey(request)) + + request.RemoteAddr = "" + require.Empty(t, clientKey(request)) + }) +} + +func newTestRouterWithRepository() (nethttp.Handler, *routerRepository) { + cfg := testRouterConfig() + repository := newRouterRepository() + router := NewRouter( + cfg, + slog.New(slog.NewTextHandler(io.Discard, nil)), + service.New(cfg, repository, &routerStorage{}, ratelimit.NewMemoryLimiter()), + ) + return router, repository +} + +func performAuthorizedJSONRequest(t *testing.T, handler nethttp.Handler, method string, path string, bearer string, body string, remoteAddr string) *httptest.ResponseRecorder { + t.Helper() + + request := httptest.NewRequest(method, path, bytes.NewBufferString(body)) + if body != "" { + request.Header.Set("Content-Type", "application/json") + } + if bearer != "" { + request.Header.Set("Authorization", "Bearer "+bearer) + } + request.RemoteAddr = remoteAddr + + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + return response +} diff --git a/apps/api/internal/jobs/expiry.go b/apps/api/internal/jobs/expiry.go new file mode 100644 index 0000000..30af305 --- /dev/null +++ b/apps/api/internal/jobs/expiry.go @@ -0,0 +1,26 @@ +package jobs + +import ( + "context" + "log/slog" + "time" + + "github.com/xdrop/monorepo/internal/service" +) + +// StartCleanup periodically purges expired or deleted transfers until the context is canceled. +func StartCleanup(ctx context.Context, logger *slog.Logger, interval time.Duration, svc *service.Service) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := svc.CleanupExpired(ctx); err != nil { + logger.Error("cleanup tick failed", "error", err) + } + } + } +} diff --git a/apps/api/internal/jobs/expiry_test.go b/apps/api/internal/jobs/expiry_test.go new file mode 100644 index 0000000..0a23fa7 --- /dev/null +++ b/apps/api/internal/jobs/expiry_test.go @@ -0,0 +1,223 @@ +package jobs + +import ( + "bytes" + "context" + "errors" + "io" + "log/slog" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/models" + "github.com/xdrop/monorepo/internal/repo" + "github.com/xdrop/monorepo/internal/service" +) + +func TestStartCleanupPurgesExpiredTransfersOnTick(t *testing.T) { + t.Parallel() + + cleanupRepo := &cleanupRepository{ + transfer: models.Transfer{ + ID: "expired-transfer", + Status: models.TransferStatusReady, + ExpiresAt: time.Now().UTC().Add(-time.Hour), + }, + purged: make(chan struct{}), + } + objectStorage := &cleanupStorage{deletedPrefixes: make(chan string, 1)} + svc := service.New(config.Config{}, cleanupRepo, objectStorage, nil) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan struct{}) + go func() { + StartCleanup(ctx, slog.New(slog.NewTextHandler(io.Discard, nil)), 10*time.Millisecond, svc) + close(done) + }() + + select { + case prefix := <-objectStorage.deletedPrefixes: + require.Equal(t, "transfers/expired-transfer/", prefix) + case <-time.After(time.Second): + t.Fatal("cleanup job did not delete expired transfer objects") + } + + select { + case <-cleanupRepo.purged: + case <-time.After(time.Second): + t.Fatal("cleanup job did not mark transfer as purged") + } + + cancel() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("cleanup job did not stop after cancellation") + } +} + +func TestStartCleanupStopsWhenContextIsCancelled(t *testing.T) { + t.Parallel() + + svc := service.New(config.Config{}, &cleanupRepository{purged: make(chan struct{})}, &cleanupStorage{ + deletedPrefixes: make(chan string, 1), + }, nil) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + StartCleanup(ctx, slog.New(slog.NewTextHandler(io.Discard, nil)), time.Hour, svc) + close(done) + }() + + cancel() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("cleanup job did not exit after cancellation") + } +} + +func TestStartCleanupLogsCleanupErrors(t *testing.T) { + t.Parallel() + + var buffer bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buffer, nil)) + svc := service.New(config.Config{}, &cleanupRepository{ + listErr: errors.New("cleanup failed"), + purged: make(chan struct{}), + }, &cleanupStorage{ + deletedPrefixes: make(chan string, 1), + }, nil) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + StartCleanup(ctx, logger, 10*time.Millisecond, svc) + close(done) + }() + + require.Eventually(t, func() bool { + return strings.Contains(buffer.String(), "cleanup tick failed") + }, time.Second, 10*time.Millisecond) + + cancel() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("cleanup job did not stop after logging an error") + } +} + +type cleanupRepository struct { + mu sync.Mutex + listErr error + transfer models.Transfer + purged chan struct{} + once sync.Once +} + +func (r *cleanupRepository) CreateTransfer(context.Context, models.Transfer) error { + return nil +} + +func (r *cleanupRepository) GetTransfer(context.Context, string) (models.Transfer, error) { + return models.Transfer{}, repo.ErrNotFound +} + +func (r *cleanupRepository) RegisterFiles(context.Context, string, []models.TransferFile) error { + return nil +} + +func (r *cleanupRepository) ListFiles(context.Context, string) ([]models.TransferFile, error) { + return nil, nil +} + +func (r *cleanupRepository) CompleteChunks(context.Context, string, []models.TransferChunk) error { + return nil +} + +func (r *cleanupRepository) GetResumeState(context.Context, string) (models.TransferResumeState, error) { + return models.TransferResumeState{}, nil +} + +func (r *cleanupRepository) SetManifest(context.Context, string, string, int64) error { + return nil +} + +func (r *cleanupRepository) FinalizeTransfer(context.Context, string, string, int, int64) error { + return nil +} + +func (r *cleanupRepository) UpdateTransfer(context.Context, string, models.UpdateTransferParams) error { + return nil +} + +func (r *cleanupRepository) MarkDeleted(context.Context, string) error { + return nil +} + +func (r *cleanupRepository) ListCleanupCandidates(context.Context, int) ([]models.Transfer, error) { + if r.listErr != nil { + return nil, r.listErr + } + + r.mu.Lock() + defer r.mu.Unlock() + + if r.transfer.ID == "" || r.transfer.PurgedAt != nil { + return nil, nil + } + + return []models.Transfer{r.transfer}, nil +} + +func (r *cleanupRepository) MarkPurged(context.Context, string) error { + r.mu.Lock() + defer r.mu.Unlock() + + now := time.Now().UTC() + r.transfer.PurgedAt = &now + r.transfer.Status = models.TransferStatusExpired + r.once.Do(func() { + close(r.purged) + }) + return nil +} + +type cleanupStorage struct { + deletedPrefixes chan string +} + +func (s *cleanupStorage) PresignUpload(context.Context, string, time.Duration) (string, error) { + return "", nil +} + +func (s *cleanupStorage) PresignDownload(context.Context, string, time.Duration) (string, error) { + return "", nil +} + +func (s *cleanupStorage) PutObject(context.Context, string, []byte, string) error { + return nil +} + +func (s *cleanupStorage) DeletePrefix(_ context.Context, prefix string) error { + select { + case s.deletedPrefixes <- prefix: + default: + } + return nil +} + +func (s *cleanupStorage) EnsureBucket(context.Context) error { + return nil +} diff --git a/apps/api/internal/models/models.go b/apps/api/internal/models/models.go new file mode 100644 index 0000000..0e87081 --- /dev/null +++ b/apps/api/internal/models/models.go @@ -0,0 +1,80 @@ +package models + +import "time" + +// TransferStatus describes the lifecycle stage of a transfer in persistent storage. +type TransferStatus string + +const ( + // Draft transfers exist but do not have any registered files yet. + TransferStatusDraft TransferStatus = "draft" + // Uploading transfers have registered files and may still be receiving chunks. + TransferStatusUploading TransferStatus = "uploading" + // Ready transfers have a manifest, wrapped root key, and all chunks uploaded. + TransferStatusReady TransferStatus = "ready" + // Incomplete transfers are visible to the public API but not yet downloadable. + TransferStatusIncomplete TransferStatus = "incomplete" + // Expired transfers have passed their retention window and await cleanup. + TransferStatusExpired TransferStatus = "expired" + // Deleted transfers were explicitly removed by their creator. + TransferStatusDeleted TransferStatus = "deleted" + // Failed transfers represent interrupted local state that never became ready. + TransferStatusFailed TransferStatus = "failed" +) + +// Transfer stores transfer-level metadata and lifecycle state. +type Transfer struct { + ID string + Status TransferStatus + WrappedRootKey string + ManifestObjectKey string + ManifestCiphertextSize int64 + TotalFiles int + TotalCiphertextBytes int64 + ExpiresAt time.Time + CreatedAt time.Time + UpdatedAt time.Time + FinalizedAt *time.Time + ManageTokenHash string + DeletedAt *time.Time + PurgedAt *time.Time +} + +// TransferFile stores file-level metadata for a transfer. +type TransferFile struct { + TransferID string + OpaqueFileID string + TotalChunks int + CiphertextBytes int64 + PlaintextBytes *int64 + ChunkSize int64 + UploadStatus string + CreatedAt time.Time + UpdatedAt time.Time +} + +// TransferChunk tracks each uploaded ciphertext chunk. +type TransferChunk struct { + TransferID string + OpaqueFileID string + ChunkIndex int + ObjectKey string + CiphertextSize int64 + ChecksumSHA256 string + UploadedAt time.Time +} + +// TransferResumeState combines a transfer with file metadata and completed chunk indexes. +type TransferResumeState struct { + Transfer Transfer + Files []TransferFile + UploadedChunks map[string][]int +} + +// UpdateTransferParams describes the mutable fields the service can update after creation. +type UpdateTransferParams struct { + ManifestObjectKey *string + ExpiresAt *time.Time + EncryptedManifest []byte + ManifestCiphertextSize *int64 +} diff --git a/apps/api/internal/ratelimit/limiter.go b/apps/api/internal/ratelimit/limiter.go new file mode 100644 index 0000000..be973cd --- /dev/null +++ b/apps/api/internal/ratelimit/limiter.go @@ -0,0 +1,82 @@ +package ratelimit + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/redis/go-redis/v9" +) + +// Limiter decides whether a caller may proceed within a given fixed time window. +type Limiter interface { + Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) +} + +// RedisLimiter stores counters in Redis so limits can be shared across API instances. +type RedisLimiter struct { + client *redis.Client +} + +// NewRedisLimiter builds a Redis-backed limiter for production deployments. +func NewRedisLimiter(client *redis.Client) *RedisLimiter { + return &RedisLimiter{client: client} +} + +// Allow increments the caller key and compares the result with the configured limit. +func (l *RedisLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) { + if limit <= 0 { + return true, nil + } + + pipeline := l.client.TxPipeline() + countCmd := pipeline.Incr(ctx, key) + pipeline.Expire(ctx, key, window) + + if _, err := pipeline.Exec(ctx); err != nil { + return false, fmt.Errorf("exec rate limit pipeline: %w", err) + } + + return countCmd.Val() <= int64(limit), nil +} + +// MemoryLimiter provides an in-process limiter for tests and single-node development. +type MemoryLimiter struct { + mu sync.Mutex + entries map[string]memoryEntry +} + +type memoryEntry struct { + count int + deadline time.Time +} + +// NewMemoryLimiter builds an empty in-memory limiter state. +func NewMemoryLimiter() *MemoryLimiter { + return &MemoryLimiter{ + entries: map[string]memoryEntry{}, + } +} + +// Allow applies the same fixed-window semantics as RedisLimiter without external state. +func (l *MemoryLimiter) Allow(_ context.Context, key string, limit int, window time.Duration) (bool, error) { + if limit <= 0 { + return true, nil + } + + now := time.Now() + + l.mu.Lock() + defer l.mu.Unlock() + + entry, ok := l.entries[key] + if !ok || now.After(entry.deadline) { + entry = memoryEntry{count: 0, deadline: now.Add(window)} + } + + entry.count++ + l.entries[key] = entry + + return entry.count <= limit, nil +} diff --git a/apps/api/internal/ratelimit/limiter_integration_test.go b/apps/api/internal/ratelimit/limiter_integration_test.go new file mode 100644 index 0000000..1198e37 --- /dev/null +++ b/apps/api/internal/ratelimit/limiter_integration_test.go @@ -0,0 +1,82 @@ +package ratelimit + +import ( + "context" + "fmt" + "os/exec" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestRedisLimiterBlocksThenResetsAfterWindow(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + client := startRedisClient(t, ctx) + limiter := NewRedisLimiter(client) + + key := "rate-limit:test" + allowed, err := limiter.Allow(ctx, key, 1, 2*time.Second) + require.NoError(t, err) + require.True(t, allowed) + + allowed, err = limiter.Allow(ctx, key, 1, 2*time.Second) + require.NoError(t, err) + require.False(t, allowed) + + time.Sleep(2200 * time.Millisecond) + + allowed, err = limiter.Allow(ctx, key, 1, 2*time.Second) + require.NoError(t, err) + require.True(t, allowed) +} + +func startRedisClient(t *testing.T, ctx context.Context) *redis.Client { + t.Helper() + + container, err := testcontainers.Run( + ctx, + "redis:7-alpine", + testcontainers.WithExposedPorts("6379/tcp"), + testcontainers.WithWaitStrategy( + wait.ForLog("Ready to accept connections").WithStartupTimeout(60*time.Second), + ), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, testcontainers.TerminateContainer(container)) + }) + + host, err := container.Host(ctx) + require.NoError(t, err) + port, err := container.MappedPort(ctx, "6379/tcp") + require.NoError(t, err) + + client := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%s", host, port.Port()), + DB: 0, + }) + require.NoError(t, client.Ping(ctx).Err()) + t.Cleanup(func() { + require.NoError(t, client.Close()) + }) + + return client +} + +func skipIfDockerUnavailable(t *testing.T) { + t.Helper() + + if testing.Short() { + t.Skip("skipping docker-backed integration test in short mode") + } + + if err := exec.Command("docker", "info").Run(); err != nil { + t.Skipf("skipping docker-backed integration test: %v", err) + } +} diff --git a/apps/api/internal/ratelimit/limiter_test.go b/apps/api/internal/ratelimit/limiter_test.go new file mode 100644 index 0000000..37f8524 --- /dev/null +++ b/apps/api/internal/ratelimit/limiter_test.go @@ -0,0 +1,86 @@ +package ratelimit + +import ( + "context" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" +) + +func TestMemoryLimiterAllowsUpToLimitThenBlocks(t *testing.T) { + t.Parallel() + + limiter := NewMemoryLimiter() + + allowed, err := limiter.Allow(context.Background(), "client-a", 2, time.Minute) + require.NoError(t, err) + require.True(t, allowed) + + allowed, err = limiter.Allow(context.Background(), "client-a", 2, time.Minute) + require.NoError(t, err) + require.True(t, allowed) + + allowed, err = limiter.Allow(context.Background(), "client-a", 2, time.Minute) + require.NoError(t, err) + require.False(t, allowed) +} + +func TestMemoryLimiterResetsAfterWindow(t *testing.T) { + t.Parallel() + + limiter := NewMemoryLimiter() + + allowed, err := limiter.Allow(context.Background(), "client-a", 1, 10*time.Millisecond) + require.NoError(t, err) + require.True(t, allowed) + + allowed, err = limiter.Allow(context.Background(), "client-a", 1, 10*time.Millisecond) + require.NoError(t, err) + require.False(t, allowed) + + time.Sleep(25 * time.Millisecond) + + allowed, err = limiter.Allow(context.Background(), "client-a", 1, 10*time.Millisecond) + require.NoError(t, err) + require.True(t, allowed) +} + +func TestMemoryLimiterAllowsNonPositiveLimits(t *testing.T) { + t.Parallel() + + limiter := NewMemoryLimiter() + + for _, limit := range []int{0, -1} { + allowed, err := limiter.Allow(context.Background(), "client-a", limit, time.Minute) + require.NoError(t, err) + require.True(t, allowed) + } +} + +func TestRedisLimiterAllowsNonPositiveLimits(t *testing.T) { + t.Parallel() + + allowed, err := NewRedisLimiter(nil).Allow(context.Background(), "client-a", 0, time.Minute) + require.NoError(t, err) + require.True(t, allowed) +} + +func TestRedisLimiterReturnsPipelineErrors(t *testing.T) { + t.Parallel() + + client := redis.NewClient(&redis.Options{ + Addr: "127.0.0.1:1", + DialTimeout: 20 * time.Millisecond, + ReadTimeout: 20 * time.Millisecond, + WriteTimeout: 20 * time.Millisecond, + }) + t.Cleanup(func() { + _ = client.Close() + }) + + allowed, err := NewRedisLimiter(client).Allow(context.Background(), "client-a", 1, time.Minute) + require.False(t, allowed) + require.ErrorContains(t, err, "exec rate limit pipeline") +} diff --git a/apps/api/internal/repo/migrate.go b/apps/api/internal/repo/migrate.go new file mode 100644 index 0000000..547e714 --- /dev/null +++ b/apps/api/internal/repo/migrate.go @@ -0,0 +1,93 @@ +package repo + +import ( + "context" + "embed" + "fmt" + "io/fs" + "path/filepath" + "sort" + + "github.com/jackc/pgx/v5/pgxpool" +) + +//go:embed migrations/*.sql +var migrationFS embed.FS + +var ( + embeddedMigrationFiles migrationFiles = migrationFS + newMigrationDBFromPool = func(db *pgxpool.Pool) postgresDB { + return pgxPoolDB{pool: db} + } +) + +// RunMigrations applies embedded SQL migrations in lexical order and records each one once. +func RunMigrations(ctx context.Context, db *pgxpool.Pool) error { + return runMigrations(ctx, newMigrationDBFromPool(db), embeddedMigrationFiles) +} + +// migrationFiles abstracts embedded files so migration tests can inject custom fixtures. +type migrationFiles interface { + ReadDir(name string) ([]fs.DirEntry, error) + ReadFile(name string) ([]byte, error) +} + +// runMigrations ensures each migration is executed inside its own transaction. +func runMigrations(ctx context.Context, db postgresDB, files migrationFiles) error { + if err := db.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS schema_migrations ( + name text PRIMARY KEY, + applied_at timestamptz NOT NULL DEFAULT now() + ) + `); err != nil { + return fmt.Errorf("create schema_migrations: %w", err) + } + + entries, err := files.ReadDir("migrations") + if err != nil { + return fmt.Errorf("read migrations: %w", err) + } + + names := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + names = append(names, entry.Name()) + } + sort.Strings(names) + + for _, name := range names { + var exists bool + if err := db.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE name = $1)`, name).Scan(&exists); err != nil { + return fmt.Errorf("query migration %s: %w", name, err) + } + if exists { + continue + } + + payload, err := files.ReadFile(filepath.ToSlash(filepath.Join("migrations", name))) + if err != nil { + return fmt.Errorf("read migration %s: %w", name, err) + } + + tx, err := db.Begin(ctx) + if err != nil { + return fmt.Errorf("begin migration %s: %w", name, err) + } + + if err = tx.Exec(ctx, string(payload)); err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("apply migration %s: %w", name, err) + } + if err = tx.Exec(ctx, `INSERT INTO schema_migrations (name) VALUES ($1)`, name); err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("record migration %s: %w", name, err) + } + if err = tx.Commit(ctx); err != nil { + return fmt.Errorf("commit migration %s: %w", name, err) + } + } + + return nil +} diff --git a/apps/api/internal/repo/migrate_unit_test.go b/apps/api/internal/repo/migrate_unit_test.go new file mode 100644 index 0000000..ea3b890 --- /dev/null +++ b/apps/api/internal/repo/migrate_unit_test.go @@ -0,0 +1,222 @@ +package repo + +import ( + "context" + "errors" + "io/fs" + "strings" + "testing" + "testing/fstest" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/require" +) + +func TestRunMigrationsUnit(t *testing.T) { + t.Parallel() + + t.Run("create schema failure", func(t *testing.T) { + err := runMigrations(context.Background(), stubPostgresDB{ + execFn: func(context.Context, string, ...any) error { + return errors.New("boom") + }, + }, fstest.MapFS{}) + require.ErrorContains(t, err, "create schema_migrations") + }) + + t.Run("read dir failure", func(t *testing.T) { + err := runMigrations(context.Background(), stubPostgresDB{}, stubMigrationFiles{ + readDirFn: func(string) ([]fs.DirEntry, error) { + return nil, errors.New("boom") + }, + }) + require.ErrorContains(t, err, "read migrations") + }) + + t.Run("query existing migration failure", func(t *testing.T) { + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{err: errors.New("boom")} + }, + }, fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }) + require.ErrorContains(t, err, "query migration 001_first.sql") + }) + + t.Run("read migration failure", func(t *testing.T) { + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + }, stubMigrationFiles{ + readDirFn: func(string) ([]fs.DirEntry, error) { + return fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }.ReadDir("migrations") + }, + readFileFn: func(string) ([]byte, error) { + return nil, errors.New("boom") + }, + }) + require.ErrorContains(t, err, "read migration 001_first.sql") + }) + + t.Run("begin migration failure", func(t *testing.T) { + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return nil, errors.New("boom") + }, + }, fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }) + require.ErrorContains(t, err, "begin migration 001_first.sql") + }) + + t.Run("apply migration failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(context.Context, string, ...any) error { + return errors.New("boom") + }, + } + + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }, fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }) + + require.ErrorContains(t, err, "apply migration 001_first.sql") + require.True(t, tx.rollbackCalled) + }) + + t.Run("record migration failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "INSERT INTO schema_migrations") { + return errors.New("boom") + } + return nil + }, + } + + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }, fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }) + + require.ErrorContains(t, err, "record migration 001_first.sql") + require.True(t, tx.rollbackCalled) + }) + + t.Run("commit migration failure", func(t *testing.T) { + tx := &stubPostgresTx{commitErr: errors.New("boom")} + + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }, fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }) + + require.ErrorContains(t, err, "commit migration 001_first.sql") + }) + + t.Run("success sorts and skips existing migrations", func(t *testing.T) { + applied := []string{} + checked := []string{} + + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, args ...any) error { + if strings.Contains(sql, "INSERT INTO schema_migrations") { + applied = append(applied, args[0].(string)) + } + return nil + }, + } + + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(_ context.Context, _ string, args ...any) postgresRow { + name := args[0].(string) + checked = append(checked, name) + return stubRow{values: []any{name == "001_first.sql"}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }, fstest.MapFS{ + "migrations/002_second.sql": &fstest.MapFile{Data: []byte("select 2;")}, + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + }) + + require.NoError(t, err) + require.Equal(t, []string{"001_first.sql", "002_second.sql"}, checked) + require.Equal(t, []string{"002_second.sql"}, applied) + }) + + t.Run("ignores directory entries", func(t *testing.T) { + tx := &stubPostgresTx{} + + err := runMigrations(context.Background(), stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }, fstest.MapFS{ + "migrations/nested": &fstest.MapFile{Mode: fs.ModeDir}, + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + "migrations/nested/skip.sql": &fstest.MapFile{Data: []byte("select 2;")}, + }) + + require.NoError(t, err) + require.True(t, tx.commitCalled) + }) + + t.Run("RunMigrations uses the pool wrapper and embedded files", func(t *testing.T) { + originalFiles := embeddedMigrationFiles + originalFactory := newMigrationDBFromPool + t.Cleanup(func() { + embeddedMigrationFiles = originalFiles + newMigrationDBFromPool = originalFactory + }) + + embeddedMigrationFiles = fstest.MapFS{ + "migrations/001_first.sql": &fstest.MapFile{Data: []byte("select 1;")}, + } + + tx := &stubPostgresTx{} + newMigrationDBFromPool = func(*pgxpool.Pool) postgresDB { + return stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{false}} + }, + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + } + } + + require.NoError(t, RunMigrations(context.Background(), nil)) + require.True(t, tx.commitCalled) + }) +} diff --git a/apps/api/internal/repo/migrations/001_init.sql b/apps/api/internal/repo/migrations/001_init.sql new file mode 100644 index 0000000..91f4b18 --- /dev/null +++ b/apps/api/internal/repo/migrations/001_init.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS transfers ( + id text PRIMARY KEY, + status text NOT NULL, + wrapped_root_key text NOT NULL DEFAULT '', + manifest_object_key text NOT NULL DEFAULT '', + manifest_ciphertext_size bigint NOT NULL DEFAULT 0, + total_files integer NOT NULL DEFAULT 0, + total_ciphertext_bytes bigint NOT NULL DEFAULT 0, + expires_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + finalized_at timestamptz, + manage_token_hash text NOT NULL, + deleted_at timestamptz, + purged_at timestamptz +); + +CREATE TABLE IF NOT EXISTS transfer_files ( + id bigserial PRIMARY KEY, + transfer_id text NOT NULL REFERENCES transfers(id) ON DELETE CASCADE, + opaque_file_id text NOT NULL, + total_chunks integer NOT NULL, + ciphertext_bytes bigint NOT NULL, + plaintext_bytes bigint, + chunk_size bigint NOT NULL, + upload_status text NOT NULL DEFAULT 'pending', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (transfer_id, opaque_file_id) +); + +CREATE TABLE IF NOT EXISTS transfer_chunks ( + id bigserial PRIMARY KEY, + transfer_id text NOT NULL REFERENCES transfers(id) ON DELETE CASCADE, + opaque_file_id text NOT NULL, + chunk_index integer NOT NULL, + object_key text NOT NULL, + ciphertext_size bigint NOT NULL, + checksum_sha256 text NOT NULL, + uploaded_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (transfer_id, opaque_file_id, chunk_index) +); + +CREATE INDEX IF NOT EXISTS idx_transfers_status_expires ON transfers(status, expires_at); +CREATE INDEX IF NOT EXISTS idx_transfer_files_transfer_id ON transfer_files(transfer_id); +CREATE INDEX IF NOT EXISTS idx_transfer_chunks_transfer_file ON transfer_chunks(transfer_id, opaque_file_id); diff --git a/apps/api/internal/repo/postgres.go b/apps/api/internal/repo/postgres.go new file mode 100644 index 0000000..6bda89f --- /dev/null +++ b/apps/api/internal/repo/postgres.go @@ -0,0 +1,510 @@ +package repo + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/xdrop/monorepo/internal/models" +) + +// ErrNotFound reports that a requested transfer does not exist. +var ErrNotFound = errors.New("not found") + +type postgresRow interface { + Scan(dest ...any) error +} + +type postgresRows interface { + Close() + Err() error + Next() bool + Scan(dest ...any) error +} + +type postgresTx interface { + Commit(ctx context.Context) error + Exec(ctx context.Context, sql string, args ...any) error + QueryRow(ctx context.Context, sql string, args ...any) postgresRow + Rollback(ctx context.Context) error +} + +type postgresDB interface { + Begin(ctx context.Context) (postgresTx, error) + Exec(ctx context.Context, sql string, args ...any) error + Query(ctx context.Context, sql string, args ...any) (postgresRows, error) + QueryRow(ctx context.Context, sql string, args ...any) postgresRow +} + +type pgxPoolDB struct { + pool *pgxpool.Pool +} + +func (d pgxPoolDB) Begin(ctx context.Context) (postgresTx, error) { + tx, err := d.pool.Begin(ctx) + if err != nil { + return nil, err + } + + return pgxTx{tx: tx}, nil +} + +func (d pgxPoolDB) Exec(ctx context.Context, sql string, args ...any) error { + _, err := d.pool.Exec(ctx, sql, args...) + return err +} + +func (d pgxPoolDB) Query(ctx context.Context, sql string, args ...any) (postgresRows, error) { + return d.pool.Query(ctx, sql, args...) +} + +func (d pgxPoolDB) QueryRow(ctx context.Context, sql string, args ...any) postgresRow { + return d.pool.QueryRow(ctx, sql, args...) +} + +type pgxTx struct { + tx pgx.Tx +} + +func (t pgxTx) Commit(ctx context.Context) error { + return t.tx.Commit(ctx) +} + +func (t pgxTx) Exec(ctx context.Context, sql string, args ...any) error { + _, err := t.tx.Exec(ctx, sql, args...) + return err +} + +func (t pgxTx) QueryRow(ctx context.Context, sql string, args ...any) postgresRow { + return t.tx.QueryRow(ctx, sql, args...) +} + +func (t pgxTx) Rollback(ctx context.Context) error { + return t.tx.Rollback(ctx) +} + +// PostgresRepository stores transfers, files, and chunks in PostgreSQL. +type PostgresRepository struct { + db postgresDB +} + +// NewPostgresRepository wraps a pgx pool with the repository implementation. +func NewPostgresRepository(db *pgxpool.Pool) *PostgresRepository { + return &PostgresRepository{db: pgxPoolDB{pool: db}} +} + +// CreateTransfer inserts a new transfer in draft state. +func (r *PostgresRepository) CreateTransfer(ctx context.Context, transfer models.Transfer) error { + err := r.db.Exec(ctx, ` + INSERT INTO transfers ( + id, status, expires_at, manage_token_hash, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $5) + `, transfer.ID, transfer.Status, transfer.ExpiresAt, transfer.ManageTokenHash, transfer.CreatedAt) + if err != nil { + return fmt.Errorf("insert transfer: %w", err) + } + + return nil +} + +// GetTransfer loads the persisted transfer record for the given identifier. +func (r *PostgresRepository) GetTransfer(ctx context.Context, transferID string) (models.Transfer, error) { + var transfer models.Transfer + + err := r.db.QueryRow(ctx, ` + SELECT + id, status, wrapped_root_key, manifest_object_key, manifest_ciphertext_size, + total_files, total_ciphertext_bytes, expires_at, created_at, updated_at, + finalized_at, manage_token_hash, deleted_at, purged_at + FROM transfers + WHERE id = $1 + `, transferID).Scan( + &transfer.ID, + &transfer.Status, + &transfer.WrappedRootKey, + &transfer.ManifestObjectKey, + &transfer.ManifestCiphertextSize, + &transfer.TotalFiles, + &transfer.TotalCiphertextBytes, + &transfer.ExpiresAt, + &transfer.CreatedAt, + &transfer.UpdatedAt, + &transfer.FinalizedAt, + &transfer.ManageTokenHash, + &transfer.DeletedAt, + &transfer.PurgedAt, + ) + if errors.Is(err, pgx.ErrNoRows) { + return models.Transfer{}, ErrNotFound + } + if err != nil { + return models.Transfer{}, fmt.Errorf("select transfer: %w", err) + } + + return transfer, nil +} + +// RegisterFiles upserts file metadata and marks the transfer as uploading. +func (r *PostgresRepository) RegisterFiles(ctx context.Context, transferID string, files []models.TransferFile) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return fmt.Errorf("begin register files: %w", err) + } + defer tx.Rollback(ctx) + + for _, file := range files { + err = tx.Exec(ctx, ` + INSERT INTO transfer_files ( + transfer_id, opaque_file_id, total_chunks, ciphertext_bytes, plaintext_bytes, chunk_size, upload_status, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now()) + ON CONFLICT (transfer_id, opaque_file_id) DO UPDATE + SET total_chunks = EXCLUDED.total_chunks, + ciphertext_bytes = EXCLUDED.ciphertext_bytes, + plaintext_bytes = EXCLUDED.plaintext_bytes, + chunk_size = EXCLUDED.chunk_size, + updated_at = now() + `, transferID, file.OpaqueFileID, file.TotalChunks, file.CiphertextBytes, file.PlaintextBytes, file.ChunkSize, "pending") + if err != nil { + return fmt.Errorf("insert file %s: %w", file.OpaqueFileID, err) + } + } + + err = tx.Exec(ctx, `UPDATE transfers SET status = $2, updated_at = now() WHERE id = $1`, transferID, models.TransferStatusUploading) + if err != nil { + return fmt.Errorf("set uploading status: %w", err) + } + + if err = tx.Commit(ctx); err != nil { + return fmt.Errorf("commit register files: %w", err) + } + + return nil +} + +// ListFiles returns every file registered for a transfer in stable order. +func (r *PostgresRepository) ListFiles(ctx context.Context, transferID string) ([]models.TransferFile, error) { + rows, err := r.db.Query(ctx, ` + SELECT transfer_id, opaque_file_id, total_chunks, ciphertext_bytes, plaintext_bytes, chunk_size, upload_status, created_at, updated_at + FROM transfer_files + WHERE transfer_id = $1 + ORDER BY opaque_file_id + `, transferID) + if err != nil { + return nil, fmt.Errorf("query files: %w", err) + } + defer rows.Close() + + files := []models.TransferFile{} + for rows.Next() { + var file models.TransferFile + if err := rows.Scan( + &file.TransferID, + &file.OpaqueFileID, + &file.TotalChunks, + &file.CiphertextBytes, + &file.PlaintextBytes, + &file.ChunkSize, + &file.UploadStatus, + &file.CreatedAt, + &file.UpdatedAt, + ); err != nil { + return nil, fmt.Errorf("scan file: %w", err) + } + files = append(files, file) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate files: %w", err) + } + + return files, nil +} + +// CompleteChunks records uploaded chunks and advances per-file upload status. +func (r *PostgresRepository) CompleteChunks(ctx context.Context, transferID string, chunks []models.TransferChunk) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return fmt.Errorf("begin complete chunks: %w", err) + } + defer tx.Rollback(ctx) + + filesTouched := []string{} + for _, chunk := range chunks { + err = tx.Exec(ctx, ` + INSERT INTO transfer_chunks ( + transfer_id, opaque_file_id, chunk_index, object_key, ciphertext_size, checksum_sha256, uploaded_at + ) VALUES ($1, $2, $3, $4, $5, $6, now()) + ON CONFLICT (transfer_id, opaque_file_id, chunk_index) DO UPDATE + SET object_key = EXCLUDED.object_key, + ciphertext_size = EXCLUDED.ciphertext_size, + checksum_sha256 = EXCLUDED.checksum_sha256, + uploaded_at = now() + `, transferID, chunk.OpaqueFileID, chunk.ChunkIndex, chunk.ObjectKey, chunk.CiphertextSize, chunk.ChecksumSHA256) + if err != nil { + return fmt.Errorf("upsert chunk %s/%d: %w", chunk.OpaqueFileID, chunk.ChunkIndex, err) + } + + if !slices.Contains(filesTouched, chunk.OpaqueFileID) { + filesTouched = append(filesTouched, chunk.OpaqueFileID) + } + } + + for _, fileID := range filesTouched { + var totalChunks int + if err = tx.QueryRow(ctx, `SELECT total_chunks FROM transfer_files WHERE transfer_id = $1 AND opaque_file_id = $2`, transferID, fileID).Scan(&totalChunks); err != nil { + return fmt.Errorf("select file chunk count %s: %w", fileID, err) + } + + var uploadedCount int + if err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM transfer_chunks WHERE transfer_id = $1 AND opaque_file_id = $2`, transferID, fileID).Scan(&uploadedCount); err != nil { + return fmt.Errorf("count uploaded chunks %s: %w", fileID, err) + } + + status := "pending" + if uploadedCount >= totalChunks { + status = "complete" + } + + if err = tx.Exec(ctx, `UPDATE transfer_files SET upload_status = $3, updated_at = now() WHERE transfer_id = $1 AND opaque_file_id = $2`, transferID, fileID, status); err != nil { + return fmt.Errorf("update file status %s: %w", fileID, err) + } + } + + if err = tx.Exec(ctx, `UPDATE transfers SET updated_at = now() WHERE id = $1`, transferID); err != nil { + return fmt.Errorf("touch transfer: %w", err) + } + + if err = tx.Commit(ctx); err != nil { + return fmt.Errorf("commit complete chunks: %w", err) + } + + return nil +} + +// GetResumeState reconstructs the transfer, its files, and uploaded chunk indexes. +func (r *PostgresRepository) GetResumeState(ctx context.Context, transferID string) (models.TransferResumeState, error) { + transfer, err := r.GetTransfer(ctx, transferID) + if err != nil { + return models.TransferResumeState{}, err + } + + files, err := r.ListFiles(ctx, transferID) + if err != nil { + return models.TransferResumeState{}, err + } + + rows, err := r.db.Query(ctx, ` + SELECT opaque_file_id, chunk_index + FROM transfer_chunks + WHERE transfer_id = $1 + ORDER BY opaque_file_id, chunk_index + `, transferID) + if err != nil { + return models.TransferResumeState{}, fmt.Errorf("query chunks: %w", err) + } + defer rows.Close() + + uploaded := map[string][]int{} + for rows.Next() { + var fileID string + var chunkIndex int + if err := rows.Scan(&fileID, &chunkIndex); err != nil { + return models.TransferResumeState{}, fmt.Errorf("scan chunk: %w", err) + } + uploaded[fileID] = append(uploaded[fileID], chunkIndex) + } + if err := rows.Err(); err != nil { + return models.TransferResumeState{}, fmt.Errorf("iterate chunks: %w", err) + } + + return models.TransferResumeState{ + Transfer: transfer, + Files: files, + UploadedChunks: uploaded, + }, nil +} + +// SetManifest stores the manifest object location after the ciphertext upload succeeds. +func (r *PostgresRepository) SetManifest(ctx context.Context, transferID string, objectKey string, ciphertextSize int64) error { + taggedStatus := models.TransferStatusUploading + err := r.db.Exec(ctx, ` + UPDATE transfers + SET manifest_object_key = $2, manifest_ciphertext_size = $3, status = $4, updated_at = now() + WHERE id = $1 + `, transferID, objectKey, ciphertextSize, taggedStatus) + if err != nil { + return fmt.Errorf("set manifest: %w", err) + } + + return nil +} + +// FinalizeTransfer verifies completeness and promotes the transfer to ready state. +func (r *PostgresRepository) FinalizeTransfer(ctx context.Context, transferID string, wrappedRootKey string, totalFiles int, totalCiphertextBytes int64) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return fmt.Errorf("begin finalize: %w", err) + } + defer tx.Rollback(ctx) + + var manifestObjectKey string + if err = tx.QueryRow(ctx, `SELECT manifest_object_key FROM transfers WHERE id = $1`, transferID).Scan(&manifestObjectKey); err != nil { + return fmt.Errorf("select manifest object key: %w", err) + } + if manifestObjectKey == "" { + return fmt.Errorf("manifest not registered") + } + + var incompleteCount int + if err = tx.QueryRow(ctx, ` + SELECT COUNT(*) + FROM transfer_files + WHERE transfer_id = $1 AND upload_status <> 'complete' + `, transferID).Scan(&incompleteCount); err != nil { + return fmt.Errorf("count incomplete files: %w", err) + } + if incompleteCount > 0 { + return fmt.Errorf("upload incomplete") + } + + finalizedAt := time.Now().UTC() + err = tx.Exec(ctx, ` + UPDATE transfers + SET wrapped_root_key = $2, + total_files = $3, + total_ciphertext_bytes = $4, + status = $5, + finalized_at = $6, + updated_at = $6 + WHERE id = $1 + `, transferID, wrappedRootKey, totalFiles, totalCiphertextBytes, models.TransferStatusReady, finalizedAt) + if err != nil { + return fmt.Errorf("update finalized transfer: %w", err) + } + + if err = tx.Commit(ctx); err != nil { + return fmt.Errorf("commit finalize: %w", err) + } + + return nil +} + +// UpdateTransfer applies supported metadata changes without rewriting immutable fields. +func (r *PostgresRepository) UpdateTransfer(ctx context.Context, transferID string, params models.UpdateTransferParams) error { + if params.ManifestObjectKey != nil { + err := r.db.Exec(ctx, ` + UPDATE transfers + SET manifest_object_key = $2, updated_at = now() + WHERE id = $1 + `, transferID, *params.ManifestObjectKey) + if err != nil { + return fmt.Errorf("update manifest object key: %w", err) + } + } + + if params.ExpiresAt != nil { + err := r.db.Exec(ctx, `UPDATE transfers SET expires_at = $2, updated_at = now() WHERE id = $1`, transferID, *params.ExpiresAt) + if err != nil { + return fmt.Errorf("update expires_at: %w", err) + } + } + + if params.ManifestCiphertextSize != nil { + err := r.db.Exec(ctx, ` + UPDATE transfers + SET manifest_ciphertext_size = $2, updated_at = now() + WHERE id = $1 + `, transferID, *params.ManifestCiphertextSize) + if err != nil { + return fmt.Errorf("update manifest size: %w", err) + } + } + + return nil +} + +// MarkDeleted tombstones a transfer while retaining enough metadata for cleanup. +func (r *PostgresRepository) MarkDeleted(ctx context.Context, transferID string) error { + err := r.db.Exec(ctx, ` + UPDATE transfers + SET status = $2, deleted_at = now(), updated_at = now() + WHERE id = $1 + `, transferID, models.TransferStatusDeleted) + if err != nil { + return fmt.Errorf("mark deleted: %w", err) + } + + return nil +} + +// ListCleanupCandidates returns transfers whose remote objects should be purged. +func (r *PostgresRepository) ListCleanupCandidates(ctx context.Context, limit int) ([]models.Transfer, error) { + rows, err := r.db.Query(ctx, ` + SELECT + id, status, wrapped_root_key, manifest_object_key, manifest_ciphertext_size, + total_files, total_ciphertext_bytes, expires_at, created_at, updated_at, + finalized_at, manage_token_hash, deleted_at, purged_at + FROM transfers + WHERE purged_at IS NULL + AND ( + (status = 'deleted' AND deleted_at IS NOT NULL) + OR (status IN ('ready', 'uploading', 'draft', 'failed') AND expires_at <= now()) + OR status = 'expired' + ) + ORDER BY updated_at ASC + LIMIT $1 + `, limit) + if err != nil { + return nil, fmt.Errorf("query cleanup candidates: %w", err) + } + defer rows.Close() + + transfers := []models.Transfer{} + for rows.Next() { + var transfer models.Transfer + if err := rows.Scan( + &transfer.ID, + &transfer.Status, + &transfer.WrappedRootKey, + &transfer.ManifestObjectKey, + &transfer.ManifestCiphertextSize, + &transfer.TotalFiles, + &transfer.TotalCiphertextBytes, + &transfer.ExpiresAt, + &transfer.CreatedAt, + &transfer.UpdatedAt, + &transfer.FinalizedAt, + &transfer.ManageTokenHash, + &transfer.DeletedAt, + &transfer.PurgedAt, + ); err != nil { + return nil, fmt.Errorf("scan cleanup transfer: %w", err) + } + if transfer.Status != models.TransferStatusDeleted && transfer.ExpiresAt.Before(time.Now().UTC()) { + transfer.Status = models.TransferStatusExpired + } + transfers = append(transfers, transfer) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate cleanup transfers: %w", err) + } + + return transfers, nil +} + +// MarkPurged records that all remote objects for a transfer have been removed. +func (r *PostgresRepository) MarkPurged(ctx context.Context, transferID string) error { + err := r.db.Exec(ctx, ` + UPDATE transfers + SET status = $2, purged_at = now(), updated_at = now() + WHERE id = $1 + `, transferID, models.TransferStatusExpired) + if err != nil { + return fmt.Errorf("mark purged: %w", err) + } + + return nil +} diff --git a/apps/api/internal/repo/postgres_adapter_integration_test.go b/apps/api/internal/repo/postgres_adapter_integration_test.go new file mode 100644 index 0000000..2699738 --- /dev/null +++ b/apps/api/internal/repo/postgres_adapter_integration_test.go @@ -0,0 +1,56 @@ +package repo + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPGXPoolDBAndPGXTxAdapters(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + pool := startPostgresTestDB(t, ctx) + database := pgxPoolDB{pool: pool} + + require.NoError(t, database.Exec(ctx, `CREATE TEMP TABLE adapter_test (id text PRIMARY KEY)`)) + + rows, err := database.Query(ctx, `SELECT 'row-value'`) + require.NoError(t, err) + require.True(t, rows.Next()) + var rowValue string + require.NoError(t, rows.Scan(&rowValue)) + require.Equal(t, "row-value", rowValue) + rows.Close() + require.NoError(t, rows.Err()) + + var scalar string + require.NoError(t, database.QueryRow(ctx, `SELECT 'query-row-value'`).Scan(&scalar)) + require.Equal(t, "query-row-value", scalar) + + tx, err := database.Begin(ctx) + require.NoError(t, err) + require.NoError(t, tx.Exec(ctx, `INSERT INTO adapter_test (id) VALUES ($1)`, "committed")) + require.NoError(t, tx.QueryRow(ctx, `SELECT id FROM adapter_test WHERE id = $1`, "committed").Scan(&scalar)) + require.Equal(t, "committed", scalar) + require.NoError(t, tx.Commit(ctx)) + + tx, err = database.Begin(ctx) + require.NoError(t, err) + require.NoError(t, tx.Exec(ctx, `INSERT INTO adapter_test (id) VALUES ($1)`, "rolled-back")) + require.NoError(t, tx.Rollback(ctx)) + + rows, err = database.Query(ctx, `SELECT id FROM adapter_test ORDER BY id`) + require.NoError(t, err) + defer rows.Close() + + values := []string{} + for rows.Next() { + var id string + require.NoError(t, rows.Scan(&id)) + values = append(values, id) + } + require.NoError(t, rows.Err()) + require.Equal(t, []string{"committed"}, values) +} diff --git a/apps/api/internal/repo/postgres_core_unit_test.go b/apps/api/internal/repo/postgres_core_unit_test.go new file mode 100644 index 0000000..efd65fa --- /dev/null +++ b/apps/api/internal/repo/postgres_core_unit_test.go @@ -0,0 +1,526 @@ +package repo + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/models" +) + +func TestPostgresRepositoryTransferAccessors(t *testing.T) { + t.Parallel() + + ctx := context.Background() + now := time.Now().UTC().Truncate(time.Second) + transfer := createTestTransfer("transfer-unit", models.TransferStatusReady, now.Add(time.Hour)) + transfer.WrappedRootKey = `{"key":1}` + transfer.ManifestObjectKey = "transfers/unit/manifest.bin" + transfer.ManifestCiphertextSize = 88 + transfer.TotalFiles = 2 + transfer.TotalCiphertextBytes = 123 + transfer.FinalizedAt = &now + + t.Run("create transfer success and error", func(t *testing.T) { + execCalls := 0 + repository := PostgresRepository{db: stubPostgresDB{ + execFn: func(_ context.Context, _ string, _ ...any) error { + execCalls++ + if execCalls == 2 { + return errors.New("boom") + } + return nil + }, + }} + + require.NoError(t, repository.CreateTransfer(ctx, transfer)) + err := repository.CreateTransfer(ctx, transfer) + require.ErrorContains(t, err, "insert transfer") + }) + + t.Run("get transfer handles not found, query errors, and success", func(t *testing.T) { + call := 0 + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + call++ + switch call { + case 1: + return stubRow{err: pgx.ErrNoRows} + case 2: + return stubRow{err: errors.New("boom")} + default: + return stubRow{values: transferScanValues(transfer)} + } + }, + }} + + _, err := repository.GetTransfer(ctx, transfer.ID) + require.ErrorIs(t, err, ErrNotFound) + + _, err = repository.GetTransfer(ctx, transfer.ID) + require.ErrorContains(t, err, "select transfer") + + stored, err := repository.GetTransfer(ctx, transfer.ID) + require.NoError(t, err) + require.Equal(t, transfer.ID, stored.ID) + require.Equal(t, transfer.ManifestObjectKey, stored.ManifestObjectKey) + require.Equal(t, transfer.TotalCiphertextBytes, stored.TotalCiphertextBytes) + require.NotNil(t, stored.FinalizedAt) + }) +} + +func TestPostgresRepositoryRegisterFiles(t *testing.T) { + t.Parallel() + + ctx := context.Background() + files := []models.TransferFile{ + {OpaqueFileID: "file-a", TotalChunks: 2, CiphertextBytes: 64, PlaintextBytes: int64ptr(32), ChunkSize: 32}, + {OpaqueFileID: "file-b", TotalChunks: 1, CiphertextBytes: 16, PlaintextBytes: int64ptr(8), ChunkSize: 16}, + } + + t.Run("success commits after inserting files", func(t *testing.T) { + execCalls := 0 + tx := &stubPostgresTx{ + execFn: func(context.Context, string, ...any) error { + execCalls++ + return nil + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + require.NoError(t, repository.RegisterFiles(ctx, "transfer-1", files)) + require.Equal(t, 3, execCalls) + require.True(t, tx.commitCalled) + }) + + t.Run("begin failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return nil, errors.New("boom") + }, + }} + + err := repository.RegisterFiles(ctx, "transfer-1", files) + require.ErrorContains(t, err, "begin register files") + }) + + t.Run("file insert failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, args ...any) error { + if strings.Contains(sql, "INSERT INTO transfer_files") && args[1] == "file-a" { + return errors.New("boom") + } + return nil + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.RegisterFiles(ctx, "transfer-1", files) + require.ErrorContains(t, err, "insert file file-a") + }) + + t.Run("status update failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "UPDATE transfers SET status") { + return errors.New("boom") + } + return nil + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.RegisterFiles(ctx, "transfer-1", files) + require.ErrorContains(t, err, "set uploading status") + }) + + t.Run("commit failure", func(t *testing.T) { + tx := &stubPostgresTx{commitErr: errors.New("boom")} + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.RegisterFiles(ctx, "transfer-1", files) + require.ErrorContains(t, err, "commit register files") + }) +} + +func TestPostgresRepositoryListFiles(t *testing.T) { + t.Parallel() + + ctx := context.Background() + file := models.TransferFile{ + TransferID: "transfer-1", + OpaqueFileID: "file-a", + TotalChunks: 2, + CiphertextBytes: 64, + PlaintextBytes: int64ptr(32), + ChunkSize: 32, + UploadStatus: "pending", + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + + t.Run("query failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return nil, errors.New("boom") + }, + }} + + _, err := repository.ListFiles(ctx, "transfer-1") + require.ErrorContains(t, err, "query files") + }) + + t.Run("scan failure", func(t *testing.T) { + rows := &stubRows{values: [][]any{fileScanValues(file)}, scanErrAt: 1, scanErr: errors.New("boom")} + repository := PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return rows, nil + }, + }} + + _, err := repository.ListFiles(ctx, "transfer-1") + require.ErrorContains(t, err, "scan file") + require.True(t, rows.closed) + }) + + t.Run("iteration failure", func(t *testing.T) { + rows := &stubRows{values: [][]any{fileScanValues(file)}, err: errors.New("boom")} + repository := PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return rows, nil + }, + }} + + _, err := repository.ListFiles(ctx, "transfer-1") + require.ErrorContains(t, err, "iterate files") + }) + + t.Run("success", func(t *testing.T) { + rows := &stubRows{values: [][]any{fileScanValues(file)}} + repository := PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return rows, nil + }, + }} + + files, err := repository.ListFiles(ctx, "transfer-1") + require.NoError(t, err) + require.Len(t, files, 1) + require.Equal(t, "file-a", files[0].OpaqueFileID) + }) +} + +func TestPostgresRepositoryCompleteChunks(t *testing.T) { + t.Parallel() + + ctx := context.Background() + chunks := []models.TransferChunk{ + {OpaqueFileID: "file-a", ChunkIndex: 0, ObjectKey: "chunk-0", CiphertextSize: 10, ChecksumSHA256: "sum-0"}, + {OpaqueFileID: "file-a", ChunkIndex: 1, ObjectKey: "chunk-1", CiphertextSize: 10, ChecksumSHA256: "sum-1"}, + } + + t.Run("success marks completed files once", func(t *testing.T) { + totalQueries := 0 + countQueries := 0 + statusUpdates := 0 + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "UPDATE transfer_files SET upload_status") { + statusUpdates++ + } + return nil + }, + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + switch { + case strings.Contains(sql, "SELECT total_chunks"): + totalQueries++ + return stubRow{values: []any{2}} + case strings.Contains(sql, "SELECT COUNT(*) FROM transfer_chunks"): + countQueries++ + return stubRow{values: []any{2}} + default: + return stubRow{err: fmt.Errorf("unexpected query: %s", sql)} + } + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + require.NoError(t, repository.CompleteChunks(ctx, "transfer-1", chunks)) + require.Equal(t, 1, totalQueries) + require.Equal(t, 1, countQueries) + require.Equal(t, 1, statusUpdates) + require.True(t, tx.commitCalled) + }) + + t.Run("begin failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return nil, errors.New("boom") + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks) + require.ErrorContains(t, err, "begin complete chunks") + }) + + t.Run("chunk upsert failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "INSERT INTO transfer_chunks") { + return errors.New("boom") + } + return nil + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks) + require.ErrorContains(t, err, "upsert chunk file-a/0") + }) + + t.Run("select total chunks failure", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT total_chunks") { + return stubRow{err: errors.New("boom")} + } + return stubRow{values: []any{1}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks[:1]) + require.ErrorContains(t, err, "select file chunk count file-a") + }) + + t.Run("count uploaded chunks failure", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT COUNT(*) FROM transfer_chunks") { + return stubRow{err: errors.New("boom")} + } + return stubRow{values: []any{1}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks[:1]) + require.ErrorContains(t, err, "count uploaded chunks file-a") + }) + + t.Run("update file status failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "UPDATE transfer_files SET upload_status") { + return errors.New("boom") + } + return nil + }, + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{1}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks[:1]) + require.ErrorContains(t, err, "update file status file-a") + }) + + t.Run("touch transfer failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "UPDATE transfers SET updated_at") { + return errors.New("boom") + } + return nil + }, + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{1}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks[:1]) + require.ErrorContains(t, err, "touch transfer") + }) + + t.Run("commit failure", func(t *testing.T) { + tx := &stubPostgresTx{ + commitErr: errors.New("boom"), + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{1}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.CompleteChunks(ctx, "transfer-1", chunks[:1]) + require.ErrorContains(t, err, "commit complete chunks") + }) +} + +func TestPostgresRepositoryGetResumeState(t *testing.T) { + t.Parallel() + + ctx := context.Background() + now := time.Now().UTC().Truncate(time.Second) + transfer := createTestTransfer("transfer-resume", models.TransferStatusUploading, now.Add(time.Hour)) + file := models.TransferFile{ + TransferID: transfer.ID, + OpaqueFileID: "file-a", + TotalChunks: 2, + CiphertextBytes: 64, + PlaintextBytes: int64ptr(32), + ChunkSize: 32, + UploadStatus: "pending", + CreatedAt: now, + UpdatedAt: now, + } + + t.Run("transfer lookup failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{err: pgx.ErrNoRows} + }, + }} + + _, err := repository.GetResumeState(ctx, transfer.ID) + require.ErrorIs(t, err, ErrNotFound) + }) + + t.Run("list files failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: transferScanValues(transfer)} + }, + queryFn: func(_ context.Context, sql string, _ ...any) (postgresRows, error) { + if strings.Contains(sql, "FROM transfer_files") { + return nil, errors.New("boom") + } + return &stubRows{}, nil + }, + }} + + _, err := repository.GetResumeState(ctx, transfer.ID) + require.ErrorContains(t, err, "query files") + }) + + t.Run("query chunks failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: transferScanValues(transfer)} + }, + queryFn: func(_ context.Context, sql string, _ ...any) (postgresRows, error) { + if strings.Contains(sql, "FROM transfer_files") { + return &stubRows{values: [][]any{fileScanValues(file)}}, nil + } + return nil, errors.New("boom") + }, + }} + + _, err := repository.GetResumeState(ctx, transfer.ID) + require.ErrorContains(t, err, "query chunks") + }) + + t.Run("scan chunk failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: transferScanValues(transfer)} + }, + queryFn: func(_ context.Context, sql string, _ ...any) (postgresRows, error) { + if strings.Contains(sql, "FROM transfer_files") { + return &stubRows{values: [][]any{fileScanValues(file)}}, nil + } + return &stubRows{values: [][]any{{"file-a", 0}}, scanErrAt: 1, scanErr: errors.New("boom")}, nil + }, + }} + + _, err := repository.GetResumeState(ctx, transfer.ID) + require.ErrorContains(t, err, "scan chunk") + }) + + t.Run("iterate chunks failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: transferScanValues(transfer)} + }, + queryFn: func(_ context.Context, sql string, _ ...any) (postgresRows, error) { + if strings.Contains(sql, "FROM transfer_files") { + return &stubRows{values: [][]any{fileScanValues(file)}}, nil + } + return &stubRows{values: [][]any{{"file-a", 0}}, err: errors.New("boom")}, nil + }, + }} + + _, err := repository.GetResumeState(ctx, transfer.ID) + require.ErrorContains(t, err, "iterate chunks") + }) + + t.Run("success", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: transferScanValues(transfer)} + }, + queryFn: func(_ context.Context, sql string, _ ...any) (postgresRows, error) { + if strings.Contains(sql, "FROM transfer_files") { + return &stubRows{values: [][]any{fileScanValues(file)}}, nil + } + return &stubRows{values: [][]any{{"file-a", 0}, {"file-a", 1}}}, nil + }, + }} + + state, err := repository.GetResumeState(ctx, transfer.ID) + require.NoError(t, err) + require.Equal(t, transfer.ID, state.Transfer.ID) + require.Len(t, state.Files, 1) + require.Equal(t, []int{0, 1}, state.UploadedChunks["file-a"]) + }) +} diff --git a/apps/api/internal/repo/postgres_integration_test.go b/apps/api/internal/repo/postgres_integration_test.go new file mode 100644 index 0000000..3a43d2d --- /dev/null +++ b/apps/api/internal/repo/postgres_integration_test.go @@ -0,0 +1,272 @@ +package repo + +import ( + "context" + "os/exec" + "runtime" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/xdrop/monorepo/internal/models" +) + +func TestRunMigrationsAndRepositoryLifecycle(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + db := startPostgresTestDB(t, ctx) + require.NoError(t, RunMigrations(ctx, db)) + require.NoError(t, RunMigrations(ctx, db)) + + repository := NewPostgresRepository(db) + + _, err := repository.GetTransfer(ctx, "missing-transfer") + require.ErrorIs(t, err, ErrNotFound) + + now := time.Now().UTC().Truncate(time.Second) + transfer := models.Transfer{ + ID: "transfer-1", + Status: models.TransferStatusDraft, + ExpiresAt: now.Add(time.Hour), + CreatedAt: now, + UpdatedAt: now, + ManageTokenHash: "hash-1", + } + require.NoError(t, repository.CreateTransfer(ctx, transfer)) + + files := []models.TransferFile{ + { + TransferID: transfer.ID, + OpaqueFileID: "file-a", + TotalChunks: 2, + CiphertextBytes: 64, + PlaintextBytes: int64ptr(32), + ChunkSize: 32, + }, + { + TransferID: transfer.ID, + OpaqueFileID: "file-b", + TotalChunks: 1, + CiphertextBytes: 48, + PlaintextBytes: int64ptr(24), + ChunkSize: 48, + }, + } + require.NoError(t, repository.RegisterFiles(ctx, transfer.ID, files)) + + stored, err := repository.GetTransfer(ctx, transfer.ID) + require.NoError(t, err) + require.Equal(t, models.TransferStatusUploading, stored.Status) + + listedFiles, err := repository.ListFiles(ctx, transfer.ID) + require.NoError(t, err) + require.Len(t, listedFiles, 2) + require.Equal(t, "file-a", listedFiles[0].OpaqueFileID) + require.Equal(t, "pending", listedFiles[0].UploadStatus) + require.Equal(t, "file-b", listedFiles[1].OpaqueFileID) + + require.NoError(t, repository.CompleteChunks(ctx, transfer.ID, []models.TransferChunk{ + { + TransferID: transfer.ID, + OpaqueFileID: "file-a", + ChunkIndex: 0, + ObjectKey: "transfers/transfer-1/files/file-a/chunks/00000000.bin", + CiphertextSize: 32, + ChecksumSHA256: "checksum-a0", + }, + { + TransferID: transfer.ID, + OpaqueFileID: "file-a", + ChunkIndex: 0, + ObjectKey: "transfers/transfer-1/files/file-a/chunks/00000000.bin", + CiphertextSize: 32, + ChecksumSHA256: "checksum-a0-updated", + }, + { + TransferID: transfer.ID, + OpaqueFileID: "file-b", + ChunkIndex: 0, + ObjectKey: "transfers/transfer-1/files/file-b/chunks/00000000.bin", + CiphertextSize: 48, + ChecksumSHA256: "checksum-b0", + }, + })) + + resume, err := repository.GetResumeState(ctx, transfer.ID) + require.NoError(t, err) + require.Equal(t, []int{0}, resume.UploadedChunks["file-a"]) + require.Equal(t, []int{0}, resume.UploadedChunks["file-b"]) + + listedFiles, err = repository.ListFiles(ctx, transfer.ID) + require.NoError(t, err) + require.Equal(t, "pending", listedFiles[0].UploadStatus) + require.Equal(t, "complete", listedFiles[1].UploadStatus) + + require.NoError(t, repository.CompleteChunks(ctx, transfer.ID, []models.TransferChunk{ + { + TransferID: transfer.ID, + OpaqueFileID: "file-a", + ChunkIndex: 1, + ObjectKey: "transfers/transfer-1/files/file-a/chunks/00000001.bin", + CiphertextSize: 32, + ChecksumSHA256: "checksum-a1", + }, + })) + require.NoError(t, repository.SetManifest(ctx, transfer.ID, "transfers/transfer-1/manifest.bin", 77)) + require.NoError(t, repository.FinalizeTransfer(ctx, transfer.ID, `{"version":1}`, 2, 112)) + + finalized, err := repository.GetTransfer(ctx, transfer.ID) + require.NoError(t, err) + require.Equal(t, models.TransferStatusReady, finalized.Status) + require.Equal(t, "transfers/transfer-1/manifest.bin", finalized.ManifestObjectKey) + require.Equal(t, `{"version":1}`, finalized.WrappedRootKey) + require.Equal(t, 2, finalized.TotalFiles) + require.Equal(t, int64(112), finalized.TotalCiphertextBytes) + require.NotNil(t, finalized.FinalizedAt) + + newExpiry := now.Add(3 * time.Hour) + newManifestKey := "transfers/transfer-1/manifest-renamed.bin" + newManifestSize := int64(99) + require.NoError(t, repository.UpdateTransfer(ctx, transfer.ID, models.UpdateTransferParams{ + ExpiresAt: &newExpiry, + ManifestObjectKey: &newManifestKey, + ManifestCiphertextSize: &newManifestSize, + })) + + updated, err := repository.GetTransfer(ctx, transfer.ID) + require.NoError(t, err) + require.WithinDuration(t, newExpiry, updated.ExpiresAt, time.Second) + require.Equal(t, newManifestKey, updated.ManifestObjectKey) + require.Equal(t, newManifestSize, updated.ManifestCiphertextSize) +} + +func TestPostgresRepositoryFinalizeTransferValidatesManifestAndUploads(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + db := startPostgresTestDB(t, ctx) + require.NoError(t, RunMigrations(ctx, db)) + + repository := NewPostgresRepository(db) + transfer := createTestTransfer("transfer-guard", models.TransferStatusDraft, time.Now().UTC().Add(time.Hour)) + require.NoError(t, repository.CreateTransfer(ctx, transfer)) + require.NoError(t, repository.RegisterFiles(ctx, transfer.ID, []models.TransferFile{{ + TransferID: transfer.ID, + OpaqueFileID: "file-a", + TotalChunks: 1, + CiphertextBytes: 16, + ChunkSize: 16, + }})) + + err := repository.FinalizeTransfer(ctx, transfer.ID, `{"version":1}`, 1, 16) + require.ErrorContains(t, err, "manifest not registered") + + require.NoError(t, repository.SetManifest(ctx, transfer.ID, "transfers/transfer-guard/manifest.bin", 32)) + err = repository.FinalizeTransfer(ctx, transfer.ID, `{"version":1}`, 1, 16) + require.ErrorContains(t, err, "upload incomplete") +} + +func TestPostgresRepositoryCleanupCandidatesAndPurging(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + db := startPostgresTestDB(t, ctx) + require.NoError(t, RunMigrations(ctx, db)) + + repository := NewPostgresRepository(db) + now := time.Now().UTC() + + expiredReady := createTestTransfer("expired-ready", models.TransferStatusReady, now.Add(-time.Hour)) + deletedTransfer := createTestTransfer("deleted-transfer", models.TransferStatusReady, now.Add(time.Hour)) + futureTransfer := createTestTransfer("future-transfer", models.TransferStatusUploading, now.Add(time.Hour)) + purgedTransfer := createTestTransfer("purged-transfer", models.TransferStatusReady, now.Add(-2*time.Hour)) + + for _, transfer := range []models.Transfer{expiredReady, deletedTransfer, futureTransfer, purgedTransfer} { + require.NoError(t, repository.CreateTransfer(ctx, transfer)) + } + require.NoError(t, repository.MarkDeleted(ctx, deletedTransfer.ID)) + require.NoError(t, repository.MarkPurged(ctx, purgedTransfer.ID)) + + candidates, err := repository.ListCleanupCandidates(ctx, 10) + require.NoError(t, err) + + candidateByID := map[string]models.Transfer{} + for _, candidate := range candidates { + candidateByID[candidate.ID] = candidate + } + + require.Contains(t, candidateByID, expiredReady.ID) + require.Contains(t, candidateByID, deletedTransfer.ID) + require.NotContains(t, candidateByID, futureTransfer.ID) + require.NotContains(t, candidateByID, purgedTransfer.ID) + require.Equal(t, models.TransferStatusExpired, candidateByID[expiredReady.ID].Status) + require.Equal(t, models.TransferStatusDeleted, candidateByID[deletedTransfer.ID].Status) + + require.NoError(t, repository.MarkPurged(ctx, expiredReady.ID)) + purged, err := repository.GetTransfer(ctx, expiredReady.ID) + require.NoError(t, err) + require.Equal(t, models.TransferStatusExpired, purged.Status) + require.NotNil(t, purged.PurgedAt) +} + +func startPostgresTestDB(t *testing.T, ctx context.Context) *pgxpool.Pool { + t.Helper() + + container, err := tcpostgres.Run( + ctx, + "postgres:16-alpine", + tcpostgres.WithDatabase("xdrop"), + tcpostgres.WithUsername("xdrop"), + tcpostgres.WithPassword("xdrop"), + tcpostgres.BasicWaitStrategies(), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, testcontainers.TerminateContainer(container)) + }) + + connectionString, err := container.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := pgxpool.New(ctx, connectionString) + require.NoError(t, err) + require.NoError(t, db.Ping(ctx)) + t.Cleanup(db.Close) + + return db +} + +func createTestTransfer(id string, status models.TransferStatus, expiresAt time.Time) models.Transfer { + now := time.Now().UTC().Truncate(time.Second) + return models.Transfer{ + ID: id, + Status: status, + ExpiresAt: expiresAt, + CreatedAt: now, + UpdatedAt: now, + ManageTokenHash: "manage-token-hash", + } +} + +func int64ptr(value int64) *int64 { + return &value +} + +func skipIfDockerUnavailable(t *testing.T) { + t.Helper() + + if testing.Short() { + t.Skip("skipping docker-backed integration test in short mode") + } + if runtime.GOOS == "windows" { + t.Skip("skipping docker-backed integration test on windows") + } + + if err := exec.Command("docker", "info").Run(); err != nil { + t.Skipf("skipping docker-backed integration test: %v", err) + } +} diff --git a/apps/api/internal/repo/postgres_mutation_unit_test.go b/apps/api/internal/repo/postgres_mutation_unit_test.go new file mode 100644 index 0000000..5d55fff --- /dev/null +++ b/apps/api/internal/repo/postgres_mutation_unit_test.go @@ -0,0 +1,321 @@ +package repo + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/models" +) + +func TestPostgresRepositoryFinalizeUpdateAndCleanup(t *testing.T) { + t.Parallel() + + ctx := context.Background() + now := time.Now().UTC().Truncate(time.Second) + + t.Run("set manifest success and error", func(t *testing.T) { + execCalls := 0 + repository := PostgresRepository{db: stubPostgresDB{ + execFn: func(context.Context, string, ...any) error { + execCalls++ + if execCalls == 2 { + return errors.New("boom") + } + return nil + }, + }} + + require.NoError(t, repository.SetManifest(ctx, "transfer-1", "manifest.bin", 88)) + err := repository.SetManifest(ctx, "transfer-1", "manifest.bin", 88) + require.ErrorContains(t, err, "set manifest") + }) + + t.Run("finalize variants", func(t *testing.T) { + t.Run("begin failure", func(t *testing.T) { + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return nil, errors.New("boom") + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "begin finalize") + }) + + t.Run("manifest query failure", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{err: errors.New("boom")} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "select manifest object key") + }) + + t.Run("requires manifest", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(context.Context, string, ...any) postgresRow { + return stubRow{values: []any{""}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "manifest not registered") + }) + + t.Run("incomplete count failure", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT COUNT(*)") { + return stubRow{err: errors.New("boom")} + } + return stubRow{values: []any{"manifest.bin"}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "count incomplete files") + }) + + t.Run("rejects incomplete uploads", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT COUNT(*)") { + return stubRow{values: []any{1}} + } + return stubRow{values: []any{"manifest.bin"}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "upload incomplete") + }) + + t.Run("update failure", func(t *testing.T) { + tx := &stubPostgresTx{ + execFn: func(context.Context, string, ...any) error { + return errors.New("boom") + }, + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT COUNT(*)") { + return stubRow{values: []any{0}} + } + return stubRow{values: []any{"manifest.bin"}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "update finalized transfer") + }) + + t.Run("commit failure", func(t *testing.T) { + tx := &stubPostgresTx{ + commitErr: errors.New("boom"), + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT COUNT(*)") { + return stubRow{values: []any{0}} + } + return stubRow{values: []any{"manifest.bin"}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + err := repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10) + require.ErrorContains(t, err, "commit finalize") + }) + + t.Run("success", func(t *testing.T) { + tx := &stubPostgresTx{ + queryRowFn: func(_ context.Context, sql string, _ ...any) postgresRow { + if strings.Contains(sql, "SELECT COUNT(*)") { + return stubRow{values: []any{0}} + } + return stubRow{values: []any{"manifest.bin"}} + }, + } + repository := PostgresRepository{db: stubPostgresDB{ + beginFn: func(context.Context) (postgresTx, error) { + return tx, nil + }, + }} + + require.NoError(t, repository.FinalizeTransfer(ctx, "transfer-1", "root", 1, 10)) + require.True(t, tx.commitCalled) + }) + }) + + t.Run("update transfer covers each field and errors", func(t *testing.T) { + manifestKey := "manifest-renamed.bin" + expiresAt := now.Add(2 * time.Hour) + manifestSize := int64(101) + repository := PostgresRepository{db: stubPostgresDB{ + execFn: func(_ context.Context, _ string, _ ...any) error { + return nil + }, + }} + + require.NoError(t, repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{ + ManifestObjectKey: &manifestKey, + ManifestCiphertextSize: &manifestSize, + })) + + err := repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{ + ExpiresAt: &expiresAt, + }) + require.NoError(t, err) + + err = repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{ + ManifestCiphertextSize: &manifestSize, + }) + require.NoError(t, err) + + repository = PostgresRepository{db: stubPostgresDB{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "manifest_object_key") { + return errors.New("boom") + } + return nil + }, + }} + + err = repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{ + ManifestObjectKey: &manifestKey, + }) + require.ErrorContains(t, err, "update manifest object key") + + repository = PostgresRepository{db: stubPostgresDB{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "expires_at") { + return errors.New("boom") + } + return nil + }, + }} + + err = repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{ + ExpiresAt: &expiresAt, + }) + require.ErrorContains(t, err, "update expires_at") + + repository = PostgresRepository{db: stubPostgresDB{ + execFn: func(_ context.Context, sql string, _ ...any) error { + if strings.Contains(sql, "manifest_ciphertext_size") { + return errors.New("boom") + } + return nil + }, + }} + + err = repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{ + ManifestCiphertextSize: &manifestSize, + }) + require.ErrorContains(t, err, "update manifest size") + + noOpCalls := 0 + repository = PostgresRepository{db: stubPostgresDB{ + execFn: func(context.Context, string, ...any) error { + noOpCalls++ + return nil + }, + }} + require.NoError(t, repository.UpdateTransfer(ctx, "transfer-1", models.UpdateTransferParams{})) + require.Zero(t, noOpCalls) + }) + + t.Run("mark deleted and purged success and error", func(t *testing.T) { + execCalls := 0 + repository := PostgresRepository{db: stubPostgresDB{ + execFn: func(context.Context, string, ...any) error { + execCalls++ + if execCalls == 2 || execCalls == 4 { + return errors.New("boom") + } + return nil + }, + }} + + require.NoError(t, repository.MarkDeleted(ctx, "transfer-1")) + err := repository.MarkDeleted(ctx, "transfer-1") + require.ErrorContains(t, err, "mark deleted") + + require.NoError(t, repository.MarkPurged(ctx, "transfer-1")) + err = repository.MarkPurged(ctx, "transfer-1") + require.ErrorContains(t, err, "mark purged") + }) + + t.Run("list cleanup candidates handles query, scan, iteration, and success", func(t *testing.T) { + expired := createTestTransfer("expired", models.TransferStatusReady, now.Add(-time.Hour)) + deleted := createTestTransfer("deleted", models.TransferStatusDeleted, now.Add(time.Hour)) + deletedAt := now.Add(-30 * time.Minute) + deleted.DeletedAt = &deletedAt + + repository := PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return nil, errors.New("boom") + }, + }} + _, err := repository.ListCleanupCandidates(ctx, 10) + require.ErrorContains(t, err, "query cleanup candidates") + + repository = PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return &stubRows{values: [][]any{transferScanValues(expired)}, scanErrAt: 1, scanErr: errors.New("boom")}, nil + }, + }} + _, err = repository.ListCleanupCandidates(ctx, 10) + require.ErrorContains(t, err, "scan cleanup transfer") + + repository = PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return &stubRows{values: [][]any{transferScanValues(expired)}, err: errors.New("boom")}, nil + }, + }} + _, err = repository.ListCleanupCandidates(ctx, 10) + require.ErrorContains(t, err, "iterate cleanup transfers") + + repository = PostgresRepository{db: stubPostgresDB{ + queryFn: func(context.Context, string, ...any) (postgresRows, error) { + return &stubRows{values: [][]any{transferScanValues(expired), transferScanValues(deleted)}}, nil + }, + }} + candidates, err := repository.ListCleanupCandidates(ctx, 10) + require.NoError(t, err) + require.Len(t, candidates, 2) + require.Equal(t, models.TransferStatusExpired, candidates[0].Status) + require.Equal(t, models.TransferStatusDeleted, candidates[1].Status) + }) +} diff --git a/apps/api/internal/repo/postgres_wrapper_test.go b/apps/api/internal/repo/postgres_wrapper_test.go new file mode 100644 index 0000000..de0fbb3 --- /dev/null +++ b/apps/api/internal/repo/postgres_wrapper_test.go @@ -0,0 +1,17 @@ +package repo + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewPostgresRepositoryWrapsPoolBackedDB(t *testing.T) { + t.Parallel() + + repository := NewPostgresRepository(nil) + wrapped, ok := repository.db.(pgxPoolDB) + + require.True(t, ok) + require.Nil(t, wrapped.pool) +} diff --git a/apps/api/internal/repo/repository.go b/apps/api/internal/repo/repository.go new file mode 100644 index 0000000..99a9fff --- /dev/null +++ b/apps/api/internal/repo/repository.go @@ -0,0 +1,23 @@ +package repo + +import ( + "context" + + "github.com/xdrop/monorepo/internal/models" +) + +// Repository defines the persistence contract for transfer lifecycle operations. +type Repository interface { + CreateTransfer(ctx context.Context, transfer models.Transfer) error + GetTransfer(ctx context.Context, transferID string) (models.Transfer, error) + RegisterFiles(ctx context.Context, transferID string, files []models.TransferFile) error + ListFiles(ctx context.Context, transferID string) ([]models.TransferFile, error) + CompleteChunks(ctx context.Context, transferID string, chunks []models.TransferChunk) error + GetResumeState(ctx context.Context, transferID string) (models.TransferResumeState, error) + SetManifest(ctx context.Context, transferID string, objectKey string, ciphertextSize int64) error + FinalizeTransfer(ctx context.Context, transferID string, wrappedRootKey string, totalFiles int, totalCiphertextBytes int64) error + UpdateTransfer(ctx context.Context, transferID string, params models.UpdateTransferParams) error + MarkDeleted(ctx context.Context, transferID string) error + ListCleanupCandidates(ctx context.Context, limit int) ([]models.Transfer, error) + MarkPurged(ctx context.Context, transferID string) error +} diff --git a/apps/api/internal/repo/test_helpers_test.go b/apps/api/internal/repo/test_helpers_test.go new file mode 100644 index 0000000..d193c2f --- /dev/null +++ b/apps/api/internal/repo/test_helpers_test.go @@ -0,0 +1,205 @@ +package repo + +import ( + "context" + "fmt" + "io/fs" + "reflect" + + "github.com/xdrop/monorepo/internal/models" +) + +type stubMigrationFiles struct { + readDirFn func(name string) ([]fs.DirEntry, error) + readFileFn func(name string) ([]byte, error) +} + +func (f stubMigrationFiles) ReadDir(name string) ([]fs.DirEntry, error) { + if f.readDirFn != nil { + return f.readDirFn(name) + } + return nil, nil +} + +func (f stubMigrationFiles) ReadFile(name string) ([]byte, error) { + if f.readFileFn != nil { + return f.readFileFn(name) + } + return nil, nil +} + +type stubPostgresDB struct { + beginFn func(ctx context.Context) (postgresTx, error) + execFn func(ctx context.Context, sql string, args ...any) error + queryFn func(ctx context.Context, sql string, args ...any) (postgresRows, error) + queryRowFn func(ctx context.Context, sql string, args ...any) postgresRow +} + +func (db stubPostgresDB) Begin(ctx context.Context) (postgresTx, error) { + if db.beginFn != nil { + return db.beginFn(ctx) + } + return &stubPostgresTx{}, nil +} + +func (db stubPostgresDB) Exec(ctx context.Context, sql string, args ...any) error { + if db.execFn != nil { + return db.execFn(ctx, sql, args...) + } + return nil +} + +func (db stubPostgresDB) Query(ctx context.Context, sql string, args ...any) (postgresRows, error) { + if db.queryFn != nil { + return db.queryFn(ctx, sql, args...) + } + return &stubRows{}, nil +} + +func (db stubPostgresDB) QueryRow(ctx context.Context, sql string, args ...any) postgresRow { + if db.queryRowFn != nil { + return db.queryRowFn(ctx, sql, args...) + } + return stubRow{} +} + +type stubPostgresTx struct { + commitCalled bool + commitErr error + execFn func(ctx context.Context, sql string, args ...any) error + queryRowFn func(ctx context.Context, sql string, args ...any) postgresRow + rollbackCalled bool + rollbackErr error +} + +func (tx *stubPostgresTx) Commit(context.Context) error { + tx.commitCalled = true + return tx.commitErr +} + +func (tx *stubPostgresTx) Exec(ctx context.Context, sql string, args ...any) error { + if tx.execFn != nil { + return tx.execFn(ctx, sql, args...) + } + return nil +} + +func (tx *stubPostgresTx) QueryRow(ctx context.Context, sql string, args ...any) postgresRow { + if tx.queryRowFn != nil { + return tx.queryRowFn(ctx, sql, args...) + } + return stubRow{} +} + +func (tx *stubPostgresTx) Rollback(context.Context) error { + tx.rollbackCalled = true + return tx.rollbackErr +} + +type stubRow struct { + err error + values []any +} + +func (r stubRow) Scan(dest ...any) error { + if r.err != nil { + return r.err + } + return assignScanValues(dest, r.values) +} + +type stubRows struct { + closed bool + err error + index int + scanErr error + scanErrAt int + values [][]any +} + +func (r *stubRows) Close() { + r.closed = true +} + +func (r *stubRows) Err() error { + return r.err +} + +func (r *stubRows) Next() bool { + if r.index >= len(r.values) { + return false + } + r.index++ + return true +} + +func (r *stubRows) Scan(dest ...any) error { + if r.scanErr != nil && r.index == r.scanErrAt { + return r.scanErr + } + return assignScanValues(dest, r.values[r.index-1]) +} + +func assignScanValues(dest []any, values []any) error { + if len(dest) != len(values) { + return fmt.Errorf("scan value count mismatch: %d != %d", len(dest), len(values)) + } + + for i := range dest { + target := reflect.ValueOf(dest[i]) + if !target.IsValid() || target.Kind() != reflect.Pointer || target.IsNil() { + return fmt.Errorf("scan target %d is not a pointer", i) + } + + elem := target.Elem() + if values[i] == nil { + elem.Set(reflect.Zero(elem.Type())) + continue + } + + value := reflect.ValueOf(values[i]) + switch { + case value.Type().AssignableTo(elem.Type()): + elem.Set(value) + case value.Type().ConvertibleTo(elem.Type()): + elem.Set(value.Convert(elem.Type())) + default: + return fmt.Errorf("cannot assign %s to %s", value.Type(), elem.Type()) + } + } + + return nil +} + +func transferScanValues(transfer models.Transfer) []any { + return []any{ + transfer.ID, + transfer.Status, + transfer.WrappedRootKey, + transfer.ManifestObjectKey, + transfer.ManifestCiphertextSize, + transfer.TotalFiles, + transfer.TotalCiphertextBytes, + transfer.ExpiresAt, + transfer.CreatedAt, + transfer.UpdatedAt, + transfer.FinalizedAt, + transfer.ManageTokenHash, + transfer.DeletedAt, + transfer.PurgedAt, + } +} + +func fileScanValues(file models.TransferFile) []any { + return []any{ + file.TransferID, + file.OpaqueFileID, + file.TotalChunks, + file.CiphertextBytes, + file.PlaintextBytes, + file.ChunkSize, + file.UploadStatus, + file.CreatedAt, + file.UpdatedAt, + } +} diff --git a/apps/api/internal/service/transfer_service.go b/apps/api/internal/service/transfer_service.go new file mode 100644 index 0000000..ac2a1e9 --- /dev/null +++ b/apps/api/internal/service/transfer_service.go @@ -0,0 +1,716 @@ +package service + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/models" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" + "github.com/xdrop/monorepo/internal/storage" +) + +var readRandom = rand.Read + +// Service coordinates transfer lifecycle rules across storage, persistence, and rate limits. +type Service struct { + cfg config.Config + repo repo.Repository + storage storage.ObjectStorage + limiter ratelimit.Limiter +} + +// New builds the application service with its persistence, storage, and rate-limit dependencies. +func New(cfg config.Config, repository repo.Repository, objectStorage storage.ObjectStorage, limiter ratelimit.Limiter) *Service { + return &Service{ + cfg: cfg, + repo: repository, + storage: objectStorage, + limiter: limiter, + } +} + +// UploadConfig advertises browser upload constraints derived from server policy. +type UploadConfig struct { + ChunkSize int64 `json:"chunkSize"` + MaxParallel int `json:"maxParallel"` + MaxFileCount int `json:"maxFileCount"` + MaxTransferBytes int64 `json:"maxTransferBytes"` +} + +// CreateTransferRequest starts a new transfer with a supported expiry option. +type CreateTransferRequest struct { + ExpiresInSeconds int `json:"expiresInSeconds"` + ExpiresInDays int `json:"expiresInDays,omitempty"` +} + +// CreateTransferResponse returns the transfer handle, manage token, and upload limits. +type CreateTransferResponse struct { + TransferID string `json:"transferId"` + ManageToken string `json:"manageToken"` + UploadConfig UploadConfig `json:"uploadConfig"` + ExpiresAt time.Time `json:"expiresAt"` +} + +// RegisterFileRequest describes one encrypted file before chunk upload begins. +type RegisterFileRequest struct { + FileID string `json:"fileId"` + TotalChunks int `json:"totalChunks"` + CiphertextBytes int64 `json:"ciphertextBytes"` + PlaintextBytes *int64 `json:"plaintextBytes"` + ChunkSize int64 `json:"chunkSize"` +} + +// UploadURLRequest asks the server to presign upload URLs for a batch of chunks. +type UploadURLRequest struct { + Chunks []UploadChunkRequest `json:"chunks"` +} + +// UploadChunkRequest identifies one chunk within a file. +type UploadChunkRequest struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` +} + +// UploadURLItem contains the storage coordinates for an uploadable chunk. +type UploadURLItem struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + ObjectKey string `json:"objectKey"` + URL string `json:"url"` +} + +// CompleteChunkRequest records a successfully uploaded chunk and its checksum. +type CompleteChunkRequest struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + CiphertextSize int64 `json:"ciphertextSize"` + ChecksumSHA256 string `json:"checksumSha256"` +} + +// ManifestUploadRequest uploads the encrypted manifest envelope as base64 text. +type ManifestUploadRequest struct { + CiphertextBase64 string `json:"ciphertextBase64"` +} + +// FinalizeTransferRequest promotes a complete upload into a downloadable transfer. +type FinalizeTransferRequest struct { + WrappedRootKey string `json:"wrappedRootKey"` + TotalFiles int `json:"totalFiles"` + TotalCiphertextBytes int64 `json:"totalCiphertextBytes"` +} + +// UpdateTransferRequest describes the mutable parts of a transfer after creation. +type UpdateTransferRequest struct { + ExpiresInSeconds *int `json:"expiresInSeconds"` + ExpiresInDays *int `json:"expiresInDays,omitempty"` + CiphertextBase64 string `json:"ciphertextBase64"` +} + +// ManageTransferResponse exposes resume and management state to the transfer owner. +type ManageTransferResponse struct { + ID string `json:"id"` + Status models.TransferStatus `json:"status"` + ExpiresAt time.Time `json:"expiresAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + FinalizedAt *time.Time `json:"finalizedAt,omitempty"` + ManifestCiphertextSize int64 `json:"manifestCiphertextSize"` + TotalFiles int `json:"totalFiles"` + TotalCiphertextBytes int64 `json:"totalCiphertextBytes"` + Files []ManageTransferFile `json:"files"` + UploadedChunks map[string][]int `json:"uploadedChunks,omitempty"` +} + +// ManageTransferFile reports per-file upload progress for the owner view. +type ManageTransferFile struct { + FileID string `json:"fileId"` + TotalChunks int `json:"totalChunks"` + CiphertextBytes int64 `json:"ciphertextBytes"` + ChunkSize int64 `json:"chunkSize"` + UploadStatus string `json:"uploadStatus"` +} + +// PublicTransferResponse exposes the public download state without revealing manage controls. +type PublicTransferResponse struct { + ID string `json:"id"` + Status string `json:"status"` + ExpiresAt time.Time `json:"expiresAt"` + WrappedRootKey string `json:"wrappedRootKey,omitempty"` + ManifestURL string `json:"manifestUrl,omitempty"` + ManifestCiphertextSize int64 `json:"manifestCiphertextSize,omitempty"` + DownloadConfig PublicDownloadConfig `json:"downloadConfig,omitempty"` +} + +// PublicDownloadConfig advertises public download behavior that the client should honor. +type PublicDownloadConfig struct { + PresignTTLSeconds int `json:"presignTtlSeconds"` +} + +// DownloadURLRequest asks for download URLs for specific uploaded chunks. +type DownloadURLRequest struct { + Chunks []UploadChunkRequest `json:"chunks"` +} + +// DownloadURLItem returns a public download URL for one chunk. +type DownloadURLItem struct { + FileID string `json:"fileId"` + ChunkIndex int `json:"chunkIndex"` + URL string `json:"url"` +} + +// HTTPError carries an HTTP status and machine-readable API code through the service layer. +type HTTPError struct { + Status int + Code string + Message string +} + +func (e *HTTPError) Error() string { + return e.Message +} + +// CreateTransfer creates a draft transfer and returns its manage token and upload policy. +func (s *Service) CreateTransfer(ctx context.Context, clientKey string, request CreateTransferRequest) (CreateTransferResponse, error) { + if err := s.enforceRateLimit(ctx, "create:"+clientKey, s.cfg.CreateLimit, time.Hour); err != nil { + return CreateTransferResponse{}, err + } + + expiryDuration, err := requestedCreateExpiry(request, s.cfg.DefaultExpiry) + if err != nil { + return CreateTransferResponse{}, err + } + + transferID, err := randomToken(18) + if err != nil { + return CreateTransferResponse{}, fmt.Errorf("generate transfer id: %w", err) + } + manageToken, err := randomToken(32) + if err != nil { + return CreateTransferResponse{}, fmt.Errorf("generate manage token: %w", err) + } + + now := time.Now().UTC() + expiresAt := now.Add(expiryDuration) + transfer := models.Transfer{ + ID: transferID, + Status: models.TransferStatusDraft, + ExpiresAt: expiresAt, + CreatedAt: now, + UpdatedAt: now, + ManageTokenHash: hashToken(manageToken), + } + + if err := s.repo.CreateTransfer(ctx, transfer); err != nil { + return CreateTransferResponse{}, fmt.Errorf("create transfer: %w", err) + } + + return CreateTransferResponse{ + TransferID: transferID, + ManageToken: manageToken, + UploadConfig: UploadConfig{ + ChunkSize: s.cfg.ChunkSize, + MaxParallel: 6, + MaxFileCount: s.cfg.MaxFileCount, + MaxTransferBytes: s.cfg.MaxTransferBytes, + }, + ExpiresAt: expiresAt, + }, nil +} + +// RegisterFiles validates encrypted file metadata before upload URLs are issued. +func (s *Service) RegisterFiles(ctx context.Context, transferID string, token string, files []RegisterFileRequest) error { + if len(files) == 0 { + return &HTTPError{Status: 400, Code: "empty_files", Message: "at least one file must be registered"} + } + if len(files) > s.cfg.MaxFileCount { + return &HTTPError{Status: 400, Code: "too_many_files", Message: "file count exceeds configured maximum"} + } + + transfer, err := s.authorizeManage(ctx, transferID, token) + if err != nil { + return err + } + if isExpired(transfer) { + return &HTTPError{Status: 410, Code: "expired", Message: "transfer has expired"} + } + + totalBytes := int64(0) + modelFiles := make([]models.TransferFile, 0, len(files)) + for _, file := range files { + if strings.TrimSpace(file.FileID) == "" || file.TotalChunks <= 0 || file.ChunkSize <= 0 || file.CiphertextBytes <= 0 { + return &HTTPError{Status: 400, Code: "invalid_file_registration", Message: "file registration contains invalid values"} + } + totalBytes += file.CiphertextBytes + modelFiles = append(modelFiles, models.TransferFile{ + TransferID: transferID, + OpaqueFileID: file.FileID, + TotalChunks: file.TotalChunks, + CiphertextBytes: file.CiphertextBytes, + PlaintextBytes: file.PlaintextBytes, + ChunkSize: file.ChunkSize, + UploadStatus: "pending", + }) + } + + if totalBytes > s.cfg.MaxTransferBytes { + return &HTTPError{Status: 400, Code: "transfer_too_large", Message: "transfer exceeds configured size limit"} + } + + if err := s.repo.RegisterFiles(ctx, transferID, modelFiles); err != nil { + return fmt.Errorf("register files: %w", err) + } + + return nil +} + +// CreateUploadURLs presigns upload destinations for a managed transfer. +func (s *Service) CreateUploadURLs(ctx context.Context, transferID string, token string, request UploadURLRequest) ([]UploadURLItem, error) { + transfer, err := s.authorizeManage(ctx, transferID, token) + if err != nil { + return nil, err + } + if isExpired(transfer) { + return nil, &HTTPError{Status: 410, Code: "expired", Message: "transfer has expired"} + } + + files, err := s.repo.ListFiles(ctx, transferID) + if err != nil { + return nil, fmt.Errorf("list files: %w", err) + } + fileIndex := map[string]models.TransferFile{} + for _, file := range files { + fileIndex[file.OpaqueFileID] = file + } + + urls := make([]UploadURLItem, 0, len(request.Chunks)) + for _, chunk := range request.Chunks { + file, ok := fileIndex[chunk.FileID] + if !ok || chunk.ChunkIndex < 0 || chunk.ChunkIndex >= file.TotalChunks { + return nil, &HTTPError{Status: 400, Code: "invalid_chunk_request", Message: "upload chunk request is invalid"} + } + + objectKey := chunkObjectKey(transferID, chunk.FileID, chunk.ChunkIndex) + url, err := s.storage.PresignUpload(ctx, objectKey, s.cfg.PresignTTL) + if err != nil { + return nil, fmt.Errorf("presign upload %s/%d: %w", chunk.FileID, chunk.ChunkIndex, err) + } + + urls = append(urls, UploadURLItem{ + FileID: chunk.FileID, + ChunkIndex: chunk.ChunkIndex, + ObjectKey: objectKey, + URL: url, + }) + } + + return urls, nil +} + +// CompleteChunks records uploaded chunk metadata for resume and integrity checks. +func (s *Service) CompleteChunks(ctx context.Context, transferID string, token string, chunks []CompleteChunkRequest) error { + transfer, err := s.authorizeManage(ctx, transferID, token) + if err != nil { + return err + } + if isExpired(transfer) { + return &HTTPError{Status: 410, Code: "expired", Message: "transfer has expired"} + } + + modelChunks := make([]models.TransferChunk, 0, len(chunks)) + for _, chunk := range chunks { + if strings.TrimSpace(chunk.FileID) == "" || chunk.ChunkIndex < 0 || chunk.CiphertextSize <= 0 || strings.TrimSpace(chunk.ChecksumSHA256) == "" { + return &HTTPError{Status: 400, Code: "invalid_chunk_completion", Message: "chunk completion payload is invalid"} + } + modelChunks = append(modelChunks, models.TransferChunk{ + TransferID: transferID, + OpaqueFileID: chunk.FileID, + ChunkIndex: chunk.ChunkIndex, + ObjectKey: chunkObjectKey(transferID, chunk.FileID, chunk.ChunkIndex), + CiphertextSize: chunk.CiphertextSize, + ChecksumSHA256: chunk.ChecksumSHA256, + }) + } + + if err := s.repo.CompleteChunks(ctx, transferID, modelChunks); err != nil { + return fmt.Errorf("complete chunks: %w", err) + } + + return nil +} + +// PutManifest stores the encrypted manifest and records its object location. +func (s *Service) PutManifest(ctx context.Context, transferID string, token string, request ManifestUploadRequest) error { + transfer, err := s.authorizeManage(ctx, transferID, token) + if err != nil { + return err + } + if isExpired(transfer) { + return &HTTPError{Status: 410, Code: "expired", Message: "transfer has expired"} + } + + payload, err := base64.StdEncoding.DecodeString(request.CiphertextBase64) + if err != nil || len(payload) == 0 { + return &HTTPError{Status: 400, Code: "invalid_manifest", Message: "manifest payload must be valid base64 ciphertext"} + } + + objectKey := manifestObjectKey(transferID) + if err := s.storage.PutObject(ctx, objectKey, payload, "application/octet-stream"); err != nil { + return fmt.Errorf("put manifest: %w", err) + } + if err := s.repo.SetManifest(ctx, transferID, objectKey, int64(len(payload))); err != nil { + return fmt.Errorf("set manifest: %w", err) + } + + return nil +} + +// FinalizeTransfer marks a fully uploaded transfer as ready for public downloads. +func (s *Service) FinalizeTransfer(ctx context.Context, transferID string, token string, request FinalizeTransferRequest) error { + transfer, err := s.authorizeManage(ctx, transferID, token) + if err != nil { + return err + } + if isExpired(transfer) { + return &HTTPError{Status: 410, Code: "expired", Message: "transfer has expired"} + } + if strings.TrimSpace(request.WrappedRootKey) == "" || request.TotalFiles <= 0 || request.TotalCiphertextBytes <= 0 { + return &HTTPError{Status: 400, Code: "invalid_finalize", Message: "finalize payload is invalid"} + } + + if err := s.repo.FinalizeTransfer(ctx, transferID, request.WrappedRootKey, request.TotalFiles, request.TotalCiphertextBytes); err != nil { + return err + } + + return nil +} + +// GetManageTransfer returns owner-facing transfer state, including uploaded chunk indexes. +func (s *Service) GetManageTransfer(ctx context.Context, transferID string, token string) (ManageTransferResponse, error) { + if _, err := s.authorizeManage(ctx, transferID, token); err != nil { + return ManageTransferResponse{}, err + } + + resume, err := s.repo.GetResumeState(ctx, transferID) + if err != nil { + return ManageTransferResponse{}, err + } + + files := make([]ManageTransferFile, 0, len(resume.Files)) + for _, file := range resume.Files { + files = append(files, ManageTransferFile{ + FileID: file.OpaqueFileID, + TotalChunks: file.TotalChunks, + CiphertextBytes: file.CiphertextBytes, + ChunkSize: file.ChunkSize, + UploadStatus: file.UploadStatus, + }) + } + + return ManageTransferResponse{ + ID: resume.Transfer.ID, + Status: publicStatus(resume.Transfer), + ExpiresAt: resume.Transfer.ExpiresAt, + CreatedAt: resume.Transfer.CreatedAt, + UpdatedAt: resume.Transfer.UpdatedAt, + FinalizedAt: resume.Transfer.FinalizedAt, + ManifestCiphertextSize: resume.Transfer.ManifestCiphertextSize, + TotalFiles: resume.Transfer.TotalFiles, + TotalCiphertextBytes: resume.Transfer.TotalCiphertextBytes, + Files: files, + UploadedChunks: resume.UploadedChunks, + }, nil +} + +// ResumeTransfer is an alias for GetManageTransfer used by the browser resume flow. +func (s *Service) ResumeTransfer(ctx context.Context, transferID string, token string) (ManageTransferResponse, error) { + return s.GetManageTransfer(ctx, transferID, token) +} + +// UpdateTransfer changes supported mutable fields such as expiry or manifest ciphertext. +func (s *Service) UpdateTransfer(ctx context.Context, transferID string, token string, request UpdateTransferRequest) error { + transfer, err := s.authorizeManage(ctx, transferID, token) + if err != nil { + return err + } + if publicStatus(transfer) == models.TransferStatusDeleted { + return &HTTPError{Status: 410, Code: "deleted", Message: "transfer has been deleted"} + } + + params := models.UpdateTransferParams{} + if expiryDuration, shouldUpdateExpiry, err := requestedUpdateExpiry(request); err != nil { + return err + } else if shouldUpdateExpiry { + expiresAt := time.Now().UTC().Add(expiryDuration) + params.ExpiresAt = &expiresAt + } + + if strings.TrimSpace(request.CiphertextBase64) != "" { + payload, err := base64.StdEncoding.DecodeString(request.CiphertextBase64) + if err != nil || len(payload) == 0 { + return &HTTPError{Status: 400, Code: "invalid_manifest", Message: "updated manifest must be valid base64 ciphertext"} + } + objectKey := transfer.ManifestObjectKey + if objectKey == "" { + objectKey = manifestObjectKey(transferID) + } + if err := s.storage.PutObject(ctx, objectKey, payload, "application/octet-stream"); err != nil { + return fmt.Errorf("put updated manifest: %w", err) + } + params.ManifestObjectKey = &objectKey + size := int64(len(payload)) + params.ManifestCiphertextSize = &size + } + + if err := s.repo.UpdateTransfer(ctx, transferID, params); err != nil { + return fmt.Errorf("update transfer: %w", err) + } + + return nil +} + +// DeleteTransfer tombstones a transfer and best-effort removes its remote objects. +func (s *Service) DeleteTransfer(ctx context.Context, transferID string, token string) error { + if _, err := s.authorizeManage(ctx, transferID, token); err != nil { + return err + } + + if err := s.repo.MarkDeleted(ctx, transferID); err != nil { + return fmt.Errorf("delete transfer: %w", err) + } + _ = s.storage.DeletePrefix(ctx, transferPrefix(transferID)) + + return nil +} + +// GetPublicTransfer returns the public download descriptor for recipients. +func (s *Service) GetPublicTransfer(ctx context.Context, clientKey string, transferID string) (PublicTransferResponse, error) { + if err := s.enforceRateLimit(ctx, "public:"+clientKey+":"+transferID, s.cfg.PublicReadLimit, time.Minute); err != nil { + return PublicTransferResponse{}, err + } + + transfer, err := s.repo.GetTransfer(ctx, transferID) + if errors.Is(err, repo.ErrNotFound) { + return PublicTransferResponse{}, &HTTPError{Status: 404, Code: "not_found", Message: "transfer not found"} + } + if err != nil { + return PublicTransferResponse{}, err + } + + status := publicStatus(transfer) + response := PublicTransferResponse{ + ID: transfer.ID, + Status: string(status), + ExpiresAt: transfer.ExpiresAt, + } + + if status != models.TransferStatusReady { + return response, nil + } + + manifestURL, err := s.storage.PresignDownload(ctx, transfer.ManifestObjectKey, s.cfg.PresignTTL) + if err != nil { + return PublicTransferResponse{}, fmt.Errorf("presign manifest download: %w", err) + } + + response.WrappedRootKey = transfer.WrappedRootKey + response.ManifestURL = manifestURL + response.ManifestCiphertextSize = transfer.ManifestCiphertextSize + response.DownloadConfig = PublicDownloadConfig{PresignTTLSeconds: int(s.cfg.PresignTTL.Seconds())} + + return response, nil +} + +// CreateDownloadURLs presigns recipient download URLs for chunks that actually exist. +func (s *Service) CreateDownloadURLs(ctx context.Context, clientKey string, transferID string, request DownloadURLRequest) ([]DownloadURLItem, error) { + if err := s.enforceRateLimit(ctx, "download:"+clientKey+":"+transferID, s.cfg.DownloadURLLimit, time.Minute); err != nil { + return nil, err + } + + resume, err := s.repo.GetResumeState(ctx, transferID) + if errors.Is(err, repo.ErrNotFound) { + return nil, &HTTPError{Status: 404, Code: "not_found", Message: "transfer not found"} + } + if err != nil { + return nil, err + } + if publicStatus(resume.Transfer) != models.TransferStatusReady { + return nil, &HTTPError{Status: 409, Code: "transfer_unavailable", Message: "transfer is not available for download"} + } + + uploaded := map[string]map[int]struct{}{} + for fileID, chunks := range resume.UploadedChunks { + uploaded[fileID] = map[int]struct{}{} + for _, chunkIndex := range chunks { + uploaded[fileID][chunkIndex] = struct{}{} + } + } + + urls := make([]DownloadURLItem, 0, len(request.Chunks)) + for _, chunk := range request.Chunks { + chunkSet, ok := uploaded[chunk.FileID] + if !ok { + return nil, &HTTPError{Status: 400, Code: "invalid_download_request", Message: "requested file is unavailable"} + } + if _, ok = chunkSet[chunk.ChunkIndex]; !ok { + return nil, &HTTPError{Status: 400, Code: "invalid_download_request", Message: "requested chunk is unavailable"} + } + + url, err := s.storage.PresignDownload(ctx, chunkObjectKey(transferID, chunk.FileID, chunk.ChunkIndex), s.cfg.PresignTTL) + if err != nil { + return nil, fmt.Errorf("presign chunk download: %w", err) + } + urls = append(urls, DownloadURLItem{ + FileID: chunk.FileID, + ChunkIndex: chunk.ChunkIndex, + URL: url, + }) + } + + return urls, nil +} + +// CleanupExpired removes remote objects for expired or deleted transfers and marks them purged. +func (s *Service) CleanupExpired(ctx context.Context) error { + transfers, err := s.repo.ListCleanupCandidates(ctx, 200) + if err != nil { + return fmt.Errorf("list cleanup candidates: %w", err) + } + + for _, transfer := range transfers { + if err := s.storage.DeletePrefix(ctx, transferPrefix(transfer.ID)); err != nil { + return fmt.Errorf("delete transfer objects %s: %w", transfer.ID, err) + } + if err := s.repo.MarkPurged(ctx, transfer.ID); err != nil { + return fmt.Errorf("mark purged %s: %w", transfer.ID, err) + } + } + + return nil +} + +// authorizeManage validates the owner token using a constant-time hash comparison. +func (s *Service) authorizeManage(ctx context.Context, transferID string, token string) (models.Transfer, error) { + if strings.TrimSpace(token) == "" { + return models.Transfer{}, &HTTPError{Status: 401, Code: "missing_manage_token", Message: "manage token is required"} + } + + transfer, err := s.repo.GetTransfer(ctx, transferID) + if errors.Is(err, repo.ErrNotFound) { + return models.Transfer{}, &HTTPError{Status: 404, Code: "not_found", Message: "transfer not found"} + } + if err != nil { + return models.Transfer{}, err + } + + givenHash := hashToken(token) + if subtle.ConstantTimeCompare([]byte(givenHash), []byte(transfer.ManageTokenHash)) != 1 { + return models.Transfer{}, &HTTPError{Status: 403, Code: "invalid_manage_token", Message: "manage token is invalid"} + } + + return transfer, nil +} + +// enforceRateLimit translates limiter decisions into API errors. +func (s *Service) enforceRateLimit(ctx context.Context, key string, limit int, window time.Duration) error { + allowed, err := s.limiter.Allow(ctx, key, limit, window) + if err != nil { + return fmt.Errorf("rate limit check: %w", err) + } + if !allowed { + return &HTTPError{Status: 429, Code: "rate_limited", Message: "too many requests"} + } + + return nil +} + +// hashToken stores manage tokens as SHA-256 digests instead of plaintext. +func hashToken(token string) string { + sum := sha256.Sum256([]byte(token)) + return hex.EncodeToString(sum[:]) +} + +// randomToken generates URL-safe opaque identifiers for transfers and manage tokens. +func randomToken(size int) (string, error) { + buffer := make([]byte, size) + if _, err := readRandom(buffer); err != nil { + return "", err + } + + return base64.RawURLEncoding.EncodeToString(buffer), nil +} + +// publicStatus collapses internal states into the limited public status model. +func publicStatus(transfer models.Transfer) models.TransferStatus { + if transfer.DeletedAt != nil || transfer.Status == models.TransferStatusDeleted { + return models.TransferStatusDeleted + } + if isExpired(transfer) { + return models.TransferStatusExpired + } + if transfer.Status != models.TransferStatusReady || transfer.ManifestObjectKey == "" || transfer.WrappedRootKey == "" { + return models.TransferStatusIncomplete + } + + return models.TransferStatusReady +} + +func isExpired(transfer models.Transfer) bool { + return transfer.ExpiresAt.Before(time.Now().UTC()) +} + +func requestedCreateExpiry(request CreateTransferRequest, fallback time.Duration) (time.Duration, error) { + if request.ExpiresInSeconds > 0 { + return validateExpiryDuration(time.Duration(request.ExpiresInSeconds) * time.Second) + } + if request.ExpiresInDays > 0 { + return validateExpiryDuration(time.Duration(request.ExpiresInDays) * 24 * time.Hour) + } + + return validateExpiryDuration(fallback) +} + +func requestedUpdateExpiry(request UpdateTransferRequest) (time.Duration, bool, error) { + if request.ExpiresInSeconds != nil { + duration, err := validateExpiryDuration(time.Duration(*request.ExpiresInSeconds) * time.Second) + return duration, true, err + } + if request.ExpiresInDays != nil { + duration, err := validateExpiryDuration(time.Duration(*request.ExpiresInDays) * 24 * time.Hour) + return duration, true, err + } + + return 0, false, nil +} + +func validateExpiryDuration(duration time.Duration) (time.Duration, error) { + if !config.IsAllowedExpiry(duration) { + return 0, &HTTPError{Status: 400, Code: "invalid_expiry", Message: "expiry must match a supported option"} + } + + return duration, nil +} + +// manifestObjectKey is the canonical object path for an encrypted manifest. +func manifestObjectKey(transferID string) string { + return fmt.Sprintf("transfers/%s/manifest.bin", transferID) +} + +// chunkObjectKey is the canonical object path for one encrypted chunk. +func chunkObjectKey(transferID string, fileID string, chunkIndex int) string { + return fmt.Sprintf("transfers/%s/files/%s/chunks/%08d.bin", transferID, fileID, chunkIndex) +} + +// transferPrefix returns the storage prefix containing every object for a transfer. +func transferPrefix(transferID string) string { + return fmt.Sprintf("transfers/%s/", transferID) +} diff --git a/apps/api/internal/service/transfer_service_additional_test.go b/apps/api/internal/service/transfer_service_additional_test.go new file mode 100644 index 0000000..28b183b --- /dev/null +++ b/apps/api/internal/service/transfer_service_additional_test.go @@ -0,0 +1,916 @@ +package service + +import ( + "context" + "encoding/base64" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/models" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" +) + +func TestHTTPErrorMessageAndExpiryHelpers(t *testing.T) { + t.Parallel() + + t.Run("http error exposes message", func(t *testing.T) { + t.Parallel() + + err := &HTTPError{Message: "plain message"} + require.Equal(t, "plain message", err.Error()) + }) + + t.Run("requested create expiry supports days and fallback", func(t *testing.T) { + t.Parallel() + + duration, err := requestedCreateExpiry(CreateTransferRequest{ExpiresInDays: 1}, 12*time.Hour) + require.NoError(t, err) + require.Equal(t, 24*time.Hour, duration) + + duration, err = requestedCreateExpiry(CreateTransferRequest{}, 7*24*time.Hour) + require.NoError(t, err) + require.Equal(t, 7*24*time.Hour, duration) + }) + + t.Run("requested update expiry supports days and noop", func(t *testing.T) { + t.Parallel() + + days := 7 + duration, shouldUpdate, err := requestedUpdateExpiry(UpdateTransferRequest{ExpiresInDays: &days}) + require.NoError(t, err) + require.True(t, shouldUpdate) + require.Equal(t, 7*24*time.Hour, duration) + + duration, shouldUpdate, err = requestedUpdateExpiry(UpdateTransferRequest{}) + require.NoError(t, err) + require.False(t, shouldUpdate) + require.Zero(t, duration) + }) +} + +func TestCreateTransferPropagatesLimiterAndRepositoryErrors(t *testing.T) { + t.Parallel() + + t.Run("limiter error", func(t *testing.T) { + t.Parallel() + + svc := New(testConfig(), newMemoryRepository(), &memoryStorage{}, errorLimiter{err: errors.New("redis unavailable")}) + + _, err := svc.CreateTransfer(context.Background(), "198.51.100.10", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.ErrorContains(t, err, "rate limit check") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + createTransferErr: errors.New("insert failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + _, err := svc.CreateTransfer(context.Background(), "198.51.100.10", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.ErrorContains(t, err, "create transfer") + }) +} + +func TestCreateTransferPropagatesRandomTokenFailures(t *testing.T) { + originalReadRandom := readRandom + t.Cleanup(func() { + readRandom = originalReadRandom + }) + + t.Run("transfer id generation", func(t *testing.T) { + readRandom = func([]byte) (int, error) { + return 0, errors.New("entropy failed") + } + + svc := newTestService(newMemoryRepository()) + _, err := svc.CreateTransfer(context.Background(), "198.51.100.10", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.ErrorContains(t, err, "generate transfer id") + }) + + t.Run("manage token generation", func(t *testing.T) { + calls := 0 + readRandom = func(buffer []byte) (int, error) { + calls++ + if calls == 2 { + return 0, errors.New("entropy failed") + } + for index := range buffer { + buffer[index] = byte(index + 1) + } + return len(buffer), nil + } + + svc := newTestService(newMemoryRepository()) + _, err := svc.CreateTransfer(context.Background(), "198.51.100.10", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.ErrorContains(t, err, "generate manage token") + }) +} + +func TestCreateUploadURLsHandlesExpiryAndStorageFailures(t *testing.T) { + t.Parallel() + + t.Run("expired transfer", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + expireTransfer(t, repository, create.TransferID) + + _, err := svc.CreateUploadURLs(context.Background(), create.TransferID, create.ManageToken, UploadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + requireHTTPError(t, err, 410, "expired") + }) + + t.Run("presign failure", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + presignUploadErr: errors.New("presign upload failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + + _, err := svc.CreateUploadURLs(context.Background(), create.TransferID, create.ManageToken, UploadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + require.ErrorContains(t, err, "presign upload") + }) + + t.Run("list files failure", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + listFilesErr: errors.New("query failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + _, err := svc.CreateUploadURLs(context.Background(), create.TransferID, create.ManageToken, UploadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + require.ErrorContains(t, err, "list files") + }) +} + +func TestCreateUploadURLsReturnsPresignedItems(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + + items, err := svc.CreateUploadURLs(context.Background(), create.TransferID, create.ManageToken, UploadURLRequest{ + Chunks: []UploadChunkRequest{ + {FileID: "file-a", ChunkIndex: 0}, + {FileID: "file-a", ChunkIndex: 1}, + }, + }) + require.NoError(t, err) + require.Len(t, items, 2) + require.Equal(t, "file-a", items[0].FileID) + require.Equal(t, 0, items[0].ChunkIndex) + require.Contains(t, items[0].URL, chunkObjectKey(create.TransferID, "file-a", 0)) +} + +func TestCompleteChunksHandlesExpiryAndRepositoryFailures(t *testing.T) { + t.Parallel() + + t.Run("expired transfer", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + expireTransfer(t, repository, create.TransferID) + + err := svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{{ + FileID: "file-a", + ChunkIndex: 0, + CiphertextSize: 32, + ChecksumSHA256: "checksum", + }}) + requireHTTPError(t, err, 410, "expired") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + completeChunksErr: errors.New("upsert failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + + err := svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{{ + FileID: "file-a", + ChunkIndex: 0, + CiphertextSize: 32, + ChecksumSHA256: "checksum", + }}) + require.ErrorContains(t, err, "complete chunks") + }) + + t.Run("repository lookup error during authorization", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{memoryRepository: newMemoryRepository()} + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + repository.getTransferErr = errors.New("lookup failed") + + err := svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{{ + FileID: "file-a", + ChunkIndex: 0, + CiphertextSize: 32, + ChecksumSHA256: "checksum", + }}) + require.ErrorContains(t, err, "lookup failed") + }) +} + +func TestManageAuthorizedMethodsPropagateAuthorizationFailures(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + requireHTTPError( + t, + svc.RegisterFiles(context.Background(), create.TransferID, "", []RegisterFileRequest{{ + FileID: "file-a", + TotalChunks: 1, + CiphertextBytes: 32, + ChunkSize: 32, + }}), + 401, + "missing_manage_token", + ) + + _, err := svc.CreateUploadURLs(context.Background(), create.TransferID, "", UploadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + requireHTTPError(t, err, 401, "missing_manage_token") + + err = svc.PutManifest(context.Background(), create.TransferID, "", ManifestUploadRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString([]byte("manifest")), + }) + requireHTTPError(t, err, 401, "missing_manage_token") + + err = svc.FinalizeTransfer(context.Background(), create.TransferID, "", FinalizeTransferRequest{ + WrappedRootKey: "wrapped", + TotalFiles: 1, + TotalCiphertextBytes: 32, + }) + requireHTTPError(t, err, 401, "missing_manage_token") + + err = svc.UpdateTransfer(context.Background(), create.TransferID, "", UpdateTransferRequest{}) + requireHTTPError(t, err, 401, "missing_manage_token") +} + +func TestCompleteChunksRejectsInvalidPayload(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + err := svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{{ + FileID: " ", + ChunkIndex: 0, + CiphertextSize: 32, + ChecksumSHA256: "checksum", + }}) + requireHTTPError(t, err, 400, "invalid_chunk_completion") +} + +func TestPutManifestHandlesExpiryAndFailures(t *testing.T) { + t.Parallel() + + t.Run("expired transfer", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + expireTransfer(t, repository, create.TransferID) + + err := svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString([]byte("manifest")), + }) + requireHTTPError(t, err, 410, "expired") + }) + + t.Run("storage error", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + putObjectErr: errors.New("upload failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + err := svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString([]byte("manifest")), + }) + require.ErrorContains(t, err, "put manifest") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + setManifestErr: errors.New("update failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + err := svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString([]byte("manifest")), + }) + require.ErrorContains(t, err, "set manifest") + }) +} + +func TestFinalizeTransferHandlesExpiryAndRepositoryFailures(t *testing.T) { + t.Parallel() + + t.Run("expired transfer", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + expireTransfer(t, repository, create.TransferID) + + err := svc.FinalizeTransfer(context.Background(), create.TransferID, create.ManageToken, FinalizeTransferRequest{ + WrappedRootKey: "wrapped", + TotalFiles: 1, + TotalCiphertextBytes: 32, + }) + requireHTTPError(t, err, 410, "expired") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + } + storage := &memoryStorage{} + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + require.NoError(t, svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{ + {FileID: "file-a", ChunkIndex: 0, CiphertextSize: 32, ChecksumSHA256: "a"}, + {FileID: "file-a", ChunkIndex: 1, CiphertextSize: 32, ChecksumSHA256: "b"}, + })) + require.NoError(t, svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString([]byte("manifest")), + })) + repository.finalizeErr = errors.New("finalize failed") + + err := svc.FinalizeTransfer(context.Background(), create.TransferID, create.ManageToken, FinalizeTransferRequest{ + WrappedRootKey: "wrapped", + TotalFiles: 1, + TotalCiphertextBytes: 64, + }) + require.ErrorContains(t, err, "finalize failed") + }) +} + +func TestGetManageTransferAndResumeTransferReturnState(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + require.NoError(t, svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{{ + FileID: "file-a", + ChunkIndex: 0, + CiphertextSize: 32, + ChecksumSHA256: "checksum", + }})) + + manage, err := svc.GetManageTransfer(context.Background(), create.TransferID, create.ManageToken) + require.NoError(t, err) + require.Equal(t, create.TransferID, manage.ID) + require.Len(t, manage.Files, 1) + require.Equal(t, "file-a", manage.Files[0].FileID) + require.Equal(t, []int{0}, manage.UploadedChunks["file-a"]) + + resume, err := svc.ResumeTransfer(context.Background(), create.TransferID, create.ManageToken) + require.NoError(t, err) + require.Equal(t, manage, resume) +} + +func TestGetManageTransferPropagatesResumeErrors(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + getResumeStateErr: errors.New("resume failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + _, err := svc.GetManageTransfer(context.Background(), create.TransferID, create.ManageToken) + require.ErrorContains(t, err, "resume failed") +} + +func TestUpdateTransferHandlesInvalidManifestAndRepositoryErrors(t *testing.T) { + t.Parallel() + + t.Run("invalid manifest", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + expirySeconds := 3600 + + err := svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + ExpiresInSeconds: &expirySeconds, + CiphertextBase64: "not-base64", + }) + requireHTTPError(t, err, 400, "invalid_manifest") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + updateErr: errors.New("update failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + expirySeconds := 3600 + + err := svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + ExpiresInSeconds: &expirySeconds, + }) + require.ErrorContains(t, err, "update transfer") + }) + + t.Run("invalid expiry", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + expirySeconds := 2 * 3600 + + err := svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + ExpiresInSeconds: &expirySeconds, + }) + requireHTTPError(t, err, 400, "invalid_expiry") + }) + + t.Run("storage error", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + putObjectErr: errors.New("upload failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + ciphertext := base64.StdEncoding.EncodeToString([]byte("updated-manifest")) + + err := svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + CiphertextBase64: ciphertext, + }) + require.ErrorContains(t, err, "put updated manifest") + }) + + t.Run("no-op update", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + err := svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{}) + require.NoError(t, err) + }) +} + +func TestDeleteTransferHandlesRepositoryAndStorageFailures(t *testing.T) { + t.Parallel() + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + markDeletedErr: errors.New("delete failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + err := svc.DeleteTransfer(context.Background(), create.TransferID, create.ManageToken) + require.ErrorContains(t, err, "delete transfer") + }) + + t.Run("storage delete prefix errors are ignored", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + deletePrefixErr: errors.New("delete prefix failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + err := svc.DeleteTransfer(context.Background(), create.TransferID, create.ManageToken) + require.NoError(t, err) + + transfer, getErr := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, getErr) + require.Equal(t, models.TransferStatusDeleted, transfer.Status) + }) + + t.Run("missing token", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + err := svc.DeleteTransfer(context.Background(), create.TransferID, "") + requireHTTPError(t, err, 401, "missing_manage_token") + }) +} + +func TestRegisterFilesPropagatesRepositoryErrors(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + registerFilesErr: errors.New("insert files failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + + err := svc.RegisterFiles(context.Background(), create.TransferID, create.ManageToken, []RegisterFileRequest{{ + FileID: "file-a", + TotalChunks: 1, + CiphertextBytes: 32, + ChunkSize: 32, + }}) + require.ErrorContains(t, err, "register files") +} + +func TestGetPublicTransferHandlesLimiterAndPresignFailures(t *testing.T) { + t.Parallel() + + t.Run("limiter error", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := New(testConfig(), repository, &memoryStorage{}, errorLimiter{err: errors.New("redis unavailable")}) + + _, err := svc.GetPublicTransfer(context.Background(), "198.51.100.10", "transfer-id") + require.ErrorContains(t, err, "rate limit check") + }) + + t.Run("presign error", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + presignDownloadErr: errors.New("presign download failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + makeReadyTransferForTest(t, svc, create.TransferID, create.ManageToken) + + _, err := svc.GetPublicTransfer(context.Background(), "198.51.100.10", create.TransferID) + require.ErrorContains(t, err, "presign manifest download") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + getTransferErr: errors.New("lookup failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + _, err := svc.GetPublicTransfer(context.Background(), "198.51.100.10", "transfer-id") + require.ErrorContains(t, err, "lookup failed") + }) +} + +func TestCreateDownloadURLsHandlesNotFoundAndPresignFailures(t *testing.T) { + t.Parallel() + + t.Run("not found", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + getResumeStateErr: repo.ErrNotFound, + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.10", "missing-transfer", DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + requireHTTPError(t, err, 404, "not_found") + }) + + t.Run("presign error", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + presignDownloadErr: errors.New("presign download failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + create := createTransferForTest(t, svc) + makeReadyTransferForTest(t, svc, create.TransferID, create.ManageToken) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.10", create.TransferID, DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + require.ErrorContains(t, err, "presign chunk download") + }) + + t.Run("limiter error", func(t *testing.T) { + t.Parallel() + + svc := New(testConfig(), newMemoryRepository(), &memoryStorage{}, errorLimiter{err: errors.New("redis unavailable")}) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.10", "transfer-id", DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + require.ErrorContains(t, err, "rate limit check") + }) + + t.Run("repository error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + getResumeStateErr: errors.New("resume failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.10", "transfer-id", DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + require.ErrorContains(t, err, "resume failed") + }) + + t.Run("requested file is unavailable", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + create := createTransferForTest(t, svc) + makeReadyTransferForTest(t, svc, create.TransferID, create.ManageToken) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.10", create.TransferID, DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "missing-file", ChunkIndex: 0}}, + }) + requireHTTPError(t, err, 400, "invalid_download_request") + }) +} + +func TestCleanupExpiredHandlesRepositoryAndStorageFailures(t *testing.T) { + t.Parallel() + + t.Run("list cleanup candidates error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + listCleanupErr: errors.New("list failed"), + } + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + err := svc.CleanupExpired(context.Background()) + require.ErrorContains(t, err, "list cleanup candidates") + }) + + t.Run("delete prefix error", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + expired := createExpiredTransferForCleanup() + require.NoError(t, repository.CreateOrReplace(expired)) + + storage := &failingStorage{ + memoryStorage: &memoryStorage{}, + deletePrefixErr: errors.New("delete failed"), + } + svc := New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) + + err := svc.CleanupExpired(context.Background()) + require.ErrorContains(t, err, "delete transfer objects") + }) + + t.Run("mark purged error", func(t *testing.T) { + t.Parallel() + + repository := &failingRepository{ + memoryRepository: newMemoryRepository(), + markPurgedErr: errors.New("mark purged failed"), + } + expired := createExpiredTransferForCleanup() + require.NoError(t, repository.CreateOrReplace(expired)) + + svc := New(testConfig(), repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + err := svc.CleanupExpired(context.Background()) + require.ErrorContains(t, err, "mark purged") + }) +} + +func createExpiredTransferForCleanup() models.Transfer { + return models.Transfer{ + ID: "expired-for-cleanup", + Status: models.TransferStatusReady, + ManageTokenHash: hashToken("manage-token"), + CreatedAt: time.Now().UTC().Add(-48 * time.Hour), + UpdatedAt: time.Now().UTC().Add(-48 * time.Hour), + ExpiresAt: time.Now().UTC().Add(-time.Hour), + } +} + +type errorLimiter struct { + err error +} + +func (l errorLimiter) Allow(context.Context, string, int, time.Duration) (bool, error) { + return false, l.err +} + +type failingRepository struct { + *memoryRepository + createTransferErr error + getTransferErr error + registerFilesErr error + listFilesErr error + completeChunksErr error + getResumeStateErr error + setManifestErr error + finalizeErr error + updateErr error + markDeletedErr error + listCleanupErr error + markPurgedErr error +} + +func (r *failingRepository) CreateTransfer(ctx context.Context, transfer models.Transfer) error { + if r.createTransferErr != nil { + return r.createTransferErr + } + return r.memoryRepository.CreateTransfer(ctx, transfer) +} + +func (r *failingRepository) GetTransfer(ctx context.Context, transferID string) (models.Transfer, error) { + if r.getTransferErr != nil { + return models.Transfer{}, r.getTransferErr + } + return r.memoryRepository.GetTransfer(ctx, transferID) +} + +func (r *failingRepository) RegisterFiles(ctx context.Context, transferID string, files []models.TransferFile) error { + if r.registerFilesErr != nil { + return r.registerFilesErr + } + return r.memoryRepository.RegisterFiles(ctx, transferID, files) +} + +func (r *failingRepository) ListFiles(ctx context.Context, transferID string) ([]models.TransferFile, error) { + if r.listFilesErr != nil { + return nil, r.listFilesErr + } + return r.memoryRepository.ListFiles(ctx, transferID) +} + +func (r *failingRepository) CompleteChunks(ctx context.Context, transferID string, chunks []models.TransferChunk) error { + if r.completeChunksErr != nil { + return r.completeChunksErr + } + return r.memoryRepository.CompleteChunks(ctx, transferID, chunks) +} + +func (r *failingRepository) GetResumeState(ctx context.Context, transferID string) (models.TransferResumeState, error) { + if r.getResumeStateErr != nil { + return models.TransferResumeState{}, r.getResumeStateErr + } + return r.memoryRepository.GetResumeState(ctx, transferID) +} + +func (r *failingRepository) SetManifest(ctx context.Context, transferID string, objectKey string, ciphertextSize int64) error { + if r.setManifestErr != nil { + return r.setManifestErr + } + return r.memoryRepository.SetManifest(ctx, transferID, objectKey, ciphertextSize) +} + +func (r *failingRepository) FinalizeTransfer(ctx context.Context, transferID string, wrappedRootKey string, totalFiles int, totalCiphertextBytes int64) error { + if r.finalizeErr != nil { + return r.finalizeErr + } + return r.memoryRepository.FinalizeTransfer(ctx, transferID, wrappedRootKey, totalFiles, totalCiphertextBytes) +} + +func (r *failingRepository) UpdateTransfer(ctx context.Context, transferID string, params models.UpdateTransferParams) error { + if r.updateErr != nil { + return r.updateErr + } + return r.memoryRepository.UpdateTransfer(ctx, transferID, params) +} + +func (r *failingRepository) MarkDeleted(ctx context.Context, transferID string) error { + if r.markDeletedErr != nil { + return r.markDeletedErr + } + return r.memoryRepository.MarkDeleted(ctx, transferID) +} + +func (r *failingRepository) ListCleanupCandidates(ctx context.Context, limit int) ([]models.Transfer, error) { + if r.listCleanupErr != nil { + return nil, r.listCleanupErr + } + return r.memoryRepository.ListCleanupCandidates(ctx, limit) +} + +func (r *failingRepository) MarkPurged(ctx context.Context, transferID string) error { + if r.markPurgedErr != nil { + return r.markPurgedErr + } + return r.memoryRepository.MarkPurged(ctx, transferID) +} + +type failingStorage struct { + *memoryStorage + presignUploadErr error + presignDownloadErr error + putObjectErr error + deletePrefixErr error +} + +func (s *failingStorage) PresignUpload(ctx context.Context, objectKey string, ttl time.Duration) (string, error) { + if s.presignUploadErr != nil { + return "", s.presignUploadErr + } + return s.memoryStorage.PresignUpload(ctx, objectKey, ttl) +} + +func (s *failingStorage) PresignDownload(ctx context.Context, objectKey string, ttl time.Duration) (string, error) { + if s.presignDownloadErr != nil { + return "", s.presignDownloadErr + } + return s.memoryStorage.PresignDownload(ctx, objectKey, ttl) +} + +func (s *failingStorage) PutObject(ctx context.Context, objectKey string, body []byte, contentType string) error { + if s.putObjectErr != nil { + return s.putObjectErr + } + return s.memoryStorage.PutObject(ctx, objectKey, body, contentType) +} + +func (s *failingStorage) DeletePrefix(ctx context.Context, prefix string) error { + if s.deletePrefixErr != nil { + return s.deletePrefixErr + } + return s.memoryStorage.DeletePrefix(ctx, prefix) +} + +func (s *failingStorage) EnsureBucket(context.Context) error { + return nil +} diff --git a/apps/api/internal/service/transfer_service_edge_test.go b/apps/api/internal/service/transfer_service_edge_test.go new file mode 100644 index 0000000..969f04f --- /dev/null +++ b/apps/api/internal/service/transfer_service_edge_test.go @@ -0,0 +1,408 @@ +package service + +import ( + "context" + "encoding/base64" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/models" +) + +func TestGetManageTransferRejectsMissingInvalidAndUnknownTokens(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + _, err := svc.GetManageTransfer(context.Background(), create.TransferID, "") + requireHTTPError(t, err, 401, "missing_manage_token") + + _, err = svc.GetManageTransfer(context.Background(), create.TransferID, "wrong-token") + requireHTTPError(t, err, 403, "invalid_manage_token") + + _, err = svc.GetManageTransfer(context.Background(), "missing-transfer", "present-token") + requireHTTPError(t, err, 404, "not_found") +} + +func TestRegisterFilesRejectsInvalidInputs(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + files []RegisterFileRequest + code string + }{ + { + name: "empty files", + files: nil, + code: "empty_files", + }, + { + name: "too many files", + files: func() []RegisterFileRequest { + files := make([]RegisterFileRequest, testConfig().MaxFileCount+1) + for i := range files { + files[i] = RegisterFileRequest{ + FileID: "file", + TotalChunks: 1, + CiphertextBytes: 1, + ChunkSize: 1, + } + } + return files + }(), + code: "too_many_files", + }, + { + name: "blank file id", + files: []RegisterFileRequest{{ + FileID: " ", + TotalChunks: 1, + CiphertextBytes: 1, + ChunkSize: 1, + }}, + code: "invalid_file_registration", + }, + { + name: "zero chunk size", + files: []RegisterFileRequest{{ + FileID: "file-a", + TotalChunks: 1, + CiphertextBytes: 1, + ChunkSize: 0, + }}, + code: "invalid_file_registration", + }, + { + name: "transfer too large", + files: []RegisterFileRequest{{ + FileID: "file-a", + TotalChunks: 1, + CiphertextBytes: testConfig().MaxTransferBytes + 1, + ChunkSize: 1, + }}, + code: "transfer_too_large", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + err := svc.RegisterFiles(context.Background(), create.TransferID, create.ManageToken, tc.files) + requireHTTPError(t, err, 400, tc.code) + }) + } +} + +func TestRegisterFilesRejectsExpiredTransfer(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + expireTransfer(t, repository, create.TransferID) + + err := svc.RegisterFiles(context.Background(), create.TransferID, create.ManageToken, []RegisterFileRequest{{ + FileID: "file-a", + TotalChunks: 1, + CiphertextBytes: 1, + ChunkSize: 1, + }}) + requireHTTPError(t, err, 410, "expired") +} + +func TestCreateUploadURLsRejectsUnknownAndOutOfRangeChunks(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + + testCases := []struct { + name string + chunk UploadChunkRequest + }{ + { + name: "unknown file", + chunk: UploadChunkRequest{FileID: "missing-file", ChunkIndex: 0}, + }, + { + name: "negative index", + chunk: UploadChunkRequest{FileID: "file-a", ChunkIndex: -1}, + }, + { + name: "past total chunks", + chunk: UploadChunkRequest{FileID: "file-a", ChunkIndex: 2}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := svc.CreateUploadURLs(context.Background(), create.TransferID, create.ManageToken, UploadURLRequest{ + Chunks: []UploadChunkRequest{tc.chunk}, + }) + requireHTTPError(t, err, 400, "invalid_chunk_request") + }) + } +} + +func TestPutManifestRejectsInvalidCiphertext(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + err := svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: "not-base64", + }) + requireHTTPError(t, err, 400, "invalid_manifest") + + err = svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: "", + }) + requireHTTPError(t, err, 400, "invalid_manifest") +} + +func TestFinalizeTransferRejectsInvalidPayload(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + create := createTransferForTest(t, svc) + registerFileForTest(t, svc, create.TransferID, create.ManageToken) + + err := svc.FinalizeTransfer(context.Background(), create.TransferID, create.ManageToken, FinalizeTransferRequest{ + WrappedRootKey: "", + TotalFiles: 1, + TotalCiphertextBytes: 16, + }) + requireHTTPError(t, err, 400, "invalid_finalize") +} + +func TestUpdateTransferUpdatesExpiryAndManifest(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + create := createTransferForTest(t, svc) + + transfer, err := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, err) + transfer.ManifestObjectKey = "transfers/custom/manifest.bin" + require.NoError(t, repository.CreateOrReplace(transfer)) + + ciphertext := []byte(`{"version":2}`) + expirySeconds := 3 * 3600 + err = svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + ExpiresInSeconds: &expirySeconds, + CiphertextBase64: base64.StdEncoding.EncodeToString(ciphertext), + }) + require.NoError(t, err) + + updated, err := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, err) + require.Equal(t, int64(len(ciphertext)), updated.ManifestCiphertextSize) + require.WithinDuration(t, time.Now().UTC().Add(3*time.Hour), updated.ExpiresAt, 3*time.Second) + require.Equal(t, ciphertext, storage.objects["transfers/custom/manifest.bin"]) +} + +func TestUpdateTransferGeneratesManifestObjectKeyWhenMissing(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + create := createTransferForTest(t, svc) + + ciphertext := []byte(`{"renamed":true}`) + err := svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString(ciphertext), + }) + require.NoError(t, err) + + updated, err := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, err) + require.Equal(t, manifestObjectKey(create.TransferID), updated.ManifestObjectKey) + require.Equal(t, int64(len(ciphertext)), updated.ManifestCiphertextSize) + require.Equal(t, ciphertext, storage.objects[manifestObjectKey(create.TransferID)]) +} + +func TestUpdateTransferRejectsDeletedTransfer(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + transfer, err := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, err) + now := time.Now().UTC() + transfer.Status = models.TransferStatusDeleted + transfer.DeletedAt = &now + require.NoError(t, repository.CreateOrReplace(transfer)) + + expirySeconds := 3600 + err = svc.UpdateTransfer(context.Background(), create.TransferID, create.ManageToken, UpdateTransferRequest{ + ExpiresInSeconds: &expirySeconds, + }) + requireHTTPError(t, err, 410, "deleted") +} + +func TestGetPublicTransferReturnsNotFoundAndExpiredStatuses(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + + _, err := svc.GetPublicTransfer(context.Background(), "198.51.100.20", "missing-transfer") + requireHTTPError(t, err, 404, "not_found") + + create := createTransferForTest(t, svc) + transfer, err := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, err) + transfer.Status = models.TransferStatusReady + transfer.ManifestObjectKey = manifestObjectKey(create.TransferID) + transfer.WrappedRootKey = `{"version":1}` + transfer.ExpiresAt = time.Now().UTC().Add(-time.Minute) + require.NoError(t, repository.CreateOrReplace(transfer)) + + response, err := svc.GetPublicTransfer(context.Background(), "198.51.100.20", create.TransferID) + require.NoError(t, err) + require.Equal(t, "expired", response.Status) + require.Empty(t, response.ManifestURL) + require.Empty(t, response.WrappedRootKey) +} + +func TestCreateDownloadURLsValidatesAvailabilityAndChunkPresence(t *testing.T) { + t.Parallel() + + t.Run("transfer not ready", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + create := createTransferForTest(t, svc) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.20", create.TransferID, DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 0}}, + }) + requireHTTPError(t, err, 409, "transfer_unavailable") + }) + + t.Run("invalid chunk request", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + create := createTransferForTest(t, svc) + makeReadyTransferForTest(t, svc, create.TransferID, create.ManageToken) + + _, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.20", create.TransferID, DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 99}}, + }) + requireHTTPError(t, err, 400, "invalid_download_request") + }) + + t.Run("returns presigned url for uploaded chunk", func(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + create := createTransferForTest(t, svc) + makeReadyTransferForTest(t, svc, create.TransferID, create.ManageToken) + + items, err := svc.CreateDownloadURLs(context.Background(), "198.51.100.20", create.TransferID, DownloadURLRequest{ + Chunks: []UploadChunkRequest{{FileID: "file-a", ChunkIndex: 1}}, + }) + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, "file-a", items[0].FileID) + require.Equal(t, 1, items[0].ChunkIndex) + require.Contains(t, items[0].URL, chunkObjectKey(create.TransferID, "file-a", 1)) + }) +} + +func createTransferForTest(t *testing.T, svc *Service) CreateTransferResponse { + t.Helper() + + response, err := svc.CreateTransfer(context.Background(), "198.51.100.10", CreateTransferRequest{ + ExpiresInSeconds: 3600, + }) + require.NoError(t, err) + return response +} + +func registerFileForTest(t *testing.T, svc *Service, transferID string, manageToken string) { + t.Helper() + + err := svc.RegisterFiles(context.Background(), transferID, manageToken, []RegisterFileRequest{{ + FileID: "file-a", + TotalChunks: 2, + CiphertextBytes: 64, + PlaintextBytes: int64ptr(32), + ChunkSize: 32, + }}) + require.NoError(t, err) +} + +func makeReadyTransferForTest(t *testing.T, svc *Service, transferID string, manageToken string) { + t.Helper() + + registerFileForTest(t, svc, transferID, manageToken) + + err := svc.CompleteChunks(context.Background(), transferID, manageToken, []CompleteChunkRequest{ + {FileID: "file-a", ChunkIndex: 0, CiphertextSize: 32, ChecksumSHA256: "a"}, + {FileID: "file-a", ChunkIndex: 1, CiphertextSize: 32, ChecksumSHA256: "b"}, + }) + require.NoError(t, err) + + err = svc.PutManifest(context.Background(), transferID, manageToken, ManifestUploadRequest{ + CiphertextBase64: base64.StdEncoding.EncodeToString([]byte(`{"version":1}`)), + }) + require.NoError(t, err) + + err = svc.FinalizeTransfer(context.Background(), transferID, manageToken, FinalizeTransferRequest{ + WrappedRootKey: `{"version":1}`, + TotalFiles: 1, + TotalCiphertextBytes: 64, + }) + require.NoError(t, err) +} + +func expireTransfer(t *testing.T, repository *memoryRepository, transferID string) { + t.Helper() + + transfer, err := repository.GetTransfer(context.Background(), transferID) + require.NoError(t, err) + transfer.ExpiresAt = time.Now().UTC().Add(-time.Minute) + require.NoError(t, repository.CreateOrReplace(transfer)) +} + +func requireHTTPError(t *testing.T, err error, status int, code string) { + t.Helper() + + require.Error(t, err) + + httpErr, ok := err.(*HTTPError) + require.True(t, ok) + require.Equal(t, status, httpErr.Status) + require.Equal(t, code, httpErr.Code) +} diff --git a/apps/api/internal/service/transfer_service_test.go b/apps/api/internal/service/transfer_service_test.go new file mode 100644 index 0000000..f4ad1a2 --- /dev/null +++ b/apps/api/internal/service/transfer_service_test.go @@ -0,0 +1,374 @@ +package service + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xdrop/monorepo/internal/config" + "github.com/xdrop/monorepo/internal/models" + "github.com/xdrop/monorepo/internal/ratelimit" + "github.com/xdrop/monorepo/internal/repo" +) + +func TestCreateTransferStoresHashedManageToken(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + + response, err := svc.CreateTransfer(context.Background(), "127.0.0.1", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.NoError(t, err) + require.NotEmpty(t, response.TransferID) + require.NotEmpty(t, response.ManageToken) + + transfer, err := repository.GetTransfer(context.Background(), response.TransferID) + require.NoError(t, err) + require.NotEqual(t, response.ManageToken, transfer.ManageTokenHash) + require.Len(t, transfer.ManageTokenHash, 64) +} + +func TestCreateTransferRejectsUnsupportedExpiry(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + svc := newTestService(repository) + + _, err := svc.CreateTransfer(context.Background(), "127.0.0.1", CreateTransferRequest{ExpiresInSeconds: 2 * 3600}) + require.Error(t, err) + + httpErr, ok := err.(*HTTPError) + require.True(t, ok) + require.Equal(t, 400, httpErr.Status) + require.Equal(t, "invalid_expiry", httpErr.Code) +} + +func TestFinalizeFlowPublishesDescriptor(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + + create, err := svc.CreateTransfer(context.Background(), "127.0.0.1", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.NoError(t, err) + + err = svc.RegisterFiles(context.Background(), create.TransferID, create.ManageToken, []RegisterFileRequest{ + { + FileID: "file_a", + TotalChunks: 2, + CiphertextBytes: 64, + PlaintextBytes: int64ptr(32), + ChunkSize: 16, + }, + }) + require.NoError(t, err) + + err = svc.CompleteChunks(context.Background(), create.TransferID, create.ManageToken, []CompleteChunkRequest{ + {FileID: "file_a", ChunkIndex: 0, CiphertextSize: 32, ChecksumSHA256: strings.Repeat("a", 64)}, + {FileID: "file_a", ChunkIndex: 1, CiphertextSize: 32, ChecksumSHA256: strings.Repeat("b", 64)}, + }) + require.NoError(t, err) + + ciphertext := base64.StdEncoding.EncodeToString([]byte(`{"version":1}`)) + err = svc.PutManifest(context.Background(), create.TransferID, create.ManageToken, ManifestUploadRequest{ + CiphertextBase64: ciphertext, + }) + require.NoError(t, err) + + err = svc.FinalizeTransfer(context.Background(), create.TransferID, create.ManageToken, FinalizeTransferRequest{ + WrappedRootKey: `{"version":1}`, + TotalFiles: 1, + TotalCiphertextBytes: 64, + }) + require.NoError(t, err) + + publicDescriptor, err := svc.GetPublicTransfer(context.Background(), "127.0.0.1", create.TransferID) + require.NoError(t, err) + require.Equal(t, "ready", publicDescriptor.Status) + require.NotEmpty(t, publicDescriptor.ManifestURL) + require.Equal(t, `{"version":1}`, publicDescriptor.WrappedRootKey) +} + +func TestRateLimitBlocksSecondCreate(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + cfg := testConfig() + cfg.CreateLimit = 1 + svc := New(cfg, repository, &memoryStorage{}, ratelimit.NewMemoryLimiter()) + + _, err := svc.CreateTransfer(context.Background(), "shared-ip", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.NoError(t, err) + + _, err = svc.CreateTransfer(context.Background(), "shared-ip", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.Error(t, err) + + httpErr, ok := err.(*HTTPError) + require.True(t, ok) + require.Equal(t, 429, httpErr.Status) +} + +func TestDeleteMakesTransferUnavailable(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + + create, err := svc.CreateTransfer(context.Background(), "127.0.0.1", CreateTransferRequest{ExpiresInSeconds: 3600}) + require.NoError(t, err) + + readyTransfer, err := repository.GetTransfer(context.Background(), create.TransferID) + require.NoError(t, err) + readyTransfer.Status = models.TransferStatusReady + readyTransfer.ManifestObjectKey = "transfers/demo/manifest.bin" + readyTransfer.WrappedRootKey = `{"version":1}` + require.NoError(t, repository.CreateOrReplace(readyTransfer)) + + err = svc.DeleteTransfer(context.Background(), create.TransferID, create.ManageToken) + require.NoError(t, err) + + publicDescriptor, err := svc.GetPublicTransfer(context.Background(), "127.0.0.1", create.TransferID) + require.NoError(t, err) + require.Equal(t, "deleted", publicDescriptor.Status) +} + +func TestCleanupRemovesExpiredPrefixes(t *testing.T) { + t.Parallel() + + repository := newMemoryRepository() + storage := &memoryStorage{} + svc := newTestServiceWithStorage(repository, storage) + + expired := models.Transfer{ + ID: "expired-transfer", + Status: models.TransferStatusReady, + ManageTokenHash: hashToken("manage-token"), + CreatedAt: time.Now().Add(-48 * time.Hour), + UpdatedAt: time.Now().Add(-48 * time.Hour), + ExpiresAt: time.Now().Add(-24 * time.Hour), + ManifestObjectKey: "transfers/expired-transfer/manifest.bin", + WrappedRootKey: `{"version":1}`, + } + require.NoError(t, repository.CreateOrReplace(expired)) + + err := svc.CleanupExpired(context.Background()) + require.NoError(t, err) + require.Contains(t, storage.deletedPrefixes, "transfers/expired-transfer/") + + transfer, err := repository.GetTransfer(context.Background(), expired.ID) + require.NoError(t, err) + require.NotNil(t, transfer.PurgedAt) +} + +type memoryRepository struct { + transfers map[string]models.Transfer + files map[string][]models.TransferFile + chunks map[string][]models.TransferChunk +} + +func newMemoryRepository() *memoryRepository { + return &memoryRepository{ + transfers: map[string]models.Transfer{}, + files: map[string][]models.TransferFile{}, + chunks: map[string][]models.TransferChunk{}, + } +} + +func (m *memoryRepository) CreateTransfer(_ context.Context, transfer models.Transfer) error { + m.transfers[transfer.ID] = transfer + return nil +} + +func (m *memoryRepository) CreateOrReplace(transfer models.Transfer) error { + m.transfers[transfer.ID] = transfer + return nil +} + +func (m *memoryRepository) GetTransfer(_ context.Context, transferID string) (models.Transfer, error) { + transfer, ok := m.transfers[transferID] + if !ok { + return models.Transfer{}, repo.ErrNotFound + } + return transfer, nil +} + +func (m *memoryRepository) RegisterFiles(_ context.Context, transferID string, files []models.TransferFile) error { + m.files[transferID] = append([]models.TransferFile{}, files...) + transfer := m.transfers[transferID] + transfer.Status = models.TransferStatusUploading + m.transfers[transferID] = transfer + return nil +} + +func (m *memoryRepository) ListFiles(_ context.Context, transferID string) ([]models.TransferFile, error) { + return append([]models.TransferFile{}, m.files[transferID]...), nil +} + +func (m *memoryRepository) CompleteChunks(_ context.Context, transferID string, chunks []models.TransferChunk) error { + m.chunks[transferID] = append(m.chunks[transferID], chunks...) + files := m.files[transferID] + for fileIndex := range files { + uploaded := 0 + for _, chunk := range m.chunks[transferID] { + if chunk.OpaqueFileID == files[fileIndex].OpaqueFileID { + uploaded++ + } + } + if uploaded >= files[fileIndex].TotalChunks { + files[fileIndex].UploadStatus = "complete" + } + } + m.files[transferID] = files + return nil +} + +func (m *memoryRepository) GetResumeState(ctx context.Context, transferID string) (models.TransferResumeState, error) { + transfer, err := m.GetTransfer(ctx, transferID) + if err != nil { + return models.TransferResumeState{}, err + } + uploaded := map[string][]int{} + for _, chunk := range m.chunks[transferID] { + uploaded[chunk.OpaqueFileID] = append(uploaded[chunk.OpaqueFileID], chunk.ChunkIndex) + } + return models.TransferResumeState{ + Transfer: transfer, + Files: append([]models.TransferFile{}, m.files[transferID]...), + UploadedChunks: uploaded, + }, nil +} + +func (m *memoryRepository) SetManifest(_ context.Context, transferID string, objectKey string, ciphertextSize int64) error { + transfer := m.transfers[transferID] + transfer.ManifestObjectKey = objectKey + transfer.ManifestCiphertextSize = ciphertextSize + m.transfers[transferID] = transfer + return nil +} + +func (m *memoryRepository) FinalizeTransfer(_ context.Context, transferID string, wrappedRootKey string, totalFiles int, totalCiphertextBytes int64) error { + transfer := m.transfers[transferID] + if transfer.ManifestObjectKey == "" { + return fmt.Errorf("manifest missing") + } + for _, file := range m.files[transferID] { + if file.UploadStatus != "complete" { + return fmt.Errorf("upload incomplete") + } + } + transfer.Status = models.TransferStatusReady + transfer.WrappedRootKey = wrappedRootKey + transfer.TotalFiles = totalFiles + transfer.TotalCiphertextBytes = totalCiphertextBytes + now := time.Now() + transfer.FinalizedAt = &now + m.transfers[transferID] = transfer + return nil +} + +func (m *memoryRepository) UpdateTransfer(_ context.Context, transferID string, params models.UpdateTransferParams) error { + transfer := m.transfers[transferID] + if params.ManifestObjectKey != nil { + transfer.ManifestObjectKey = *params.ManifestObjectKey + } + if params.ExpiresAt != nil { + transfer.ExpiresAt = *params.ExpiresAt + } + if params.ManifestCiphertextSize != nil { + transfer.ManifestCiphertextSize = *params.ManifestCiphertextSize + } + m.transfers[transferID] = transfer + return nil +} + +func (m *memoryRepository) MarkDeleted(_ context.Context, transferID string) error { + transfer := m.transfers[transferID] + now := time.Now() + transfer.Status = models.TransferStatusDeleted + transfer.DeletedAt = &now + m.transfers[transferID] = transfer + return nil +} + +func (m *memoryRepository) ListCleanupCandidates(_ context.Context, _ int) ([]models.Transfer, error) { + candidates := []models.Transfer{} + for _, transfer := range m.transfers { + if transfer.PurgedAt != nil { + continue + } + if transfer.DeletedAt != nil || transfer.ExpiresAt.Before(time.Now()) { + candidates = append(candidates, transfer) + } + } + return candidates, nil +} + +func (m *memoryRepository) MarkPurged(_ context.Context, transferID string) error { + transfer := m.transfers[transferID] + now := time.Now() + transfer.PurgedAt = &now + transfer.Status = models.TransferStatusExpired + m.transfers[transferID] = transfer + return nil +} + +type memoryStorage struct { + objects map[string][]byte + deletedPrefixes []string +} + +func (m *memoryStorage) PresignUpload(_ context.Context, objectKey string, _ time.Duration) (string, error) { + return "https://example.test/upload/" + objectKey, nil +} + +func (m *memoryStorage) PresignDownload(_ context.Context, objectKey string, _ time.Duration) (string, error) { + return "https://example.test/download/" + objectKey, nil +} + +func (m *memoryStorage) PutObject(_ context.Context, objectKey string, body []byte, _ string) error { + if m.objects == nil { + m.objects = map[string][]byte{} + } + m.objects[objectKey] = append([]byte{}, body...) + return nil +} + +func (m *memoryStorage) DeletePrefix(_ context.Context, prefix string) error { + m.deletedPrefixes = append(m.deletedPrefixes, prefix) + return nil +} + +func (m *memoryStorage) EnsureBucket(_ context.Context) error { + return nil +} + +func newTestService(repository *memoryRepository) *Service { + return newTestServiceWithStorage(repository, &memoryStorage{}) +} + +func newTestServiceWithStorage(repository *memoryRepository, storage *memoryStorage) *Service { + return New(testConfig(), repository, storage, ratelimit.NewMemoryLimiter()) +} + +func testConfig() config.Config { + return config.Config{ + ChunkSize: 8 * 1024 * 1024, + DefaultExpiry: time.Hour, + CreateLimit: 20, + PublicReadLimit: 120, + DownloadURLLimit: 120, + PresignTTL: 5 * time.Minute, + MaxFileCount: 100, + MaxTransferBytes: 256 * 1024 * 1024, + } +} + +func int64ptr(value int64) *int64 { + return &value +} diff --git a/apps/api/internal/storage/s3.go b/apps/api/internal/storage/s3.go new file mode 100644 index 0000000..27a1d0c --- /dev/null +++ b/apps/api/internal/storage/s3.go @@ -0,0 +1,246 @@ +package storage + +import ( + "bytes" + "context" + "fmt" + "io" + "net/url" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +// ObjectStorage defines the storage operations the service layer needs for transfer objects. +type ObjectStorage interface { + PresignUpload(ctx context.Context, objectKey string, ttl time.Duration) (string, error) + PresignDownload(ctx context.Context, objectKey string, ttl time.Duration) (string, error) + PutObject(ctx context.Context, objectKey string, body []byte, contentType string) error + DeletePrefix(ctx context.Context, prefix string) error + EnsureBucket(ctx context.Context) error +} + +type s3Client interface { + s3.ListObjectsV2APIClient + CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) + DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) + GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) + HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error) +} + +type s3Presigner interface { + PresignGetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) + PresignPutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) +} + +type s3Uploader interface { + Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) +} + +type listObjectsV2Paginator interface { + HasMorePages() bool + NextPage(ctx context.Context, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) +} + +// S3Storage stores transfer objects in an S3-compatible bucket. +type S3Storage struct { + bucket string + client s3Client + presigner s3Presigner + uploader s3Uploader + paginator func(client s3.ListObjectsV2APIClient, input *s3.ListObjectsV2Input) listObjectsV2Paginator +} + +// Config describes how to connect to the private and public S3-compatible endpoints. +type Config struct { + Endpoint string + PublicEndpoint string + Region string + Bucket string + AccessKey string + SecretKey string + UseSSL bool +} + +var loadDefaultAWSConfig = awsconfig.LoadDefaultConfig + +// NewS3Storage builds an S3-backed object storage adapter with optional public presign endpoint. +func NewS3Storage(ctx context.Context, cfg Config) (*S3Storage, error) { + endpoint := cfg.Endpoint + awsCfg, err := loadDefaultAWSConfig( + ctx, + awsconfig.WithRegion(cfg.Region), + awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")), + awsconfig.WithBaseEndpoint(endpoint), + ) + if err != nil { + return nil, fmt.Errorf("load aws config: %w", err) + } + + client := s3.NewFromConfig(awsCfg, func(options *s3.Options) { + options.UsePathStyle = true + }) + presignClient := client + + if cfg.PublicEndpoint != "" && cfg.PublicEndpoint != cfg.Endpoint { + publicCfg, configErr := loadDefaultAWSConfig( + ctx, + awsconfig.WithRegion(cfg.Region), + awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")), + awsconfig.WithBaseEndpoint(cfg.PublicEndpoint), + ) + if configErr != nil { + return nil, fmt.Errorf("load public aws config: %w", configErr) + } + presignClient = s3.NewFromConfig(publicCfg, func(options *s3.Options) { + options.UsePathStyle = true + }) + } + + return &S3Storage{ + bucket: cfg.Bucket, + client: client, + presigner: s3.NewPresignClient(presignClient), + uploader: manager.NewUploader(client), + paginator: func(client s3.ListObjectsV2APIClient, input *s3.ListObjectsV2Input) listObjectsV2Paginator { + return s3.NewListObjectsV2Paginator(client, input) + }, + }, nil +} + +// EnsureBucket creates the bucket if it does not already exist. +func (s *S3Storage) EnsureBucket(ctx context.Context) error { + _, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(s.bucket)}) + if err == nil { + return nil + } + + _, err = s.client.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: aws.String(s.bucket), + }) + if err != nil && !strings.Contains(strings.ToLower(err.Error()), "bucketalreadyownedbyyou") { + return fmt.Errorf("create bucket: %w", err) + } + + return nil +} + +// PresignUpload returns a time-limited PUT URL for one object key. +func (s *S3Storage) PresignUpload(ctx context.Context, objectKey string, ttl time.Duration) (string, error) { + result, err := s.presigner.PresignPutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(objectKey), + }, s3.WithPresignExpires(ttl)) + if err != nil { + return "", fmt.Errorf("presign upload: %w", err) + } + + return result.URL, nil +} + +// PresignDownload returns a time-limited GET URL for one object key. +func (s *S3Storage) PresignDownload(ctx context.Context, objectKey string, ttl time.Duration) (string, error) { + result, err := s.presigner.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(objectKey), + }, s3.WithPresignExpires(ttl)) + if err != nil { + return "", fmt.Errorf("presign download: %w", err) + } + + return result.URL, nil +} + +// PutObject uploads a complete object payload with the provided content type. +func (s *S3Storage) PutObject(ctx context.Context, objectKey string, body []byte, contentType string) error { + reader := bytes.NewReader(body) + + _, err := s.uploader.Upload(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(objectKey), + Body: reader, + ContentType: aws.String(contentType), + }) + if err != nil { + return fmt.Errorf("upload object: %w", err) + } + + return nil +} + +// DeletePrefix removes every object currently stored beneath a transfer prefix. +func (s *S3Storage) DeletePrefix(ctx context.Context, prefix string) error { + paginator := s.paginator(s.client, &s3.ListObjectsV2Input{ + Bucket: aws.String(s.bucket), + Prefix: aws.String(prefix), + }) + + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return fmt.Errorf("list objects: %w", err) + } + + if len(page.Contents) == 0 { + continue + } + + objects := make([]s3typesObjectIdentifier, 0, len(page.Contents)) + for _, item := range page.Contents { + if item.Key == nil { + continue + } + objects = append(objects, s3typesObjectIdentifier{Key: item.Key}) + } + + if len(objects) == 0 { + continue + } + + deleteObjects := make([]s3types.ObjectIdentifier, 0, len(objects)) + for _, object := range objects { + deleteObjects = append(deleteObjects, s3types.ObjectIdentifier{Key: object.Key}) + } + + _, err = s.client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: aws.String(s.bucket), + Delete: &s3types.Delete{Objects: deleteObjects}, + }) + if err != nil { + return fmt.Errorf("delete objects: %w", err) + } + } + + return nil +} + +type s3typesObjectIdentifier struct { + Key *string +} + +// NormalizeEndpoint ensures user-supplied endpoints always parse as full URLs. +func NormalizeEndpoint(raw string) (string, error) { + value := strings.TrimSpace(raw) + if !strings.Contains(value, "://") { + value = "http://" + value + } + + parsed, err := url.Parse(value) + if err != nil { + return "", fmt.Errorf("parse endpoint: %w", err) + } + + return parsed.String(), nil +} + +// ReadAll is kept as a shim so tests can stub object reads without importing io directly. +func ReadAll(reader io.Reader) ([]byte, error) { + return io.ReadAll(reader) +} diff --git a/apps/api/internal/storage/s3_integration_test.go b/apps/api/internal/storage/s3_integration_test.go new file mode 100644 index 0000000..cb0fffe --- /dev/null +++ b/apps/api/internal/storage/s3_integration_test.go @@ -0,0 +1,172 @@ +package storage + +import ( + "context" + "fmt" + "os/exec" + "runtime" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestS3StorageLifecycleAgainstMinIO(t *testing.T) { + skipIfDockerUnavailable(t) + + ctx := context.Background() + cfg := startMinIOConfig(t, ctx) + + store, err := NewS3Storage(ctx, cfg) + require.NoError(t, err) + + require.NoError(t, store.EnsureBucket(ctx)) + require.NoError(t, store.EnsureBucket(ctx)) + + require.NoError(t, store.PutObject(ctx, "transfers/demo/manifest.bin", []byte("manifest"), "application/octet-stream")) + require.NoError(t, store.PutObject(ctx, "transfers/demo/files/file-a/chunks/00000000.bin", []byte("chunk"), "application/octet-stream")) + require.NoError(t, store.PutObject(ctx, "transfers/other/manifest.bin", []byte("other"), "application/octet-stream")) + + objectBody := getObjectBody(t, ctx, store, "transfers/demo/manifest.bin") + require.Equal(t, []byte("manifest"), objectBody) + + uploadURL, err := store.PresignUpload(ctx, "transfers/demo/files/file-a/chunks/00000001.bin", 5*time.Minute) + require.NoError(t, err) + require.Contains(t, uploadURL, "X-Amz-Algorithm=") + require.Contains(t, uploadURL, "X-Amz-Signature=") + require.Contains(t, uploadURL, cfg.PublicEndpoint) + + downloadURL, err := store.PresignDownload(ctx, "transfers/demo/manifest.bin", 5*time.Minute) + require.NoError(t, err) + require.Contains(t, downloadURL, "X-Amz-Algorithm=") + require.Contains(t, downloadURL, cfg.PublicEndpoint) + + require.NoError(t, store.DeletePrefix(ctx, "transfers/demo/")) + + keys := listObjectKeys(t, ctx, store) + require.NotContains(t, keys, "transfers/demo/manifest.bin") + require.NotContains(t, keys, "transfers/demo/files/file-a/chunks/00000000.bin") + require.Contains(t, keys, "transfers/other/manifest.bin") +} + +func TestNormalizeEndpointHandlesSchemeAndWhitespace(t *testing.T) { + t.Parallel() + + normalized, err := NormalizeEndpoint(" localhost:9000 ") + require.NoError(t, err) + require.Equal(t, "http://localhost:9000", normalized) + + normalized, err = NormalizeEndpoint("https://minio.example.test") + require.NoError(t, err) + require.Equal(t, "https://minio.example.test", normalized) +} + +func TestNormalizeEndpointRejectsInvalidURL(t *testing.T) { + t.Parallel() + + _, err := NormalizeEndpoint("http://%zz") + require.Error(t, err) +} + +func TestReadAllReadsEntireStream(t *testing.T) { + t.Parallel() + + body, err := ReadAll(strings.NewReader("hello")) + require.NoError(t, err) + require.Equal(t, []byte("hello"), body) +} + +func startMinIOConfig(t *testing.T, ctx context.Context) Config { + t.Helper() + + container, err := testcontainers.Run( + ctx, + "minio/minio:latest", + testcontainers.WithEnv(map[string]string{ + "MINIO_ROOT_USER": "minioadmin", + "MINIO_ROOT_PASSWORD": "minioadmin", + }), + testcontainers.WithExposedPorts("9000/tcp"), + testcontainers.WithCmd("server", "/data"), + testcontainers.WithWaitStrategy( + wait.ForHTTP("/minio/health/live"). + WithPort("9000/tcp"). + WithStartupTimeout(90*time.Second), + ), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, testcontainers.TerminateContainer(container)) + }) + + host, err := container.Host(ctx) + require.NoError(t, err) + port, err := container.MappedPort(ctx, "9000/tcp") + require.NoError(t, err) + + internalEndpoint := fmt.Sprintf("http://%s:%s", host, port.Port()) + return Config{ + Endpoint: internalEndpoint, + PublicEndpoint: "http://public.example.test:9000", + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "minioadmin", + SecretKey: "minioadmin", + } +} + +func getObjectBody(t *testing.T, ctx context.Context, store *S3Storage, objectKey string) []byte { + t.Helper() + + response, err := store.client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(store.bucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err) + defer response.Body.Close() + + body, err := ReadAll(response.Body) + require.NoError(t, err) + return body +} + +func listObjectKeys(t *testing.T, ctx context.Context, store *S3Storage) []string { + t.Helper() + + paginator := s3.NewListObjectsV2Paginator(store.client, &s3.ListObjectsV2Input{ + Bucket: aws.String(store.bucket), + }) + + keys := []string{} + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + require.NoError(t, err) + for _, item := range page.Contents { + if item.Key != nil { + keys = append(keys, *item.Key) + } + } + } + + return keys +} + +func skipIfDockerUnavailable(t *testing.T) { + t.Helper() + + if testing.Short() { + t.Skip("skipping docker-backed integration test in short mode") + } + if runtime.GOOS == "windows" { + t.Skip("skipping docker-backed integration test on windows") + } + + if err := exec.Command("docker", "info").Run(); err != nil { + t.Skipf("skipping docker-backed integration test: %v", err) + } +} diff --git a/apps/api/internal/storage/s3_unit_test.go b/apps/api/internal/storage/s3_unit_test.go new file mode 100644 index 0000000..de59535 --- /dev/null +++ b/apps/api/internal/storage/s3_unit_test.go @@ -0,0 +1,465 @@ +package storage + +import ( + "context" + "errors" + "io" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/stretchr/testify/require" +) + +func TestNewS3StoragePresignsAgainstConfiguredEndpoints(t *testing.T) { + ctx := context.Background() + store, err := NewS3Storage(ctx, Config{ + Endpoint: "http://internal.example.test:9000", + PublicEndpoint: "http://public.example.test:9000", + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "key", + SecretKey: "secret", + }) + require.NoError(t, err) + + uploadURL, err := store.PresignUpload(ctx, "transfers/demo/object.bin", time.Minute) + require.NoError(t, err) + require.Contains(t, uploadURL, "public.example.test:9000") + + downloadURL, err := store.PresignDownload(ctx, "transfers/demo/object.bin", time.Minute) + require.NoError(t, err) + require.Contains(t, downloadURL, "public.example.test:9000") + + internalStore, err := NewS3Storage(ctx, Config{ + Endpoint: "http://internal.example.test:9000", + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "key", + SecretKey: "secret", + }) + require.NoError(t, err) + + uploadURL, err = internalStore.PresignUpload(ctx, "transfers/demo/object.bin", time.Minute) + require.NoError(t, err) + require.Contains(t, uploadURL, "internal.example.test:9000") + + t.Run("surfaces aws config load failures", func(t *testing.T) { + originalLoader := loadDefaultAWSConfig + t.Cleanup(func() { + loadDefaultAWSConfig = originalLoader + }) + loadDefaultAWSConfig = func(context.Context, ...func(*awsconfig.LoadOptions) error) (aws.Config, error) { + return aws.Config{}, errors.New("boom") + } + + _, err := NewS3Storage(ctx, Config{ + Endpoint: "http://internal.example.test:9000", + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "key", + SecretKey: "secret", + }) + require.ErrorContains(t, err, "load aws config") + }) + + t.Run("surfaces public aws config load failures", func(t *testing.T) { + originalLoader := loadDefaultAWSConfig + t.Cleanup(func() { + loadDefaultAWSConfig = originalLoader + }) + callCount := 0 + loadDefaultAWSConfig = func(innerCtx context.Context, opts ...func(*awsconfig.LoadOptions) error) (aws.Config, error) { + callCount++ + if callCount == 2 { + return aws.Config{}, errors.New("boom") + } + return originalLoader(innerCtx, opts...) + } + + _, err := NewS3Storage(ctx, Config{ + Endpoint: "http://internal.example.test:9000", + PublicEndpoint: "http://public.example.test:9000", + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "key", + SecretKey: "secret", + }) + require.ErrorContains(t, err, "load public aws config") + }) + + t.Run("initializes the default paginator factory", func(t *testing.T) { + t.Parallel() + + store, err := NewS3Storage(ctx, Config{ + Endpoint: "http://internal.example.test:9000", + Region: "us-east-1", + Bucket: "xdrop", + AccessKey: "key", + SecretKey: "secret", + }) + require.NoError(t, err) + require.NotNil(t, store.paginator) + require.NotNil(t, store.paginator(store.client, &s3.ListObjectsV2Input{ + Bucket: aws.String("xdrop"), + })) + }) +} + +func TestS3StorageEnsureBucket(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("head bucket success", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + headBucketFn: func(context.Context, *s3.HeadBucketInput, ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { + return &s3.HeadBucketOutput{}, nil + }, + }, + } + + require.NoError(t, store.EnsureBucket(ctx)) + }) + + t.Run("create bucket on missing bucket", func(t *testing.T) { + createCalls := 0 + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + headBucketFn: func(context.Context, *s3.HeadBucketInput, ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { + return nil, errors.New("missing") + }, + createBucketFn: func(context.Context, *s3.CreateBucketInput, ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { + createCalls++ + return &s3.CreateBucketOutput{}, nil + }, + }, + } + + require.NoError(t, store.EnsureBucket(ctx)) + require.Equal(t, 1, createCalls) + }) + + t.Run("bucket already owned is ignored", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + headBucketFn: func(context.Context, *s3.HeadBucketInput, ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { + return nil, errors.New("missing") + }, + createBucketFn: func(context.Context, *s3.CreateBucketInput, ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { + return nil, errors.New("BucketAlreadyOwnedByYou") + }, + }, + } + + require.NoError(t, store.EnsureBucket(ctx)) + }) + + t.Run("create bucket failure", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + headBucketFn: func(context.Context, *s3.HeadBucketInput, ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { + return nil, errors.New("missing") + }, + createBucketFn: func(context.Context, *s3.CreateBucketInput, ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { + return nil, errors.New("boom") + }, + }, + } + + err := store.EnsureBucket(ctx) + require.ErrorContains(t, err, "create bucket") + }) +} + +func TestS3StoragePresignAndUploadOperations(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("presign upload success and error", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + presigner: fakePresigner{ + presignPutObjectFn: func(context.Context, *s3.PutObjectInput, ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + return &v4.PresignedHTTPRequest{URL: "http://upload.example.test"}, nil + }, + }, + } + + url, err := store.PresignUpload(ctx, "object.bin", time.Minute) + require.NoError(t, err) + require.Equal(t, "http://upload.example.test", url) + + store.presigner = fakePresigner{ + presignPutObjectFn: func(context.Context, *s3.PutObjectInput, ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + return nil, errors.New("boom") + }, + } + _, err = store.PresignUpload(ctx, "object.bin", time.Minute) + require.ErrorContains(t, err, "presign upload") + }) + + t.Run("presign download success and error", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + presigner: fakePresigner{ + presignGetObjectFn: func(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + return &v4.PresignedHTTPRequest{URL: "http://download.example.test"}, nil + }, + }, + } + + url, err := store.PresignDownload(ctx, "object.bin", time.Minute) + require.NoError(t, err) + require.Equal(t, "http://download.example.test", url) + + store.presigner = fakePresigner{ + presignGetObjectFn: func(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + return nil, errors.New("boom") + }, + } + _, err = store.PresignDownload(ctx, "object.bin", time.Minute) + require.ErrorContains(t, err, "presign download") + }) + + t.Run("put object success and error", func(t *testing.T) { + body := []byte("payload") + store := S3Storage{ + bucket: "xdrop", + uploader: fakeUploader{ + uploadFn: func(_ context.Context, input *s3.PutObjectInput, _ ...func(*manager.Uploader)) (*manager.UploadOutput, error) { + uploaded, err := io.ReadAll(input.Body) + require.NoError(t, err) + require.Equal(t, body, uploaded) + require.Equal(t, "application/octet-stream", aws.ToString(input.ContentType)) + return &manager.UploadOutput{}, nil + }, + }, + } + + require.NoError(t, store.PutObject(ctx, "object.bin", body, "application/octet-stream")) + + store.uploader = fakeUploader{ + uploadFn: func(context.Context, *s3.PutObjectInput, ...func(*manager.Uploader)) (*manager.UploadOutput, error) { + return nil, errors.New("boom") + }, + } + err := store.PutObject(ctx, "object.bin", body, "application/octet-stream") + require.ErrorContains(t, err, "upload object") + }) +} + +func TestS3StorageDeletePrefix(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("list error", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{}, + paginator: func(s3.ListObjectsV2APIClient, *s3.ListObjectsV2Input) listObjectsV2Paginator { + return &fakePaginator{nextPageErr: errors.New("boom"), remaining: 1} + }, + } + + err := store.DeletePrefix(ctx, "transfers/demo/") + require.ErrorContains(t, err, "list objects") + }) + + t.Run("delete error", func(t *testing.T) { + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + deleteObjectsFn: func(context.Context, *s3.DeleteObjectsInput, ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) { + return nil, errors.New("boom") + }, + }, + paginator: func(s3.ListObjectsV2APIClient, *s3.ListObjectsV2Input) listObjectsV2Paginator { + return &fakePaginator{ + pages: []*s3.ListObjectsV2Output{{ + Contents: []s3types.Object{{Key: aws.String("transfers/demo/one.bin")}}, + }}, + remaining: 1, + } + }, + } + + err := store.DeletePrefix(ctx, "transfers/demo/") + require.ErrorContains(t, err, "delete objects") + }) + + t.Run("success skips empty pages and nil keys", func(t *testing.T) { + deletedKeys := []string{} + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + deleteObjectsFn: func(_ context.Context, input *s3.DeleteObjectsInput, _ ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) { + for _, object := range input.Delete.Objects { + deletedKeys = append(deletedKeys, aws.ToString(object.Key)) + } + return &s3.DeleteObjectsOutput{}, nil + }, + }, + paginator: func(s3.ListObjectsV2APIClient, *s3.ListObjectsV2Input) listObjectsV2Paginator { + return &fakePaginator{ + pages: []*s3.ListObjectsV2Output{ + {}, + { + Contents: []s3types.Object{ + {}, + {Key: aws.String("transfers/demo/one.bin")}, + {Key: aws.String("transfers/demo/two.bin")}, + }, + }, + }, + remaining: 2, + } + }, + } + + require.NoError(t, store.DeletePrefix(ctx, "transfers/demo/")) + require.Equal(t, []string{"transfers/demo/one.bin", "transfers/demo/two.bin"}, deletedKeys) + }) + + t.Run("skips delete calls when a page only has nil keys", func(t *testing.T) { + deleteCalls := 0 + store := S3Storage{ + bucket: "xdrop", + client: fakeS3Client{ + deleteObjectsFn: func(context.Context, *s3.DeleteObjectsInput, ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) { + deleteCalls++ + return &s3.DeleteObjectsOutput{}, nil + }, + }, + paginator: func(s3.ListObjectsV2APIClient, *s3.ListObjectsV2Input) listObjectsV2Paginator { + return &fakePaginator{ + pages: []*s3.ListObjectsV2Output{{ + Contents: []s3types.Object{{}}, + }}, + remaining: 1, + } + }, + } + + require.NoError(t, store.DeletePrefix(ctx, "transfers/demo/")) + require.Zero(t, deleteCalls) + }) +} + +type fakeS3Client struct { + createBucketFn func(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) + deleteObjectsFn func(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) + getObjectFn func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) + headBucketFn func(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error) + listObjectsFn func(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) +} + +func (c fakeS3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { + if c.createBucketFn != nil { + return c.createBucketFn(ctx, params, optFns...) + } + return &s3.CreateBucketOutput{}, nil +} + +func (c fakeS3Client) DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) { + if c.deleteObjectsFn != nil { + return c.deleteObjectsFn(ctx, params, optFns...) + } + return &s3.DeleteObjectsOutput{}, nil +} + +func (c fakeS3Client) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + if c.getObjectFn != nil { + return c.getObjectFn(ctx, params, optFns...) + } + return &s3.GetObjectOutput{}, nil +} + +func (c fakeS3Client) HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { + if c.headBucketFn != nil { + return c.headBucketFn(ctx, params, optFns...) + } + return &s3.HeadBucketOutput{}, nil +} + +func (c fakeS3Client) ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) { + if c.listObjectsFn != nil { + return c.listObjectsFn(ctx, params, optFns...) + } + return &s3.ListObjectsV2Output{}, nil +} + +type fakePresigner struct { + presignGetObjectFn func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) + presignPutObjectFn func(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) +} + +func (p fakePresigner) PresignGetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if p.presignGetObjectFn != nil { + return p.presignGetObjectFn(ctx, params, optFns...) + } + return &v4.PresignedHTTPRequest{}, nil +} + +func (p fakePresigner) PresignPutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if p.presignPutObjectFn != nil { + return p.presignPutObjectFn(ctx, params, optFns...) + } + return &v4.PresignedHTTPRequest{}, nil +} + +type fakeUploader struct { + uploadFn func(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) +} + +func (u fakeUploader) Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) { + if u.uploadFn != nil { + return u.uploadFn(ctx, input, opts...) + } + return &manager.UploadOutput{}, nil +} + +type fakePaginator struct { + nextPageErr error + pages []*s3.ListObjectsV2Output + remaining int +} + +func (p *fakePaginator) HasMorePages() bool { + return p.remaining > 0 +} + +func (p *fakePaginator) NextPage(context.Context, ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) { + if p.nextPageErr != nil { + return nil, p.nextPageErr + } + page := p.pages[0] + p.pages = p.pages[1:] + p.remaining-- + return page, nil +} + +type errReader struct{} + +func (errReader) Read([]byte) (int, error) { + return 0, errors.New("boom") +} + +func TestReadAllPropagatesReaderErrors(t *testing.T) { + t.Parallel() + + _, err := ReadAll(errReader{}) + require.ErrorContains(t, err, "boom") +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..07ad785 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 0000000..434e4a6 --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['coverage', 'dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, + { + files: ['src/features/upload/TransferContext.tsx', 'src/features/upload/UploadStudio.tsx'], + rules: { + 'react-refresh/only-export-components': 'off', + }, + }, +]) diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..6ede3dd --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + Open Source Encrypted File Transfer in the Browser | Xdrop + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..2a7b77c --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,49 @@ +{ + "name": "@xdrop/web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build && bun ./scripts/generate-seo-assets.mjs", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", + "typecheck": "tsc --noEmit -p tsconfig.app.json" + }, + "dependencies": { + "@fontsource-variable/instrument-sans": "^5.2.8", + "@fontsource/fraunces": "^5.2.9", + "@xdrop/shared": "workspace:*", + "@zip.js/zip.js": "^2.8.10", + "idb": "^8.0.3", + "qrcode": "^1.5.4", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.9.6", + "zod": "^4.1.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.5.0", + "@types/qrcode": "^1.5.5", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^4.0.7", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.1.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "fake-indexeddb": "^6.2.2", + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.1", + "vitest": "^4.0.7" + } +} diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ee047f8a928f4beffdddee960f4aaba8d4e2ed GIT binary patch literal 24120 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

;^HcCFj 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 Hu0mjfTt}O{ literal 0 HcmV?d00001 diff --git a/apps/web/public/brand-lockup-horizontal.png b/apps/web/public/brand-lockup-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..711cee29efbd535f93466fd6345f2027a6046075 GIT binary patch literal 301747 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

X!gK$+RQR{a+&l%$ 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.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

a_Pz#FADRk5RL7cp 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

A-`qwaLS5^A#35APZM`i=~U(WimE>TJ%)PKGGw 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 kJ@8urUWD@Z;K~*F + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Xdrop + + + diff --git a/apps/web/public/brand-symbol-192.png b/apps/web/public/brand-symbol-192.png new file mode 100644 index 0000000000000000000000000000000000000000..27ad98d79e64e90624615316751b7eadf34f11ee GIT binary patch literal 26891 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 z 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 WyW8zA8Nm5u#v9! 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 literal 0 HcmV?d00001 diff --git a/apps/web/public/brand-symbol-maskable-512.png b/apps/web/public/brand-symbol-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..8f441c24c6531a9729751c8a9ee15510cd99c09c GIT binary patch literal 143224 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.GB=YRx60L?QZC6ywD^Axpzb+e1?oP^)Gb_< z67ipXhHsbMN3m<*k|bp76EoYD@l*0%qw1rwUtj2#SsuqNc3%n&>@Jd!yxQk`7SFJL z$Z(sI@4q6#b}@<1_|A=Z?YJu!l20tdM6O4&q<0htKDKF`qEu_S9^}@y)UT! zCu|_CN0z0-Y(eG#O-vwjog@Q)u=mzx)4d-)8h-Rr=SpeAubR@(xj)%VArhI_c*nJQ zv!`@7dj7syym{^ZNG|8c;V#dtP)OItjSQf5!rhY_>U0%>5SyH*i&S!F%^po$RT1E-?YF|JMufD0%e1wYedo)4RuAZ~sD! zZLsMnJ-g{ISHj*6?{G}tsP5Px){f~b$1dQnMBH_hKH;WF2T%>AW7gzwmK~(!G-z_N z7bj@Wp$$e0x!nsAg;Wd&RSBOY-ds>71SVe9o&PhF>cUA6*zh=ux$GwV0 zDNu7ms}>qSCS%!E4au`i-- zgabRf)yHgcsK6E;7;Nd__td`ha=`k-A{_w{tBPZR2X1k3(f*M5+u3Ia!ToFKhP`ZZd zsriTYJ@w-Wpxi-~5Ztn)R*5;iDZOi7U_0_tqDg!ZZvQ3gA3sswO?CUnARYcR??FKY5ptc~MRw?cNUgrFC( zn|}s>yJ+3O)LoICkB6bNGb=GQLl)|`_IH`&zH=_TKH*|FBZfpBB0NnAl3I2Opw6RR z#!g2&sXOF|F<9pPn95h2TyA*Rqmsi@{u$MT`<8mshKW}5$?R!b^jyyDbI{nPRKITZ zRbxu2PQTx6c|shwm4hRdCGJqpNIdDWVBoBjeyRet`gmjcMOjh{*OfLe#B@nr`8(S} zt2*zTlM~Z?V)V~|;g6#T7nkVYr?wZz#G-(7V8b(udt;|pD*Rqu8kZ@V)e=Cvzn4yE z#VkjSTFHdm-DCd<-~B5(iT~x_0o|{q_hnzH0#qX7?c@~aDe#}g6oGz=6m3tHQ@nprZ_qwOJ(N|!gz;_Nm3f54l@P zcVP?8O*QPLWah{Fs)Tz5f(Tz_P-QbU>wQ(KJW-`rprq5{D~7x$y>0UNf&^F<_`mQ; zMe#H0MV?0$I96nyV{ipcJZ3Ok>h%|*qrS1>4 zl6=#(XDR;MpvveE{=l^;8uf|&H*cL^4_VZ}nA{3*|sfRGK99fEIO z!xM^_D->+k$3jTxU5;i6W-_b(|BOLo{mN`EA>a-I&q93xjNXLhLq3<77D zq-<%YB!arR&o_G9=T3(CZN7gmbV7>zUY_vZ6Mgc|a=ntGGi1RP_l1`&Moy_I@m22S z&P`AU-|jud3*kOnBL zrqk#WQSgXtr2+GbDkTW)>}~I#CBbqC=mo{!gr9vYNS*SClhQ8tiX_ImY1_!J*>@Tu zyfpu@+CKii)Xc=*?H4M%yDa9F+*}$fCb&3gjGLXYaX7~BJ zS{JzD-z50h(??h9V#UTl$&YL)bX^3pl4D?K{VmpvrELEoAfJtrZ) z?J3A|6}Ws8j*$YC-siC(dMakAKr?t^{vpn@Ig-su)g5S|*M4yQRA6mHPS#H{tK=iw zsTlubrXg1&DYA!fPRaQ3x!v0&4`Ij8OT+Zd@+z<2dx{yVj3|IWYLTwzW4N+EynOdJ zkdTJxqPE}_WP#xPAye63cmp{2}aQGttT*?^$y;nwjuGR zA>ya*rNeX7ldcrqF)XDb?fTRTSJ})`d=!-lqCRRl?4yLm2vo)=CRQ7$ENP8nuUT_)-VHoPBt;yaWS^c}>nY~TCJCO?HGQt!nHg6W&VEOJ z!dsW|U8GSWa>#uQ%b4E8ccV_eZx??(3&{PnbT`#hZ@qZjSOOJj^Ai*+zgf5`*?G=8 zON<`-!+=TAkSrtwF~6E*(*-LNkNven4Vz+phYx!Fa0+@jLj0nX@sQN?V$1ga@I;cw5YgN0zGs3ZUTU`hin+9?`A>^*B>yFZ zJVot&JQlgSV9V|{?<=-!&EyCd32W0kpq!ICKC7HP#yIKVLC-3DiG~lvl+EZv^~}Nu zD zuw%oWYlrk;i%D8xo;`5~ZLc2gA8xD_(Lynl7P&mje~WXzmF1D};txk5wtFia3g@qf zDK;kxG5ARX3~~Uo_4(QDH!BeY!(_Fi;7)q1nSS)70JFcUtYM@WG<|zD-;;&4F~&K7 zqt)LYkJe0DBac=`UAzlT7`^OGQ-JP>;ZRkLwFdZcxxt*2gkShQBXevKIr{y;9shoa zGlnyEOu#vZ8&ifcrf@>>0X%IMf*#`hQ<5@BgVjo>RViE>(?X%GHxRom@MV4zDJ&Q;i^P`9o8xhqHR@q?atx=-sh^kK_1j-C~B zqu+Zl$-*{T5i0|G7#azEgX~$*90~ApZ#pw!HvDcGXYuyagDuX(4 zsJt#h$PXZ8eOX<#DA-XVH-i)9km+4~fG4GgorTzj@psrvocZ{SAmSGQ&p zxw&>&u{sp={hqQbUDopjv1?g!w_gBVC4X&RGTcbusR=3rxAxM4+fQ?6Q8zj=lj?>a zbJ`Jh7JM86!t z8)ew(Xs_^Dfy4kOLyB#i*wUrmsDCc+%U5Jsa~PEu zAWlDgkW* z8>s*%oE`B8{=MNrWc#FT93B5}_153&xlvI!G68#N6c^driPRF<*k%@D+aQ%+vb?y% zb4+iSPrB5;a_9eI`_QPi{*^leOpfZENn5|6Y89tP51>G|8L}b)V`_dbM)<7ID%O}W zuwA_`PR4RZhZadcK6HY&M1a*%j@knKRxQl$V6Yec0#~sv(g5j!q0sA8YeS$Tj>1N> zZ`lA}!j?tYTh6RB(7MpIP(sbiuT70=ousWZWawt6s8`OY*w7t8vEbjJ=ciON_D{(1 z=)qe>aRHwk)7mf1CZB_qWMnDrf1l|&MlJ4>f}GsIF?W?7|8bE9e$)!Oui?;k$+-2T zbeKHv>=6xng<-h6p-~e4LqCsi$|WUTTyZ@=A$UEx{C#!+T53TiYVOe%2@vA6^fspc zeE-=P=V}{}F_>Q2H?)kAP?cev-Y~h%amnp*b zS{g|i?!`oN?Ok1v<)WmtJirH=^W@;rcT2fk)0K$75D zeV;b(;`UTcbFpp}0h~>zIHefc{>kLTc)gF12K2GKfV@yI^q>9u)J3N5O)YgE_&$OE zo_G53MxcHH`}|b<#MD4R&D^C4DZ0$HQ@nEOOzl(n{rdasw-1`YXxTBYQ#A!^yeN%(D;Jxo0zgUc1*T(CCF!&JFEfxkJeT>7PcKle%BZ2M`ywrbGX)7qHem zuNcZq!^K@fjx#3=6S4wv@@slZmB~{hOGzd12CrI|O=_MM>e{CVP?(}p(T`BVaHYlp z0pbwFzt3{FEAC$ujSh~+Bqd*tHxGA`DDQb@{Arn?$5_-)z6$n;Z}C}ComPyDG?%Aq z$7_+oN_{wPOgj<5Cf3gfpXkkC5QOU@UZu24W#TYv$xIXz;z_S zymPPqEd&N!`ttP6gd}L1R+qR%+acFHaLKY#@EEImiut_eHcbbKiL5caQ~qXtsQ!M? zF-|1l?fJluMLW^sw@cP35y+4k*es(9Nkx>u4|;9tShWF*AgbpPWb5FtCZ$ypTPIh; zu>n_-2{U)$ABAAll384a?i&Yg0{T;*d> z7TL*mRADk%2MjxPG+zOGYOC`$ymp(<|EUe*#GYDy3pdxx%l&2j;a;~=NV^ZtK$VBFNl(p~=_Q0xc~rCzljemco?C*#hUzlx++ z8p}tH8le~n>TV+9`_c}rpP$6`Na)PzPK9w*!7!Z*8gC4nza>GB{zv8>ANy#e1MGZAhusfdLHwO5e|v0(9uB-`c6^Bf4@okv zjny)jXSmTCd1c=*UAMmT{G~6G%-Ws1`gfUa-=Fn7|JjmQf2RZ1eRVjKHqi+?-HlI5 zK(gEagy!jD6ADTQ&_O~${mvR4#gd1WrLYaKG3w;%6btX?R!j6?>r`;8FUFapg&UQw#4Z+@EU!C{%eZ=GiA z!@hQWG3G;0fiV$;!oLAeP-rQC2hROKGmRae_gjid6S=OM3_EOYD_%l&yAm>R}44Q~i#ILKFN z!GQV+&)XjQ3VUXCtm_}n)t0T1R4D2JR2enp{kluI$xaYU-LAvg8mTMI_b)9=F`nc; zp5JKl%Chq^jMup*+3s78>4engFjC=~SI04RZh)#@LZEa*yNZ;&ax;}JzTDz$_7Xrk ztylPg|I3k%_+E)cFhNQQYT9gnf0z0E^c==M?!A1XSC-HJ)U%!LM`bjLxO>J%5!_D> z`xpEaKY>-IZS3(}2=-XLWXV6k6CFa3E_dGdv9(!C@$F-8ZDw=}<-FC?zIs~o zouy%{zhL5a*+RM!BW#r9LH+jDa*XH*xlv3TKqhj8hrCuCi?ETA!DekZ5c)jEZiUZGk0;X(p( zvhn~hIxJ}|WhkFer}@YKTMb=(Y4tp({HkBE(Re*sgDz5B9x>Yd z)bm@?_!-5x?Owg$xB3!}ewMSJd{Ye@SO8JixGu^66a{Vx9H%x0ZIy`$TDC zHgNtGUx(NdNf7(O+MYkzCOY#NDp39xB*btpQeZh(yqOg`wbc(!^&My&x!m~c>f{$@ z*GMzz#V2b>EO{Ht+>0;jQBTmN?wG*OEOx$q55Xs%D$y(z@2se8NCMflPCl}|0-SP= za^x5Lm^H+Fy>+AH3^TYacF&Y)Ict}^Vb-rSPrA?j&NB6HL!T1K)rRO7|25GrpR~u) z;+H%gg|2*;6?W&YQu)CSq?wgZ zo@|6HnXk8?jI1a*jXl1}(Q3bv#ZhJVadb%cE(pV&uKp=|kDxTH8zilVM97+)c)Ws_ zfTSiBMk^0kCSHsQJTL(qbW{a_H(sx0;B}+Qn3XAFKm06&f414?c)C`N3%uoF5;7LyDvBaf$5t@MmEWsI*{O~2!O%vor|4Ab&$wB2i_#z zTgl{#JhcQ6DJ<1RB#Yy`^GUK!0u^E1Fn|@x#kS zH?0Y^o9~>GWpji-uV?H^`OgLYT!sHa z9?mL#Vu7Kb6n^Ni|2W8EjKUm!_H0s19=r{ZYs^HVq=*cQUUJlB-P*HLdoCVu6Mw*;`3}Tw#m^J3>F;7B^x5-BRFy)O zK5(k?Z1|_(oBskvSs1h2KUdZGr?0*tS(s0Zf^Ih0*!o4~Ty1VuM8$dg_wEZey?%4#4xKXQW4bl(+wz)Pqax;W}F%==_ekz;m(`HMr&0 zHYlXRxr<|`|wb4>Rxc`+}W zvef^9=8pTP*+v3h#8d@P{}Y{5Th>~pJHrXvX(bIu@riW?ki)43RpqYc8=sTcEFWrL zuyFFJ)gm$El-cuw?sqr`CLf*QX## z^ASpPV>wc#0@5{Ag`qunwnno8Fg?G)QCF5)5&&%J<#aYrp%|HZG z68!<3nU6MsXAz!nf~SN4rK=KHj6&pgo=+Hm zz%<=S)5K!Z#d|bheDAodPERKCG>)`s7D{^pBQKdmo?c(GG`L&>tRguj?5=(RvUh9J zmh4zxDfdY+ai-)jY!@#zL$V<{38pb}9EqhMXZpiQpU0|97(1BtqyR_rpkGo9SGrF; zLqymB&KAcfHs82;4om?(nvYz3Z*S_S0f~nKXa?=;??d@SZ|dAyRmlK+`=?aGcf9XA zwM-|~(>m`PG1)P71xhYu{xFSMp|5$~jKF%)&yox>$9)@x_z>ejjSdAnL~M36Y&Nm) zU@kmQD@sl8deZZDupL!1Dm_}hAW$u)9#nnKFZ!i+0YCW?T6^{>^}EGuo^e2y9Yb#` zODck0F_*y4X3ZBh^N3v!LinjLj3iQi9{#SV%1Vw8qgVyM7{SEf_RK{U2tcFOGDg<0 z>8xh#AQ1d*Xef(x2h%mvu^Y_|s~XzryMQYUhN~{cBm9osR2K`aFe+b`kx5H2 z0j2zCDCeuPI^TQ4x*R`K;xC~5VnY%i)xKErjW`fwnk0f;6Vr-%j@-h|Y72FsuS1?U zK)jzwDgTP%`@uc*k(9h3b+eL7hA365%Fyk?u6lF+%m72*sGWl%q=7vh?1F@F$hd@* zvl{oi_Q32|&y~<+f`_KY&I{1l$-lOTnmk_qh96088ho;^)AdEE`kY360_PytaOV9O zDS`SxQX2;OF14iP{$g_0VTBeKC#e$aFR7Eu-+C0U+b> zN8w{7PeejbQvWe@pm15A*{EB#(9rF?tp|b(q>lwqs-;5;p~U9F5;yM{Ctcf1)3o|T z6<}+Xkem3#9HT(E(qVOG`QriVP3hBn%5yHc*LQ3-zHQn`)c*4S)f@M}o()} za!sib3NaczqgS=1B)1&CvFYl8s(yn5iokgdnpX}zC6ocj`cvVaB~OkYW`R>W5Yx0z*KKRCbxOW}S(nt7zO4}1a7Q`L z*&8_8*t8WzC#nK}5QCEU!aNfCq-3_{UP_l&X+5ZWu<*Alasr9yk3DEzQc9v3kPJWZ z>t*CBnhXbUw=l^mby}b-bn|S*Q7tI?(=ku9V9(eH=G#eY z>iY-It$(E$+Ai1w zV0xPj`06&ZgW2|UKiHmG2Tut=c5Ia&9(E{>etAfUiP2@YER+JztrQ;SSE@R3L*At8 zABwywG;T}4kJ-rFJ8|f5-l-{Rf%w3ubhkuH3GlODbzNYmXp?ZDKMGfHh9;vM2cAqA z*31_CeH-&%Mf}nz-XY9hw!=)@mD&Lq)+yRy80D2eIqh8;mA z?WD}m{U6|5)9wkv5$-R7!SRv?K2W9esv7!bBz4H5cu~tusrz>SZ^hCgNE*5;#A$%927OvS!L0F+&~ z)iZ3_!h-9V0Ys5{26qRha`300qS_kbd(j*GmHQ(I$kNT7X!j;8=w~yQIutq#*KK17 z{lMp}zs5L5K0Q+oKlb=*VKPP@5Q}{!-0eD62BF31Zk6cbQCF*!_zEGXi{0G;Uj&0Y z|HZcF@H;y6y{A}{Z54AojUKVyGw(7kSSQY1JiqizD+37V2N_E1EuipPAl}B_2@DuMh(-@)Vw8@c(n$k@$(3Qu-aQwJwTl=)_hU*OZ)>`J7 zj6pns&2F=pg&lFux)J;TXgUkGrrv<>pD`pv+8-Sf0)m7B(u@WH0Yw36rKFK=hIFTN zDIlPvNXL+t4k_sz-LZ|q-t%0~`}_l37mIuM`PK(vy}I~gPKvc(y=|Z=j2#l{RT+Hj zMwzwB_fNQ8GBC>vh!>PwHe0O}rqDju#k}M!i1g2(w%!{+zq>RdVjvz<%UzT5ZnsYt zYubVspH;K{dYt+ZESF-h*dcGK zP&V2)y;)S~ZwLQ!l)(K8Z>ls})BibOHS#7ScE1XK5{1YZp`8U)QWb2UOqgXP&aGw{?rU1fc`|7y6gWfWu zEtZmrI&IfF4U$_&*DfY3HBXIelL$?1?x|w=@#IkZYl9@J`CcpZ$SPIY*wO4&dwpdU zjF7`werm4lmxv=V-{d=KKdVX{-wr7ufn{>;wx3bvd- z@vS!1I2OvwJ&_rIRtBX5Blc(<3E5~Sa#R%1ACyI=l{PmvotlM2ti0niwPYebHtnoZ zb?mHQlYIYp!D?*RadLoBji!}Y0+n^|&GjO++$C0VHsSF$fo4$El6231=mzrMEW%Nb z7#ZgwG@rxNF!PDYC_=bw%RNLj!dfEe5m?%?G&sl3g`{MxrzM#Nk||MjUM|RJo6$CP zY3LTFm{&&WG?IHyH;fQfAX%zv<%JUI!cm%kDinr~2VLY7dXuO&X^RO(s5bU2XBXw) zfdlEzh>>ptK~`9h;)?#`MXAGC&=UziC3R;mCNhr%IzC$zdVEL%*ko--#4Pwj&w}%j zaG`d`DP7;fCfD`lmSxv}Z4_hDGGNI?o%ZR(i$-0}4p2tcUm}Hc?|Pbv|K?$_d*I7y zZqz{38+|(18~$UEyRB-fg_Ys11V34}@z+K2dNPm-c**!U)3j-eO({dX6XYMbF9zoj zebuhcYWc`8ojFlb>LcVEmS@khz@xY6oJ95A2T((6yt>VVar$Dwhg|op-+$FdZ+m-t zHMZFVVb@VLWai%MX*g$Cd7hGVmzhww%qxSMsjHoGRv&97f{mUxixM{(nFC9V4xyNs z?7F0ORzKEs!P}KNZ9A5c&H6H+f=TF+4`%5QPHp13leNs9a&*w<6jil3^CR)JFTXfu z%FerW!hFclM*)7rY#?S)I>{c}4J88?3o z$rqqhBLVxyqcVF@26Z6Lwso+-Tw9JAI6TeA}lX^TT~hL`{)lBDh@0I zvu=VhbQZK$Y1r(KRmQjNIZ_tdgEIP2Q@FMWO8!j;hzltkiKXc3{WOvzJCRnTwbxlz zQ*LBy_PF)#>#&6Z!EKaNOlv}jOdbL1$h1nY$3dbsH;WJ)`7|(GL$)3*%RH_V7t^M2 za7Y4EEblXH7TmJ3cVerL5Pn#7@;2?Knw>WeBQV}ORY_wxK}g|!pIfc;)HBt~bFiHH zJ~T`wM8-fy(p^@*kI<9(3-5dw)9c}8TKo#3Ni-4*O>#UT4k9M>m)|FxIXUeODLK z(8-vYFK3w(N#Mf9`)>U$*X++h{}CVa-j3*#Op=VJ)Tu>GR>kn8@8t?siUI+FFPvKH zW_A=s$%pM(hWdA0nR&I#w%Ga|H~CIXgtvVk@C3;qgC_u+SfB^gcVbxTS?-^?xlH}6 zc@h@3VpR_9cWyq!Ar`R4g$i*xH{>>wx&!^I60xEHzaCB>N{JbiW;4+^_^@3&*j`#k zI@&_kNUFJgRckZ-SqFC2L9%)-&&i z-VRfNH(&rl6!Jqmk;Dq|n`CN2e7{Wk_!;|G_jyx36`M?fGc26QT9*FugfYM5B$|;# zGu@G%x?H6cL9y8$c!z4K-U*NOH;GMf61k8M3zxstwPc2NQBj|PRPuI%N=YZu$ibg1 zWCAE(xSEzX4-%cXwYa$`_6XGsWSG9_*B^}FIaUUX7s zmWu?oZ*eNCEK8?_oq7BpaK9s29svUkaBtVw4Y4aU=ifrac2C&Ev&-!w30{y4Jmo8^ zx6M63@77caFii>1oA0oYzMU;2FxNX9V^A0ld3%{dpf-gAgru%IQr(RPc5%E&!C=$j z;sM@2Aul!PO^ZnoX89#oHlrSbNVCH|vxh#nRMKe_^^W^H8ky^6!b3?VX)-xh=iaV| zk{xhx8`j3CgQELp?v^P$;^rUK4z=_l6Yp`59dnC?1on$rqeTczRh#(vdzI(0(97-me79 z%_m}Pzn*=qDr74mPk22cNnr$|^OB!Cn(^|dO@;;d+$p|4oiwR&;ux7k-a{Q*HI3JC za;J1l9;?*hLe@F3_bVyoCxQPU16+%C@uvD-Qi5Z5#Th-rU;w_q%u^-*PdtdXI#pk` zAatIv525-$P>S7Y)BjCL)3q5gZg(MWC^nuw!kLUNZwF^{K*%REU}V>8(r_WXVVobz zaH}ckmFyP`BFIT|c|iY#(NG1{jiCxD)M$b;2BJr?4zYU< zD;!CY%ULJyedE0SNa5=Nv4YeNwB4zn-uYv1B=W`Em8x_K@9#7uR1I}&J{kMCu)dyf z%;|q6fV?6*ANHrVXZiG``qv@@P;Dxab9H5|;Q_FL!`rx^|1R!LtHp}?-G2IsVNI6C zNo8Fuc$mGmAceQV9*-1@l2kd4j;p~(@E+U`(j}?BP_6(yIr=N?ld{_@A^)ZLji=Yv-SYgTC>u1C0!qI%BfK}F+je$&u z|0lCev>?PZ1Sy!|jj{gNA~Z=H-?U5Za=NXvRvS(P@6_l0rq-cyNk>m~oxnKo-SbN4 z`Rq9wbO3&y$QuZPAIvOxVE*i3LJdL3cD+mYi}!W)m(Nkx0&Jm%bT6CTH!b$4ew;iZ z83j=;Es!BP3bDKU4J4XqLAtA^619#*>Z4PehuYH^tNWK$r0}z_HR8^EnzX^P2VUmq z69HJbl7;$$qNuDROB$kX>o+^Z7+Ni!|D;{w^vm@Xr~(}K9}qbE|7d=>`8cY0JuBkQ z7J82s+V+8Yu8PgILLWrw8n_KBCb)+#%Wh+wst6toJZBDC>_Xw5%})2w8(gj~nEmq# z^O{_qzYSWE*im>lWUE23WQ@<~>Ub>F5TNrQlaO76sof;g2HQJw#vr48nNNFMhgU-L zSWR0|{MJ07c#0BWbv!`tF&Y*si#~9mT}gd4yG&vJ-)S^Xegr176brQEn5DaZ$88mnF}e%F6-$2D1mHM^S+y>Hhj@7L4WK4}4eX#_$}fZ}vt{~vreI?#?E(Mz;; z01r|y%UMUfbp|PQI9h2B#LB+D6Uc=`q_5k6tM$?D<@?~GWs79_fRvJ!!CW2wk@3}n zXGz2<;VVnvuNb0|5;z-sy$weZkC<((s>i!`$h5X=fnMsX7jTDxWi8*mHw$c3a-zWD zoLB2v>xIwacDy@myOpFkWqmXWPQeyTVjnLpvX=eX-f?AMdE1)Bv7Fyk;?BJ!;NrYK z>X`ScUCTkNo)*bamoadmm+0vmmbYZ+_S;B7{RY1F`Rf@qK~t{> zo8X`Qy2UnQU9(_~aoF>(L6$NJqvv;^+1eRM=W~+tcQzeGlArqiAxWA88Nh4(cL)*U za-i37JoEJbaskm3F@du@JU3~c&`@;3p35=zj`yruN9MCL?C80~#Pp_ZDYTlh?fBo| za=@3iX6F0fG7iR@bs{?A8 zg5JNnndWEgwZ)K)^vfX-2H7d(EuL9uzx&Xnj`X+Id>^4r?3=e642cmVXmV^<8>2+a z@x9;vGC@I+`1es%LtWC71PPU0Y3rBD(X~nNlN;e%wb8v^Ao)Z*Q6nD_@?ndXQsQ|! zPtMvE;W((cFNguty0Zaa4vQepE=e)*`^~qzS{ubaW66-yrYLSAa z=T`5 zYcBb@={;S*=z4+MnsY)KlF9{*yc|{}5hXvAP~{rDo|SPB#V*zBjb9Sm`sKkeH=L~@ zNM})s=IwEQJF~FgU3+K$NvA|1S?z$+HPAj2WNcl9{Q-y7B-KvOd))qgZ{_ccBNM*q zmN9Qz>Mz6-#E>z<9_I$r7;tus1XmNmHQ6CM3()ZKg^Z$$O;HFF8ffI|Kohc20jAD6 zfv;raL42vN8BLeApRV25#q3VK{#v3fJ!vd%*=TDdMv`o2eYDh2VbS7#}ljTv&*-k}w3ujf}{ z<<>R#p4Kvp5&Mf^yf~QOlSz5=@<_LENDjL$U~Yy~zWsFoUuUB86%T@BYV^r7*h;Sf&nzU{`J z?!wtI2{@E9O#w`r$5QjDvUka_@_S`>P*;eHu!g|-MVW6j({%y0NdJZ$?`n!iCFPn{ z62_OI5J~)Rd|hF#|9}f4H*)VT_0QAcp@yNy-4U{likl zrL$(RJ$*`!@%{OaXp3V^^6r4~D1YLE@=hmsZ{B>4)&OmsJu^nKUlgbh9GcV+1;C)< zIgz^NW^$lrk`Sjqgl;MN==1GQhIaJ^xL~{;3+8OP@tSR4f!$w}3aH%T_7|P7-y4bL zO6V}e&j6n}IzaEv!f^9^0pWE(hF|8T znLTu};*&XbFaXiG}tKoh_fC+(5eA;72P0I>PpA*|II9F)MM(vKhH*#xmSz#CJlQ z1M*)0*K2ygSQ6g55nFZS_HkF|=oFgmxBkvF#5_ojF{>e;D?%kqIn~U#khVHmjR%!X z5NGHnU>3C0q(?m+p77-K$)T&%B-U*Ii2@Yg2@VY3+g9GM>NJ)K`tK9l z!e3gM@xR|+4znwjLK+9S1_KE_S{i%wPxp@g<3*Gp53xXp}_qTI78X0ZFQ>?tiD#*qXGJ9b_Xl*l!ix#iHmZ-4M3|^5B%|Y}BBag1@ z3XwuMdoiDy@8y&tufL1-|AMUmx#$&*Wa+2m%a2`DetSgkMLX^Wv5fdGRibrcuuFIfmEr-ee%!n500Zl{Bt>1Q6rR zQoaA?ZTbB`7CS)hd{-}g^H-1GI-`-krl9q1}BT- zXh&qW&r5w3uYgL!XEY_%O5S2rE(++Yqc(3eE^h+kM6%Mb)G~7$?bj^xSl|ye+qdc6 zAVr2UQA|ja)x^*T9;ftU>@)wLwr{G5neBj~56@|mEA0k9GB*30(5_NF9?qz>#<+9I z7wLx`9k&zrHmhOX4*$b#=_L!;-RJT`dwflZk8ths@pri?Iq(*otgYuw39I|Hn=J}q0yc5{or;g zXM)dI+=KrQbNCyyRDjltjh5-~$j4oH$rjyW%EO5#?%mf@GDxCUa)V({U2?#SEQ44Cu%+E6Tn!^r%3{ z_8zOSUjBmfQkg5vnqL;|^%L?bo-H(2gUpiedR6R@>@m>bRpNlWcgetcyuNK`EVxjU zOMlRo1{wZbJo8M4vuHHrji2IY-1|JoZpN2UZ1fL@k#QUjt&Bjj-NVOyxtlbdXBn*6 zBF*z_N_L0};eX%1`~6M(?g^u4{|Eg`-6k^eWl!KQ!*kf6*@Ad<65LQLT@`6O?RF~3>jk2x>#sfsdBfYMPXtV*o^7$ZuSaY@TQfsegMLbjd(A@q zN}>tSNF7Z4={keZ^Apo##5CVeV?0lL7In#r%D-!Fs%V?@rY~FX>NRUmzn?gCNee}; z1$61g%7UhU#OI9(mGn9hS8x<3@&P|dk%#+x|08ckl((uA$o-B$;3~wHFiB2b70de) z2L@1hr6${J{`uvRsRRAhcBPma!){HD+SY4U{g|=9sj>uzhu>7Pr-5!YVDnayRE|Ux zSQR5zdVffyAWCjz*!`oWj{&~t?Ro*rS|@EN?K;y(Hh;&kz9LqpS$hqw{^h%jc%sT^ zNA~^#x0XctC#}|?uH{j!o*!6tUispB`*id~aYtULnTp!9jbsuDEYU5nLoTJxni>`! zD-(=SxW*1&9~;r%oQ3TuVnLE*K(1pkL4_(FjlPo%)dR^Yuv=qPMenPEFURS(ZlQ;jfAW@upLOtP^ONH9Cry@=R5g*|641}aUn2_- z2|joR(F(OneX6s{o&wyT+HaJv&u7T4^fOh{66*wlcXc`Spg0LfHh=z-#S?yeKxkU} z1HN2O-5>tMoC8;AsNke(V`M?_v2uE!M5oF*n45raG|^X7gk=#f9Z+~e4FnMXB>{c9 zUQ(e9@DW;da;J#AAUsEtoZyF@R{0mE_79DHZV9T^DB-k6h*Jatd*PKK6!ze-OXZgk z#4-W?Q7^O53xt_gJV^;VBKMM{Js|-@_$1;vbJ0uamLg!2} z%)C=C|141+_JD1(%2^(@vufdM68A$cofmHq?1W-k}a8fmC9d zwYVQxWA2NRzobVsPtp@PJjhVO<62K2Qh6$n9r}cNA+71Pz6bM@fUl>U6+8OpJ-Ip_ zccpV5X|aCaa(+%0x0h^tS27D22R} zdY+s{FMi+KN~TZBQ=jdH+8ikuU8B|O`;EZ;R(8nOl=8HdR)Q5RbS=_sv+)35?gzx> zJ%8MZ#}^9}KfmFElDh%93#Z)9*U^|f@hRYgfWILWcy%cPv%lv@3U+mWtCx{%>QJxt zQt0E1oOF5g=|fR=>dog^#Y0DKq~PJsOdd?_Zk;7oT&4i)fSpIq9UEO^!5)JH%fT}c z*zR6D$q+8QJFHvy{c z&H_Q5LhARN_$CGo6=Lu3N73OaY*5z3h3&n$>9}}Rj^~>Hrv<2QiuFCnfCl_LTk_2v4&E!s$Usoy8K({QjQ{_Snm9Mkv~P=h zXgAC;X&KfA&AHlb653>uJ?%eTyErFCsPe!44sjvJw>17`$;K~w^2v35Ch<-@{hFUa zy+qsEXM31$$G=Hv^H06eWtvS?5m=D<|=mk7!FRTl@QmNQ7b^#Mwxiidk!B zzC#cw#Hj`RuH<&?<=NeN+T5RdL^-Rlw)yB&?Fr}4sGLG_m3K=kqR%D`;hflp&SAav z>|U!6mhI0u%KMnp4}3-qI-vtr^Wf}tNV06pHOi>95}U{YX)m}u{&D2jH!qY=Ex~+l z_=!YflI0gN&1~Uc!*?6|CfU=R$^f$=MN+u$>i`F1QM(xL&y@7c02{QHqK#lh(yNT% zfrHNu%_9d^n8HZE+X$$*rW(G1%FXao6$~WU0jXf*zcoJJ_0nrt%XJUsdjS*r3zT#5 zd!&iIlWWozsR%wsztq;tY^{X!YfjS6O>~tEb8zHv)Ed+EcWOxlh0mQOpr~|ZEk_X9 zTkX6LeNQzLJPD@JiqZEHkw;C-f%z(*UZcgX)EozCX7K57$_5cAId`zo;zHkB`*aH$bfoL^9nJu;;m@WF^1J>PHfW<^!jcAxxMHn*mqU5z z&v9p1(IoL`-Zd8}sDXEsy4zGcPMBz)Y66Yl==Tb)q|8 zLlfOVkdhq79g%i9a&N<3rCN{GjcAQMj9i8)_f7yKBx|*>x|sRCAT`4cM>nm;+Td?p z4IyxjDL>ftvfn=i#k)3ox} z7r5cy{>Dm*r($fiy8?dDuw7AabrQ&bv)@JNeYOX5W&nc?z`j2-djBs_01lg~4E6?q zIN>PMXu(&c0CKyEC`D-dT$lVsn0xO(4H;`8oS7L`XQ{dyXVpS>f#petiU(zlPi6V#WhRtLGCrc} zXn~;gnP1TT4kOpMX_)Sm+)(=(>QQ@7;_zMmGv)60BhQraLQ`P}d#rQOaSBIOmJ_?4 zR0d4n^rY($?Rj>HK@@_;hoH#-x7ar)H+yTL)`U4=0NakCWP<_17&hnszL`R>D5XM5=gIDWo@esYuDp{ za2Z+^^WE>YBu_2V_4eb(3(mUQcb*aRhnvZ1Dr#=zvF*A64mCgeCJ=q1fC;(^w6;{m zu|vMZ67R%Va=b|%!73m^?-j!&DC_{D({BXZuU?;n;&}H;yAj`a&m5I1u zi5((&dx*A|95P2C?e%W4Qu_h2gK8ZNJDYnk(}WC>Of1CP3T85^pLWCXPXnPj^}ILnUb7{O(%`x0(IYi!>qjXPG4C#$?HAY_FH{`k-mF#% z(UC983+P6q6fYnTQI7kA19!h{1Rcwxul&&+;o zw9Wub^-{#(|6F0wJ_m)n@auX3Q`xkgTJ~xTB(PpWihtSOF z60AkmRS2(F)esce{STelMmlK5i|~Js=C$VKEvs!r{bDYjcV-dmfGF@=caY}p-}l^# z4;f!MqAVhO3p)P`tq>jwtv(7;ZxXKLrswd8vbvfVBIA~VZM|jJ>q_O zQ(44h7L`Oqz4l26P}b@94slL8ncoR`@Wrt((;)S{-AX;5mQ{*+4RFMceTdcsUNTb(0Er8R$kT)QB4xU#{{cG36GB z=73;wjIR^j8vj7q-dVZZ)?9vxO*DR&>3jQbQJ9%($s`MW)RK66Ey_24` zcjnCZKlMaacDL*_21I>F$NAhhWyoD<@VWFL?dat^q=asj{S1>TgAgSF@BDL1DU`^2 zLYwq)Zcf8Hh0EN{j{@aOE}pLwbx`fxmw6fi>1px%a34~w7O{$G)A^iEB35C0tOgkU z1qI*hintv(DCYICj^2`U51+ILRj6$3;%Kx{gZJKSVs9)3k*p$L$-&L&*}&~+^I~Uu z;t!_O7Uz#ULAJ%$ETCsb+IYZ`EGexQ)j6Z#W-)~)x8LtY-WRJDj}N5IdyT4cfblS* z0aaUC#ZpN18O5*5FKpm5V86|EM2=^nS@GbW=E=TI(~p{leK4}1XESQe*?kY?G&tp#Ag*H3w?U=mO`;5pDRf8w&+}n@Sa#rb!CBn|sag_X zX44+O5>TQ|Vf^6P>uip(2z!-CQ>A>nhLC}?FobrC@;RDIRSjLGM-<7~uq6sK^F?uR zen*Ob9Sj>62*)o0@`##I!#2-=ur1_@k@STIbnXkJt<^ zjr(qTghyeFP$#Ac;*nTO|GkZ}O8qPH{Jpb3(4q9bE%NtRLJY0HduvrtQOU%^BZ5fk zr0K%V#wb(HoGzg*H@VPUv67Sxa?kcB&S&m#rxH3@tk2aF-9mC0wmAq<5_yoeN9`#{ ztNO&kJG5pisO{rBtF2J_%|!ZP%X9Gnl^PV!8U2{u?7Qj+hqsSk`-0VuM3m}$l5t+_ zI{8x6xXp`4{Fd^i#Y&x^qsAy|@tJQ(c$IwmbJ0sxy;YTdr#G{aN$_XWz%em=mvTd` ziqq>JoYv3ZrtClNg6uaLZj$cPy!MKWJ4a#SfnqtHJwqZk)ZdRBMO5 z$`5I;?h%`9id~$0_L1h>8>)2?sb~1q2HJ}u-ElkJmKxYpae!*^X z8eft=Km6NkI><?|;`j`<7wa6OFYOq=f6=>Zi}#1_b^oB6$ZbI#P%uxYcV8mL2BO z#(W}{5@T;9=-@A+iL_L?!zZXaozr#k>kb?1*56Qz4((W$@f1(Cd-Z{yS<6c&8_Hr8 z5S{r?8Hu5742*cRRCZ{xbK-ScVORJ|i7eL>!kkhFPe0pjc)vC^_MF-Z-xv2nB8X)S zA7^6lnZlq{aJQ(-JEqWFm?l2xTCO1j978S` zs4FmA@uU1Zb@-p;t99_V2HXCdDsKBEz{kXAb!D-u5w1PSzS%^`n_jx|vX3U#8%*&{ zdc4-vtGScd7pr7R)WPix zF$HEnh3%+>`g#WFE&e0}Lsq*!cNlYv{G49jEVuGzj?zVF$@<-{WJPSTIK+b!(ZF7K z>10@jbLoWBpR>L#%Cx+opDXWJa_L~RO+ddwQQ2Q%$pSvV`37y5BEoe>{;lE;n88G( zC{g*sTeKg()yGXD#IlU^62e3Yulb}miAL0^@?0xPtmwi@<*iAw=$hIugJW>x7F^2=AljzA;~(i(N+6xvoXUqo_@64 zTWB@=fhon72Ao4ho$y&2@2J^ksMg3()x3#sx(a=mRi`L}9iGs^pB! z!n$q(y&P>^>Yq9O-P6wt3{h<8D)VU@^Bl*s$_v)Z$A|UIBkQ4W=}w zZ!c4pBrZfIzH0$yx9QVQK#zD7K*5#f79eU$34^AeFED03Dr3elZB~O!LTqAzX4(Er z^3a^EdYf~_4kHzO9Bv=1GJE}bLrWC2=UF1fMxU&8lpD%P=GS>iCP0KAJPQBGgBRa2 zQhm>-XNJ6mg!1OWJiv92y^D*tROnN5pmpy@2>yS$zkUAed@3Z2r;xlP#lzXQB#c{b z^B6(r>AtyITE_Y09crDvp3uG-Jz6qq7%9`W`Jw9(aU~(no&)=!FpjDc@Kei|HEII> z1*^=i66JYdsdICd|4WtCYs!TnQ1((c46?s<&fUq8fB~ZD#C^P9rX6-srP?462=ygB z`Ojg({;X+3b|6B}94q7P% zc!q5TWeac}kyIq;4yF!Sdg8sgVKeZD`y7)@j_i@=Ky`Re3HA)LD=vDOfcr(Y``kkk zguvR{-?~xu>hGx7E28eYdN#d{{I9EV{-?Y#BB5Uy#Pug`SbERGj`TRDQQucaC&qA}r5JqFS=PFF68x0{XQ+%9<*)1c zk`MS{XaL`zjVP&RpxSXGM%v&<%+b?T6JRT|ti*5{WWw+*zn3DsCCaNf3HYLBvX@y? z-OxR)MJAWICVu4!6=&ugsPlMF?gof29Qxyf3FiSXm(5;EjZsK}=s^ccahPxS~Uaf3MFD{5J$CZ6*GeCfOYnBa88voiB$Ypu!j?m2>_Qj$0I zj8vvC$mEDS#S>rn!eTWpW8X%hE)={}o6J`O8z*Wl>c=NII;mzFZ4Gk-c8H2?leRfN zIGbaiN_hU$IFB~YHJh0zOj7*iP^L0@-<3tWlV*cra?}+#C*+hxnhuh|l=kaSpqD6a zv9T4wmja$>zB9M3<+s|7gPXahZj;LEY0&E3T|K(=H_;4E-z@KysdK*n*Hn4a#f~RT z$tL4$4@#2jGle{az&@q^>I(@#NV_owTA=>LyOB6@;L<(Bx-EhU@I%-z>mRJCJ1M&q zr&y9O9@j}4@@+7|)wwURHbqQ8`cNUw;4$~8`7B-a>D8ljw&SEsT zFO%R&mg3hxrboEineO9ggbtKmcwsBx>|wDGJIoi*0dv_W){Zlq+|yU2 z?aHs0)hf=07LXz%Ew@<*qXYtdOo=Y(Eems*o5_eRt5~ZXIc)ND1EOa%a2MT* zKgCJ4KXA%9OoTE4j)JNHRolW^_|egn36KgcShgi!ZWA~}3(gHRhil})f>@2*fFZTr z4ItD-Y!i^%VV5)ZJ5!SzfgG1R;*-nmyvvjQopUpSDkEH%0Jd##`YJI=&`t8IeR{;e zzT-3OeK&S#Jh{BnA(m`kcMv2x04ochN#2HjBSZp2(yXaw{_W*Z{1{6f+}zA8Z@{N9 zPG4Kj2X|@1O2Bt&S%l|4G2JF`{Lk^gKzu%kKq0iZvt3A<|LWW2{LbqvCV1Grl)*=3 zt{;k3+eXy?Yzz%iXG`M-1Y7qA4p&{fT?5RXhIh*to;k#*N^Wtc-(Svv>S!PDt?=P( zR8742WBUimVn8Nm1=W0xrZo$P@SVuPmYE@m;6~C< zTerE=ury>4+tq<9#Iu42+3SJP@#9F{(Ab z_Mh3%73TC8vM!ssD0cQ0Zzq#_R?GZbMtuABfW$2i9lKs_P1cN`Gq&s3s2}H65`rJx zfcj&N^`95<=*99zj<=OX1$vUIdLrGynp*ST0Pn_2Fc+lvWY&h)!)9_l<0+{(8SI8nekj3ee8N6*j_4l|F*3CM7h4mj%>Ot z@YIn!-`p8uXQ#lgE&)#cC<7R-}WZdOQ8gLFdvq*oi+*Mvz@8n&m}LPKI} z+^40(XZJs19FE6#A-%?c)Sqe!))DD;-EQYdq*9()e!?1q5J}K7|H`?%0ap_;PwJ$m z3hjao*KcQPXhZchu3s8`oUfq0aBNsZj|R0r(wMd*mpxCUhn8U4uaEi=H&OYWFq}S4 ziX~|OV>-F5={e@;{Do`z`EiaBy4%&tQ$p|C_;qxC;RClszT~tdJlLJ};8-GIy{}aI z8Az{4OD3k##(Ef`#C)-;Qp^CaY5Y zcOI;(_Q_O!EaVU$KUUPx+F4(;cc!*?uJQ>)DItA?8LXr6%~hk=#EFS|^C$P{@P3Bm z-#&oe?!v~I^Wp+YIau`zhVm<49s2YPDqB$*8H+T3>!wYdIqrAUUER!s8U5OjNG23N zUy!0H_KW^S!1(w@eYKHx2IB4qViylbUGNV5ywn!4nrC$W*C4Yc%!I>$Y#$%eu?~iTBBe@!AP2Sz#?Jx*e=rvoZLoTAaZkeqgyN31DH3$KSp= zy1h*_lEs_kXT9rlsF4)M7xLzj$2%H4%1b6{a5R_kcp3-QmcdrRBZBtv0{20$6vejD z`9Z9(FKs3(CF1Gv*uh>}`MyN*D~t;@WP4b6oiPFRIa&JcI`!Bg{62AG<86y9x?z1? z79)!4C%=9{n|ITaL}}2RXY#AZHb<^k|KWxj4-SjF!L6v`HW9tJ(}X}G++iH7?jS17 z^})mwu{?yTn2!@QSg=XXkep(2Y5-$Jsrn1lHD5)#4 zPJ3qFXD$Xnp{`pOPqCT#1Btio*KibJdEb_bAUDc66t5q*Qr&7^Q`u@vqMYH!;lrD}q zPG0zDmD-<+3jc{9f&0rPhwB`QzNBtI2Cz+zqkuJ5 zgA5~x+MM+$A`U+>ERWymSKa{?v3G+$Ac%8V31f}Dbr8R6_BN@HH57Zg-0hEp{qB4$ z&3f+PCjl58grMtS2$54uq54tCF+SqYO!d@<%kbFIP^o08Zx0v;r>i27q+*&Vnf%k| zDC(-4dkY?Y2f-AY=@Y-N#fxKg13fkHM!IM!oHVOxnoMeP;E*ca=EMa0vv^}OM;GV;GG|G zYdZ@Q7M!^JJ8@SH_%G05=sa(E079pX_sosya=REMbM-JW{pMw=5$vmQAaY7F`)Wn; zVjeuG19z$*30MQeb*G)_!Lqqa@vzR)jkiu4fP)I241vSUc>>U5JqF1h<{QF0NQ_Q&>A^+E=qr2AzFV{Cbnw0e1Zis8E7T&}IzF zt($eN8c~Ax_o`QYG9VW%@AARSs#=i+bQHW^3D>&X}HQh4!2F@c{dR zNkrtszlV+jKae!WDL>1gzA8$~_=0b2-A14!d+G_CvEN+b&wIB$F)>e=rG}=ryWQM? zpHWU%98wPbEPoyaJNwSDhA-TjnJ#!iP4s=8y4HQ6tFYBV{|!(;WTb#}cPqk>7>Ix%0)n)J5-Lb{jL|49jS@;ri*yX>9^GA|I|hvL zdp^H&zW;J|fNjsc_kCX(H@nkR*5t43KpS;L#o+O@*KXb8qccdY%uMBQu06*8A1=6% zHqAo(ykOF*Z4{X!?w_aD4c3YPF6(w0Pi-#rtvW4QCr&(KByAdwXI`4N*|CE@-QV>4 zB$!i`r|pwV&)1x17RN&}n&48ov3DahFEM-81gxgdIUGhwd-h)@bQ)el5M`f<4RKbF z2w{oUW$QF2SP6BXe`PDSYV|ukr8o2&Ai5t^gVD=1Lpx9r(Eeo;uZN#_&fMv#@n&+N zCINQbKO!H#{8X47A33G0{9&(R>aOR-W-I0l^Hy(F)Z6n1)hIW1>wpTP403k{!A^Q* zjStX8i?>ZjR%XYSn4wz~;q&*7s*)*lTgrGX)orkzq9czUC!g8b#b_k12KNMaTyygV zdsT*v!e9_KjBB6)h6osdgJkhXRl0!Gcqqx)qqGM@7(oyB+&7>k&cTNYx-qV zcJnBo^C1K9hJR1$mI|GULYr_yLp!O!&ZnavSTWzi`u;b5+3tem>pyOzJ>c@1s{vP{ zu9?(u0p7cLHhB?abku zlt8(62)yZ5H{-&2>86>ocB^CgQs(2Iq|IaXHhD!1#cc(p=HxU-e{Wuh{*27o0rH{q z?X7PiBPhVf^8EYXlL(g&f4W#Ir)66=Y^M}6jQhcxTLu}D0cCa>pv*HBxIAu**4I8u zU3X;fL@jxgWyaEawz9W{*Rgqrc=7DQ=b(Yb|L~DKE%H$P7HkX!IW2nInBe=hb68Uz zz5Tv;#E9Qy={a4x?Yk{Ub{JNwmx7_m;q2geKJP--{t&UQ7faRcDY?wVO^LDmZH1(7Qg#U*OR_sFCLja&YGK~^OSrfP*-pQklR>urskhV^wcc6v5v;QCjO-o zkHiH0f0h_4vwY5HU9ve%)3Y+|$2X{S!uRvsYc9T!WM=T{?_+(PV?JI*b*j|j7VW9{ zbCG3Bz;|R~Wce#nnqT@ws{I+EPA}DXujHFc0XX>uc5<>HOj}bica$UA%`DlZ-!mDxt+4_k1 zr{lZag4nl5=|w{W4TkL)D{frM+$g8!Tg?rU24N)`@w=J+slrR54sSxuaJgo|@+G~Q zcQmu8_)T_X94y)j+jgt8t92qqNwb}Z58`=kKKV_idn^$)Uz9CBlRF#-FOE~WU41NV z6F=bnX6*}Y071}lx^l7n`m|8-!Z>A5ac@qPcqWE8_08q>OtbeOy>_?rY(kZabkcci znk?V#^txkCrb>=0qimN|?v4^g_?uUWS$_q^ONglCiH?FyMIMV}jb^S@_AcIGDS8A~ zONMf~D#O1xPN;tuH4&aY2$!&H}L;rutz+HUC2$;QYP*&M$s85)GP8Tvjl#h z+^+C%P@SEb!_1bZAr|T6@=PwQNR(P6U!Xdsz;_Vv=FE(uFjC&w5RJs{g86 zVwLygMVq9d15CgXPWZ0UW9?a`~%~*!W&~0C;Ej69*TfF{igpT*T~5BeVZ#{OECLVU2m^q zMq_&q8F9wEU0gg=?9FGEj~dn9xJv#TM&j7b%d!vSRcmU9JbNWC`@4?1p8#}m&@X_!-+0XXZ zWfGGxO(GMS28kRrlz--c@7_}so5FgVV#eOriw4;PFU-Efb?Ea;Th2SW5lUA56o?T| z@7#xXvj+8p3^91Byr^>i2QXFEWtpH2tMmzJ7Tf&aC;f*vo&H zasaJA^8S}9$mEqU^x5MK>(ihu4vQr(S0-d4Gku&BTuFRFO%kuAcX6;p>KAt0fF0g* zIJ1|OSue^l6`=q4h48_@jvY9pwB#T( zM@XOoq#~rDEK^yUK?l+37|LZu(t_9TrV?iRaIXe%&0S46wD=&=0u?<{sT2=r zKDGo3&+a9(6MtkUVX$I-|17#`*%*1!a46w`$>Y4}b%($Pybn26ZFC+k

y+%t|XU^PBooAA*)kwS|*cCmct8x-Jsvg`YPC&Zm@>QQkI+!2YY_g z`HsOuec&`#e^n&*2~sZZ_Xbz5ECeM))Ku$|Xix6bhJoYwbT3^Xu<8UsbuS|yS}OHe zb}(mSAUcGb(0sio0Uy@N|G{Mt%zG%|s8C)D7zXSObs&h)RgUKJdBTkzJ(TAY5+HD* zUfCFsMS}{swwC=_%eKy4K(*D^{BOrbB{x#azvH*iSXJ~Oaa!FcW|zN$83`|l%vU!l zKwV@ZvbPb}rk8bjScV{XvOK6G{B7y|Cv30j>Fx{2%ikuZnBJW6N;VMzl+Rgv(lQ0m z6C799^QTpa(A-_KG8z9rSL{a@&{%k;6(ce4zIN#*^_+WO#F5|PnNY;B2P#vUySCmk zN7SU7YVtDNAjsB{X_1E;9lSu;UhYQRi#(cpU9#>Xw;Hq5hdfpP!3>~w5d;O!5**Op z{8mweOKG@Zav&Dm!;>gqaGW=>V;^(X7*c0wpZvQx9g?;hE9B$SXQ(9vLMS%c( zw3e~L1dm0IK*7}6l1O1o#FJ7!OqFm{ee1PaKbZFi$8tlJxA%Q~I52O;Vu&h2qa1>x zq9nI>w^ZPoXIn`FjG#AnUybK&yeOohLe5czOl31oW@Ozxm7NY8cz2`o{-5l{5k9eq zP;0XKp`+UCB!Cmy*W71z70~}EXnT&`^9=^^pv*kgwW%q6a!=t}jeN)1E@k&IS<57M zo^i4w%Ta{f#XlR^gqQSjVoQUX?G(P2h{3AgOy56a=GbEvBJvk7AuUu6d!{U2xNM0Y zQwZXS;^;vba+0=Z`x51cci6bJsL8T@i95K`YuwGfuneKw3ubtKem+O(iN>uz}6m^kc@5Xo&a17fKh<@AVh9bD<(ezPbFjsT`bl zqR3TBV^mN@cVDMv`mG{s5$6sw*ESV%E` zuyBIY1QSoQbd{bHvcnD+|IG_OY1502Oq)6&snBsJk14{{$X|R_D_A?TQm8rBTz6{q zZ{OVz_VTk(3kdAY)v#2Ps^=$t+A#z}OS7*j=Vg%5|Q4}@^^5b(DfjXriNA1p~PSXY`T$A0Fv+PWp zlhr7)xLDT%ps&>A@mf?LMFVxeH$|F{vk-z>h?}Jir{+J77Hs6FQ~ zVSXm$mDe3xZ#+v1&#-4-oFFhDPya5j2B8?Yz6X|KQybeKDTgE8*pY!wu&j`%@GuLL z{un$)?&zc)J>NeVCC*l;;d{CUC3_VI%GcS;u#C|NB&>$mh^<9)-?rOY4Pr#>e+hG3AAo}HGPFh}5#a6Km!wyFbRAwj zDR-dB?QI4B#+wYV5c^OXiQ!H7p*u>(fUB=$bN7RakrjU2-xAUXxFXp-N z_~vn-1+{O2+>4~;=OZ%j$5avgW>r~Z_|_4hln-HmZ=u9VsAP7jEyex8C(49$ujX$Y zOfGYKpr7-QMAN=&S;{4zhk6D_MYd}c0F#l`D-Uoehc2MUl%m7-2npw(4WLi0Y};AJ z!C51TA=qC+U&DOfTV`4PdHH z-qzrK0s`qg6@C4Vx=KUiL|SX zh-qig?vL?%ExALvW?X-SI)mD?qf=E-%PZfauGL*yi*hOjBAKXRp;?) zlu)BfP~FWws6AAXb4l@XwRAC+d5T$KXGz&#bo+MH@I7J>;(fenEQ&me5TqO`-m;PV zCWX{HpHKz4@rt?Xg?@*E4v(08aHQO`1mf96qq3*d`_5cVh{!;J9`GT0f(I9NfZ4&+ z-{QV&)RA3_!OC;L?YgnbF2)d~=vc&Me7*Q_S&)gpq+Gy&-=(H!qy*=MGf>-vRJnN_ z$~BOmrg$KZ`dDDcL{?+QC^eLSk@7fCz1|=1z~R6@^B@U4Eye1mhRWmOZm8;5bY^Pz zmoAF~or-u+e8`6k{(B`KyA+HwP;nqDdvX`P<3zgUIs5@HsRu>^{5e=C8VsndLY`GW zojVQ;b|G_yfDZ1KJI0SpZmwVd)yK}e;2+bAk~@R==VAypF{5>$h+`4gR*UgFPjHFw z%26{}?~cg{Vd%SWg7$sl8>%Df@Wl}H?NfaVyIyr^$5QnyNy;01U+btM;`c4Fq(RPp z3eD(+sI76c1|>Wx!j@N!yXzoT2I8eE|8-+B)Cud&xq-&;9N3$6(sWaK|g^#7eotY(~(iB=x^1btdaiRCqm5(3VJ> z`84^S#5U)WbN?Us9exDlQX(tGhIz zHnW~6Kex8U(XJ;g-fz6vF@j4G_(X^ukB;2h)kb2FA$~HYG?7xiF(q+`%P%VAGXC_7 zPY7X}=c79}j3DdaSp0G<>pUaksXg+h_2iaj&gJ6mlywATRl;{Vz_8XjlR>DKl=a0){_2^Z1_YE5lHng3 z|Kpw9Lo_HQzBt3&}UGU)xQ=K^R@afVOs za~?w0hdCCR(!52P9H8cyMHjHS;Z3>=nWYF}78NmK#74Z&?WamFc&O~E5Pe{4D@JH8 zHuwpGXcc}we|*Y_P&`Fs5@tyZPddx6GWCsYwL^h2gl=1hYVMoH3xnv3^>s46^J`Ih zDUtzU6G%&s^FpB0__ER>7vphRuR@FEA0ZhZAW)c-scj6OyL|)+*Pz}9rv8^%-5g0-u?B5gE7>J0sHN(}Z?7#68%~^!h+R6GLz){4lR5*4J1T11OuE)>#I@bZa2G1!`g~-v%g% zzlP`bMz>XxEDNjZEVT|g#dLKJmn6ZmVr}yz3MN!x@vBRipDk;AD$;^@T9?T0_y~x< zIf&!9Z?tM7b6xj4K1@wJMCNZEBcfB0Kn&kEQ;cMq?v-)KSW<_jTE_LG+DZwpp%i#- z0^(Iqdo@WS$OaSyv74d=*%efYF{5pDS!<9grefvb5zR6WwQB<#>~Ivp zW6VKGB{SF7>x=KTpWe$ zH<06`mC51Dk4^G9Xybuj zv2+^ypUFe6Lh#q7zN5Zn#9-G32_KSav?uM}l>*rb(d89RRa(2dLl$@)Q{^#scCDS>r$C z#MSN{vu4slZ&C)LC5#h*ADi1w)YGgb{jBb?{XG6v0VC`I4n;G=T4$=uxcF{5p=8y0 znE)yqNl*;EFGg9uqD{3^-j;Hsv?DRJpZIf5L*J>KhvoRncJ^~>*X4plfM|#W*9+E&=R!d8qs+-P*gn}P`MWi z6#UxMf}Uymmu=w8#?~{q@I(!=AEf{J6Ne8nFreSF2U+92P@0M)2*v?xo_C6Wp-C}* z#Le%6<|f#R-{{0W+w!xfMUz~%3K9^47?_fz&#kPJ)fKmX-^xY>1jB-01?ev;UQHB= zDiW18+?zDN&+6^_13xQXZ^c-pBZ_%jeso9D7u>tYGC-CUv7qhA7i?Cuwu2I`O=tP& zdwKQ#qAU*4#T-Q9yF&((Q}~v+xK)n{a#RLbVuuDjRXgEtts*j*+9oYtBv+ez6xgBHW-n#_X`8?3(1mHNHwJn}7~AYYb}Qw)@d(NrXV`EMV?e$uC`6MG19N!IlaKC!$D1=+Ww z=r*LrSX`ldANjI@xGd8sP8(wDe5-HMsW2hF6HA@|zr?7CBw&~2x@S!#PE~Rv;+Hmu z=SBiR4MwyjPobbstm+SjenU(d;Bq-v-I=^D=z0 zTpxSu`8t1&`>6<}sy{#fwfL9UpZQgJ#e=1$9TWAyi&82-NK36?#4T!M_*)Bvdo5Y-f+rkq=2>#; z+nt;=<78018|4=iZz8m5OJ!bvZk0u%Lf-pk3JgD?MC1{Dlo$k;4;M|!?|H!SW~cdnw8+*-(;F{!n%yH)5D)7+;&{!iVX| zP5LeNN@4L{-KG8G`G&xmOAiL-iG`!jn1_5{*b2L!bD9G~?CqdGBWu>cMF9Ht8_7Cq zeu~`&xqBbBf)ce4n~^sfArZ&16CS-N`N|-Fo(3vA6Tz< ztBdx@Yr(5uqK2vpwI}p)jp@_peXWNCeEz`|CtUx-0#ecY!k{y|{*(D$FW62GgIo@Q zETtBG#DglP{(Wd2&_e!WOK@Z!D#pWEI*B?J==8}txlM?Y2(I0t35qe>`Yc6g`O7r_ z$CNy)Bo@tu8Mj-|Gpzo)rt7E^KA(CZ_#E>9G6G%Z8iRlABIRc>?`#}jnh9}*-`*SX zQ7_KUC-|Eouf9rTV`bZ#vY9n$cKP+9<=}Em)AU;G!UY6$%T8a1Lp7L*3!F>f43@8^ zvA`21E|K9cM`-@A>?3>K?kWY!mVzM+AOnA!xI>GaN4ro-T-Ld$m#awt^(^Zo^X5{s z7{w|^nAN;k{qkPx<>Q&#Dl>dIjF6`N+yC#lQjI&8Bz-pS)mLu7yznP?-9F`~d_ zCY)vfEc*7V=lp@l2aU99$6C#uf9aonYzcpfW%W^HojZF3&peGiSsJ)-;nf=oQOHsW z1dvQlO`Jk`Dn9FsKQV?1PnBD8+3Y&mybtf-o3Y))uFWcZO-VVHV+H>^wMTbq`Ey78 zz39#Lyi*%h742zDt;qJvpE1U$h_Hj@_u@db3r$B3mve5pyFR#w%9h`WVx-~43j6NJ zo2GBX4~T7-;SZ`ozj%C4CDcA{OQGCt%?=OSVwSSuUHPk@9sC;}>F#IrtUp}diSaTG zk+A>QePY8!aZ&d^7N2#2n3=3+m2CjS*_y?qWp@~J$};(`Pv_q6_%|KQd`{F~C?K)o zuAyY*+ZCzg&D8RYLQhZXADon!&Vw zT)NCfZ)c5;N+?}RlhHa2`GysKa(`LNM4CTBXMO;z#o1=`L?Az!YuUC@j8f|>2d(vv zy&=m~41wL>nl#6pwQq+>3b%Ou167{(z`>(f{VYG6s|8AyeNi)}ZQV~cwEG(fvC+S! zlfOIdd6VVem-9@Xa%fC)KJfS-?Sj7-q%_&U;PMgUR3+iXwu+ZW%S2S^{m+J<*DlfY z-|ZgrKGn1%b#|WAIcExy)nyEO`(kMPKs$Q zT+kK^bCak}*k@6}gDQf3ofi8?$4t4pO2tf*daod6jKpi5uw|jmgq=D2F=h)X~5|c`uY_-*0-y9Ef zR4}P0;5+iWh(zcm8XPFe8#6d(cV&If!64qZ-GM*4M?vp6+Vy=aEqy~x?CCboq$S(j z{3l|Oe9GuVvRQjM`v$hn^Ny2sf?uO4X)Me2lLHcAl`~wV?vW`EA!7$KSrr~U%UAYB zHuDI27`-{toYXAy+aHizjR53rh4P!0HTqDFo)cOW*l}Hc`bBY@DgrL+`U*&O`Ew;i zh}bl*`f3;l?2xcq;O{-ZxBPJZE;&2MC^-RNv?IK9Ro|}Wnad#1cK^Yia#fP;xpX+Q z1u%MYt!DPHEqlD^>`y{x?sP&~#;1vR^BEEPjV3QI!|8*?cwlWDP03;-ksN4HffkIC z-PkHoo2#Ym+pKXh8NtKy-e$^R%XX3F*&eUMjqy!B;-s8ilWwMKnpT9&gvEhEZ6pkF`-`vrTjra#b$!`7!eEM zzt}S4Rnm@LXa=9xuSWEXq<8HsZYR+DwQq41IH#tQ$35v{>t3ewGSAlWul)@LWpBk= zd}n5ots=~}n}}z+D>j8TC`GvgmIj37F2e?z$QrpS>w$(Dk8C2{(=Qe<(=92El zQz|afU5fv!Hq*s&UV`Ey1ld3Qo-5bwrA0CxJ#4ZBUFzF=poasvrVw>DF10vN)u}`S zl6`Lft~r_7ww^M2;mr8XjVf#v>~tHEu_LM-t&~edCd`UWEJ{H^<*w zX(&~sdfQZEv*9XhhB%@w>{=80|1@e(B=Wtlp)5e%Y zE+i_^j)4NE6a05Z57Kf^#aHgA#^0_RoMM*&0kC%Bj3WTmYy7q;E)A~8)nU8pX7lA}8hZX?5=Y-&JJAly`1>+n+rRa*r3H81n$=GY zIqx$D)`RnmRrvlG^baNZG+jP-en_X!t#@$x72v~L+p+njDk~Tr_84^g8fO^>CeqVxX>WjY>>bX zu=-I>BmIvA>c+)&-x+cu*zwvNSb!A2SN(pQ>oV;wWO;yH?sz{;8k%VOU~zvxZ2HKI zLgSPYXboH&mGFLjW&0{f0H~VHqRwYAZxvC()7l~kfYZ*UIDS2-DPwFs%1C{Y>0+ye z%gvsXUqIbTA1&+$nkPFUCF+H?Uhd9Sz0 z<@VMZOE7iifjU6AXEI&6M-5HS8uTz8S-Uen^1B38KMn8gP- z-2J{*?5f0B`q0hlV3dUgvwhJOVTx&82kKgqMh3t`KYo2lYzFur0D0G|(8?@3_PLqB ze+L0k`};#+3FrrAebHpA3a8L{$*w))&ybdvZfPD7GR3QU9vlO1w6M;y9&k%!VUH6Z z_dzmFqX(leJ$}>^2gzjlYW8u8G;W?evgCd;Ct0uiyi{&By^!5aiRZ%Me$Cu5`X;`) zCy2^wW^}-HGm^6Buf!6KGCQ{kZ^7&Re$#x?8^LozQoT2(-eb)^Ib8o{9Be*m%PeIo zTd_ZEB=`7(Hnvz|mRG@xG+jc6!()Y;U-J0Dc(VPfKD}!#I8@e63Plbb#6QG$&#YEQ zN$inV@;1NqKHF^bZO6LK+9GIg^1=xnKQCt`dW(+7LfSTVxb7W+R)4oSiD#$xqgy4L zLF*G_VKP_|HqGYGb^Sl+G4+`r5$KfPm1^qIoK;!PzY*AJz%_HF9Bpx(qzesU7W#v%#^} zm9sPC6dFrOAGlU>y6{f2?vZ&DXOuufN!7Bdg4;h{$$ZB+HmSrc9Byf- znSn2=uW0-V)c%Qz(oJ0961e+~T&K#h@hl}RzKieSAe|M3w9@_iL#Pu47|Dw8)d>ez zog~bo1uf~4Lx2&~^;JvH$!+t}rHwRe%QNxmJ$|J(TcUO|Ttknlk*$;qU1}nvk75(z zIj7YFxrV`C9Mo6ZGMr|M>ZHuBWC;7b6#**rhdpL<(ig*@yygi(Jg3p%YfZ=z@Iirz z8UX$&OD;?r6<`@Ta`@o;ZQ}m?k!Y_Va_U7fqN(7pIXx?>9Dv~a{>nMVkg-w;cZ4}f3bo)NxLmk?} zplw&-?>rO#;8TruCL>K8^ZPVxKDd`-&9P<3{DbLh{oURQzn!3YvN%(0{RgM?7?B^u zC+{zvuA0b4T@$}g)H9mP+qPXlXC)kz4R<|Wk3(DFhLH1nxXStjN}e0-+P-9z*4{{0 z=Q_dSVS5lJLj=wyx9PW!@Tk=hzkSi@hdzE|3*@=KH)%M7-{ENDWU!Db!?*;Ksoz

%+OKGPFeX)MC(3l5c6(N1)<43%290}Zu06U0Ry??Dadj`PU-U8xw#VXwfJCLM z@<78#x4+mN=`+UZcM4Xo>E28pj6S8JksA3u=K5K6@_0Isz%W+MYnBo866<@0Q7}6b zI3jqea~uzsC^70!BY|NCMm7itBI-)lvmqHJ>)8Wd;c=9`$0Zo!C^h7rjG*Jpkc+NQ zoY~Ept=4@$+(;nnP)#N8nN_QmBxv+w+Uvn*y~MBqaXF1%k6UvCsVPsH)^5kTiDHM@ z|6u`wCWDO`<}Mbx1zfd^2(Gpp?;YqS=T_ZbLVAWUqf1`XHm{W&Z)eUY`aGMfZT^7W*JJz2PlNvBiiQ#(Pr%<<=-PM za#3;gOz28ZDES=w-W^f9y9St&XPoaD#zY|CeL5w)dw-rEa%rqCXx(8iFkBT5Cg z4a0BZWU`UThKpYkAeWl%J1~^W8VrV|n+eD>)@PVI<^H>&H`R31s=te_cRi@t$qjZd zdcdi+$T0u)xmIha(6C-KJwt+mDPbY+%f(JC;} zN`|dmaDQCX-A+1p9BuG=6Gfz$OyTo2V(xTCP@vzR&9rKcE}JWtT7!$T@2~af4jDnw z1PQvv=hA^ZNEzj%WBzA;d!`p9*Ep0kUx942XBNo!7t5(JQE=IFkP==qTs165iJ?nU z31nTv`CYX|iN--9O>t03VTv>>14*J0V>eYp+HSi*TE3r|uDAw6329knL7A#|<6Q81 zSt+wH>2Tx`4T~AUyfbs;XDNQB*OXJp=ne0ObMp23y+XVV!)M~PnZZ+aqL3Dz_1jE1 zmPFWd^N1ukqgE78jwSQ-^0xL10W5^(-@y8Y`+0)yChyCHYQn-N%Sj0&sO)f>KHb}S zWwpi8){(}TU|Y}3UfHZDqAb2wHfVoG`4Yd6hr^d}bb8Hbv%6c~2D#H;uMVif4?04- z4G^sTqOJ%FxD#DHQ!Bu6!|NJMW-IvlDpKWK-Jj!E-@wbd(mf$C)JY8^sGW-JL|Cxk z$G0d(5EG$eN$BeKsV9|9s z7Xr(#YEhAFoi(f_!~b}~_>R6hgcBst5o<*>66C4Ok+SN3C;!L+F5-D)$cg__MV48I ztVfHZOOAR~IfljKWh)V5(vKPra6V+-*Oojqxzve(#tt=nbVgaK?jkQxWg$dZrhW_W zKKGj{sKA$p-y%&DZ$nmUUPl4`d&K05TVjc6SD&zqm?%7!A-u|G1J`WNi5L$0`J%ix z-iW96@qs}51*b374aKL&9T{OLhn<>cvpG6(j<3O|Of`!WN7(nl6B~&_4ioERe0&4rvpL+?Lvk}FaPGD z(0DCTcRFTetls~YPH~~PJD;0`JiXCP)pw?jHW^b=9=I1OcJi_ns*I;~jr3*=Qbim( zYeW(7;x>AAN~4XkF*FQ$#xEAx@^5N92P6D`@Ha~nqXKGj;C0lIAj0L23pTz6VUyB( z_e0}KeLMp0Cx7-l!rl(hvh(^2X>ew}GYQEEj+U}hq-;&5koD)bTlcYTNsRi(X_tK% zf)Y@&vOEar%M3UB_6WN`x#NdI2IoDjMpF>ytUbbJ9fUoUv@N^iG4JGK61`4TqI!&# zcvoP&l4E!x!sk2C7hfEy;RUdBobho{kIY^T9mQHQdF>}DWoP$#oMF)mbppW-gJ4mp zkZ{<$=x1|kA2IaVx=^d$ior*;MH&#q3K?OI#fWB8Jp*w|0DXTZg>pyR_G6~g3+D>~ zQ|{ls*7Yf3%9io0jdPew3)vs_K`JxniNMLlG2#*)sI)?_^xfQTz%-QLan9R3VD z`YwMJ@2kN8N0u$Ot&@MWdD_{SMBW4Ym0X)|E8odb{@lW%!r7Ag?_!l86cXQ8SR0C3 z1?ST`d{GX50~t8nsegVAk{-Cz7UMtl$z0{tW6Tn$F+bUTsYwyA{%|5H*`K+~gnUUP zX0z5}V`g7VVpp9EV?>N|ktv;^0~%wHu{?IyD&riX-ldgo01|O@PPgdjF6#rTzWyil zBpOtF8pNkkvr7&%F^=Yp_Y`Bq(^Ix+>t93?j4^zc1X(*mT1;eb4P|fZ-8bSuC?FsX z#7oIqjFrI4>SEDMl;@6MEmj=m0PjrnF|bL>WOfpMr#UL`91p7?Y1@(sEGrjgGLUwx zyC3-|;r6&u1#jQa_KjL1ssT_=P{`hL6C$=EtHBrjmwx*Jm4m{k3p46IB5lg53GIXw z?%9cpwCk`k$Xa&5R4*7r01W%aX1*kO4E97&P%)P03=5@%_4*4Cvifvl;RDe&3JEV$ zMSZz5J!O8+of`rF^K7^?Xu?FW(An4BSN#-Av1Tq4)7k7zG^wMLTLv6{Kdk+%R`W40 zSmdKVpoXO$bNPkN5ogFjTBX`fR7fPNJe#o9Kk#8Azet}(tEIRxc#zKQ+{8NUjx}>b zilUy5eov)?asGK^DBZ`x5fKqSIC93@499;_;;KSsmLAV9n1}Pb`GvV4rsn|P@m76p zLe+OzU~0UV5#%`fHkRzleJAEZvHn!Lh&%N`_YB8LW!3reOsI(VD_b1W9nev4J;%H2 zTN7f6+?5flW$g5-ylEP-tsRnKg$A4~uA(U9K5&5YeA{p3-sC@(KVJ2hT)-w>_ouLU z<9JdAq08=F20V?S1oIzNOtx4A&YG$Jt<4kf+J!ikADMeHFC`Ce9gZLNyn2dWse828 z5QlhsNx!~bmw_vIIhXoHX7+hX(FoYEy~;G$>v-<{f0~v<#%EQd?5wWtc8aCgezZ7U%5Uo2aHF{^jcX7+w>Z~gmhU=20o9|NL=__!*heN$MOzGITVo5&3{eFK&+r)(*x!;b(z1hPbMehlCpJTw?|qkUa?b%K`(eq>*{nmuc;fAv zF-YT87?E*BmA`IX;<^NiVfK@L&u||&7H09T=xXlAAPexFBrqcYIl@=-`o?G|q`68& z1#efDo=;1L4{ym^K>4o%b~eLB)XQIcz4q()8=0BX!e=LrDsn$ow^+`w-_z`Z>>;r6 z9jZqqRvLko^~<(RD-xRY&u7l_^^A7(#HdERpL*@yWkZ=D~mJ18nfA^d}{ zUp3(G$_^Kx;oAp0wd0F%ofNQYclA$0iR#yKG!r;E$}QYQNU5T^Ck%SlRDdlpJvu(7q(K=*D49*NTLlZK1Fx+*k zhe@m>*ajNFGT*35<{WQUyzkl*?EZ6{+E&D-nm>1aJNUfoT`RQ#e~8$BsThvne!)bIk#jGdxBYKa zk4Be+lQd7f?v?c0PDfvLDq_d0f0dR`fj)#;^i4&GJAM{(C>TY|t22i#gHl--=D4T0 z;QekQ8YDJ28drtMm=qS|AC38ceZB7(wSU0%_TZn_9<*hh)nsPJ*w&-N+_mhz=goZ# z3{Y<;pJQDfD&h4{bc>E^Y>hMU_=rAf>HNW$*y_x~pUv8Pc5x-=uSr74!%e)3k$Vj} zeTo!`jo{t~@Y|C+JsT^%R{?}9kc=a7l-id#&|50~Y^z@-BfxOeJ$?IA&)48Ei&Y!d zy%o0IA9vfcX>fGJh&aPlfd|1_mm7dE9h#^Jg#epy+U&YnQ7$8Byk{??X*`6TfY2Gw`{3gte2|2V<#>K8x+LSAsNCa*sc>S%lx1CY^G3FL zv@Z)b|QYotRBS24KGV#+!W{*&u#-3 zG1{4kFlMhJzHvb7@C={b*nqoPagp00WA1|kx+%JpzedFZG*W+2z7{2{gf zVEtP&J4|R5$EV4ZaCUI@eXXO=edo<3ha~BB*xyrKX(vvQ#B1$Dq%vL>1;x$RpZ@Gn zf!lSzG%8ToIGsV4*YAE^4iGi5LH${~|Gf;FEZ?!BT;sA3i#l%6CrcwiPIpZVu0(ey zzpLV3#eFr6ZkN!(_3e1DgUzAf$52qtX|)STm$+@ab4pUv_j4Qv1hm`q^ZE+@rt`ny zvI0#OR42-qm`>GQo3cpwDOqMXKO^nv(0DP;IT5(Q_U*k+rhr~A7Dl-;M|j#N_X1%; zS;wRPFwE69yXGedNGb5gO4`;xG|2HBE=h=3E51nPVA^ejHgB|3G{-(6Y)Sr@B~MpS zGw}NYu&4J{)bCtgeEM5{vZ$cxPxi{K?s{E>ipf<#@g*6j-qP!O|0iT?=fA}xOzwSS zemq;}$G~~yfJbe{a5Cw{iI9Ek5KH)+=fT0M!+8~hok=iTz^=2C?<2o7Ram|(v(knk zYq~A@?7?$I--lrwVDGny&Eh{9z*q002&816m^a$6q>h4y_79 zy~1pd?M&k)RWDtJRa9g{Ch64Oun1q^ZOvy_nPz^O7y&u8LG3HBc_rDHHYKZ zGK5(pX6Xr}zN?<6*IiheB@N%)AhQ+8aoW13NRh3=w{)`^IEgO=T1dd}q#Zc^I=lfq z7XOCRjT|<^xKFH~ZqWda4BsYJ?_K~ZfAD5Swryv6{sdzTaTa?Y{FQT`>%ZXJsrnyP=pi5LItKqkzICrwrm`O5%Wfr1hkT6kDD7{kYFQ-hAl zpg;?vUZHQLO8k2tD%}Am+`WDsSO=zjp*A?vi1M*PTV*S+ccx-nH2MC(UrN zWD|5O|3LN$h6H!?jjsE{7S?1siT%Od_{WLCHlJCCDzr#hE)-7`7fDzflPwJPdO19C zhL>R%9jgD$jTK%+7)tN29!^%|u4?HQWHOl8KIi}#VRP#OLG-W{*hE_Iu`FE_+T5@7 zQ)5lJFl$hs&y&DT%lO2jB*Y6{dHK7RdoyF!)43PA4jN*`n;EytsSF>OwZ@-4Xm|OB z*UaznWg*gR5bTJx)~qGyI|+7p)cjP`Ifu{t%EwWMs)0=QcIEILXidm<`I*Qefz2jgz~$Ue!dLwz<1Q>477@x zP)LnyEs@!jPU$@ju1Vh$32zOK;?TqWsu~>?eEvCqMb=Qm<`PxU02-nqZxd_oJAr$~ zFs*QyR#MU)3Hs08qpMyxq){)iD1S)1;^uWs@$Hh+guXsQKN;++7j!0rXfsH>v3B&WOB_A8kUh+@x^4Ke-Tg7~S>jO_(iPgE$z4GC?%?k)^N;4n@~Wu~YF}Du0z4!;xOcb5WgrgZ$DC(p z^S&9lBe_T~56Vn25`$Fm^6Vj?vRtW`y+uYwzQQePkt@dPv41WyUFfnvD zMVI7wj`VT@T#DiGCwT(n&h^^4#YWAfBP)H2#^hO2!3%KjrKFTY%E z1($qxR_>|xd4h|@unVY|O5nev)Co|p%skbAUj|U{v^X@K^&EWhcEa!X#Bpm9l&U^N zyw>q5ylbe?Lp!So8Ufoy!MuRve)F`7@Xjuq!yfpTF_#xjRBGm{1wf`o&BX%O7ZBkuJVC11-r>a%x9r~a@(YyBQo2EZdOsBszL4& zl?sqk3X>m{rM^PJelmm$y-J{gPox4bh+BEi?9 zfK*UvNnw_N@byGoC6FN%*Xt^PM=aSbG(k(AZrm%4Ha%y$k$6?qNGozXF0v~fgMLAV zqLrA{nk9QRE~F4bh&SM={`J6CS70Fac0TG;ifaY=$mQrR3lu+YLPS~(|DixURfQWB zJZyd9pTN+}M%!W$(;Gnz=a$fo536d>dh7uGrX0>+?8^)tdhU2#YI*ygDOR9$ z%m3+fbm8C|SnuR<_Gs0h+s!VEyD!_v!{XCcD(#yA@xo{k>CH8<>)2wab`&k`PA^^_ z5qn)qDu$@0ICi0JEn$=8CvZ5bVB+t_ZM0_d(Z}{`=P1iso6N|;;~_vq{Tqt)oO&oou@9hS@pPmI|*K<1iLru5L15V z)>2KyP=Zp527(cDd4o(T*H=P(UZd{=YVzzn-0r^@Py~-a4kgI(j-*9r#)KCyMcG}_ zpM#{%&r;zy-N{hf_i0PjMb*0`rfB%5{d@y{q0&)b0mv#KOBarGC=xsQXAs`%lm&Rx z9(W{o)N?*UqelOzz?G2|->kFVuk5rMsBtrK1<%|mG%~)|buXl}2Nn7XfkeveD1wv# z58|j{oGP)~#665)mpEyF_Bv zt|(LwjG8}Ie^>Y{Qe%M{5z|bW^(9&o8vhg@LMDH(Kk7kB{4?8xBHX5WZ!%qdUKJ%C z*+zaVOsc<;?X%Pf%ipRlIsl%u6gDla;-QRP1@K*yNP$6Q;`lt6*sw!yROytyfE8MaMClp=K?u4`qGxL4w8mXs`=^Eb=!s${$?ojT?U^tY&iT=EY% zOaUT61wH`d18swWFj=g>t`Pk#SME+7kPH>32xYw8@k`ovW%w1bj==M@*FMgL^S^2o zA$REe+(o7y0ICdO8PMkI@H@;DID8a8LTK1(2<)shed)Zv&a2@I{&!y1ANKFOZwaBy zF@NgNt9$uvKf1YY{;G%PoMn$ra>(oH#6MuO2V;QE%>Dv@NEK-P$1`CIUy?nrpeXXw zPyqXu`nHSr4c`-i6P2DiUBWIZhiU_-d)E9bU^s)46-uGH{WkwcBtXp?MlK@;=u&f5 zZ*`rbg=*-FBywoO2v1!wbzEp@d@^;5HOmGZqSPM%3K?c3+*hV`=Pe@L5$?}PQE_=L zOPyDn;ORbz%{E>y!XQNZL0KVdTk_?>l!~;9JGvcp1RA7!4$&N1J;K^0{x08zyVy=+u;2u1%?;0){Ko#NGtrcrD@m4|pQN0#-~UZpq5!6x2^00eGBCT` z&gQl<^Etl=jHX^TyfGaSA%Uo=`CUXx7j&&&+d(~@wjXQyjSr(f#jM`T^DPUyiauVe zJ8=U@F&4&JbiWwsY~v&W|2!{PUFl3+Sg5#O2rc9n%J2M>BKOBpWdSJGY%2jq3|;yO z({)J96n)kF%;Ea?b8J&h)giGNc2R9js9Z^SNqjobOP&IG+@rzp@mcWjf597|K0y1m zt4g_O!v01T0o4Qda)(86wxUnbIXAMLbe>rzz zUts#;^^JfqGxFflo)~=GTbnO?>fFC5NnLgr*XIVxp@My5hd@gP zc}$kMDhiSiu#08j{&rvmpQ-UuJ32Zy2AZwwH2G3bhW2EGR>H|&cQy|TF$`)R4edBn zf&M%t-)E9(sro~*UiT*MG%kysUw8|C(FdLR(QNUv9i({*-)-h~Xykc#4`skasJBzK zSqQvlZdc`hJ$9$^85OPzr}$UHX|}}mTS2$rCEOUtyE1gxeL=Yr_h#SR#KS2Ae{H#H zwb(xE+*g2Z;hNO+;rk)jM(WYCkr19whLn|HZ%?*KwFM^FlNBI4I{e4&d5xFV_FZ~J_hZ4wvAkfF(C)#>15BSF zpae0+8Hx|y1PhaUnhfHfv5%n&|2GRr!Z9mNJ!JaM^RmmUgmhj{{g0M9E7uG8XbwYm ziV?r$bkO$(m%|nNYu11U>xV8yI>_kwoiJJVwr3eQA)MekmjcH z9sj0hNiJNA!CLEe+R30~$cHcXJLR$6t4T_GP7Rp-!KBAdSA5-ScRjz=qw3sWobrM< z$6t$H?b1cZq%P#(W@b>h)wrnRfmaS=K|pqReQ<;LiP^=Oc#hHrh6?=gn8O(ogHvkDmp1YPN&)N>1}5G8hTUWzFuvKXq=gP z5j+cZ686InAakW9e#3t@D~l!NaVcZor~^*coIARMY7W53sx)nncRyKkc?2pVy?*rc z*q?+oK`LGicjJiv-g=)_>^((9p7!=U4MjAd*^t4#Us2f{d=i6#DUcDzo(o7l{|92n zlSWE+(mTGF(I+9?vHW;nHS9Ikg&T`NrX5&_38{Y|<*x0ZJ0gqX@I89&yh{0#wOz$w z$-Fk;svz%xS~K#+)t#QiL!$eB{UCBkL}AjSP{j3)7xxx=!;knof^47rRVxDn5*#O)zoV@7B&Va`9MgSrSDs1sLX zwQDEEV5NfhLC~=8zv>JSA9k`Ij^^PC)@>vw$upH@03gM*iTPip5l<_^kueQo$ZxD0W5%W?sWz9*U8N-&He@p*W$(LK^wHrV@E=2tw3M&_qB8FkebRN-U8xWHq}=v zw(F?GcD}Cj&((7|%{cseSa@FN!s}&YtsPwuJ35ff5@MU;2x-Q?k1{xoQ4WFKfK$9+ zJ$b}PG4)AY+T79_>HhM1BRjsrD(@$&@1qZgCMUPnND0zwt&(hSeX4@+;gbw6<5;Pk zAD;fM-ge;W$*JZ+?IV|;@ACuo;?nhwf3mPbUNEUrC90HKlj-)gZz@cZwXc%T@CI zyw-7`^slL#er79WVsY=Hxa4Gl^rnfb_|s?N)<9>rp6mQCr5yuey~{r+ec~Yg=Ml{y zlvC8ha|*EaPntS}-UhZlv@HPV{mGduA9`bd4a84rs2aII?R+_2LDT#1M}X^fez$3V zP41hMjmy%|%^SV205IMQ?zM!{0Zl|+6&8RhU~eD~RKoz9eeU*=5kJV7$CCBB1)yL% z%J8|?sE1gwU*@E!w;QlP@ezg=ugQgFxBX`s;a6~CX-|0pZJT5eyO9W!e9m8zEW@wx zR{?}-xI?s1TxR42dyqKZAuGPo*~Faj(z7rVGNc%Nx%&4Azj|rVpX_{!xTmJQXfn8F z*|h=CMk^&cfZdRk(I2*_6a7M?=GaN82~P+*YsV`c1VK;n_=?y}4L9c|w zIy`@@U+_x27e=wPYOcx)#K@5BkWWdqwa9hyOH6*^lyu2q7wh4O<^2uV^4}TlIsVLp zubx+R2u$HbRu?&(YbjrG-)m`<(v>nk(S|HlxH>?d$=E)Sr7*Wo%rFm4#}tHhmv=Zv z9%He0yfAJ14uSi^^fwb4TVEFrU(*YaYmAYarziZ<3j9IRMTYg+{8>kRm5P%;9^Lz^ z*R9BmaWV##Ek;kv9X6R?_V|pDkq}1LsKhjvue!Jo@n`*ih2P<=DHlF9Ro6HBvf&bf zUDwe78VMm{__*sqj=-;c1@Ca5)FTz+wauSzKi?i$te49#Z>%PgqjSO*O^E-{%NfKf zsGZNw?^T5w-qoLiVBYic68ET=BF}rva!8$r$TzhNNH`B{zad~g7xJ3j5{5&xQ{BO9 z=gO~2R40{Fh>ZTscA@P;!AMz8!a@n)IsFtft!W%Lx~X=u?g&7d)Q>(I=uJ^OFPd#M zWtsPSBeZRKk|65Byr!SsQ-fc65?%R=X>>kKo(k3niDiD5$ zB5~K!+|)w7_2&gjlC31-16qAW`U_yTo;%q+el z;8L)Mb&z*@Gt^?3SD4hUhP%MV0MR$Qjeocv7hfd8WmN>`+%phdG8YN{+2 zd?WF56W37^hClcL&G{bhC5ROmw&VXr<^pXWv4JE`DZZ#Y&RA1$BH||nP^e--dr_;sRC|<7+ z4)=HWUTpu1y4~e_{yEx5`&JiXEjZ$HqZi`Mem{o&7i%bi4`1i8mAqQS%`P>*rmMcQ zbd*iqTs&8r7k%w#I->qb5H#i@wV+}0VL-92(0|jmpz`}bRbm?(Iv@0q$4JeE~W{8rOC8Oa=8P_Ui2K;Tqug3>wdJ^^kMQHHkFVFy{dkyuup2^fZM6zWdZ8yt z=G-exQvVhjO5kew_|0?eU^nGVu3+-GS%t_tZ3nLel0q`4@?f7%w9cl>OJ+Z0+XrrkC7@Q+l_IM#KFEqr|FV3rka|F*&2p(2b>!9ZcN{Y>AeUqu>l;qrlRvlO?i zD_8`zxz$*&y}Cv*SSo(3cKX5$hzOKCu1)zv{2KNj!=5%Y7B~}eMpi$2-Trgn(XEUUq@@D zHgzpqcQN2np)RB=MVWnq1#~+bZB%Rd2)bf|A}b!0 z;edO3#sJ)HuHk{}TyIh!j%L1*wPNH&OUW-1a2gBA-%wnpin2o1>rqCv;;_ zbe%oxPhN<}B>wL`8oHk%Me^oTM?RDNM|s2>7T(Nkv{e>n5o(|pQA2N>osM8pv_FTFTJ;S6!*i$`$>Uz<^#=?t1zv;6}#zvO$H9RD{9Ags?7v7wj> z1d0ZD`J3NRsS+8)npBe^PuaqIM_+v4La9cahqBSYqU#2&l#?FhKT;H@`2HRP?LA|& zKRTnKS{6q`*Fd?L8%<)7JZ&|FBX$N3q-S-*HLnYDF}1!Wvd*cH53#}?i*0v^)AXw$ z)N?8%Mz68}@>VJ&)1a|q$sFKJOZ3bBplk%}^=As0HDMr8ha@T-2$E&p010>C^J71ID1Ueh z>uI{30_m4EcLsLOpt$C{Jmcp@2pgOXH$2+7o zum*!;+KMm38{V6HVSDXPK5RH3lJyYMapArF0;65TrxeRt^DFZGuRgEq*1CrxgOVmOAYTQU%@toQ|A1<5+!it>RK~EmhJKBHd1VwIr znOOTJ_Z-kLjRD0^E(Z3ax7;fbP(afi+1bTIM!a9&CJaKDBr=WK8kGeM$FY?K@k$1_ z=}qr282cTbPhXbf-46=LaRj@HIIh9FLKowlWR(NgBk$a^&G!Cw&t`;^?v`MIKp4Mf zy|!NW@lpWIGYFt@8R2|6u<(k5fQDWR#rIS!*FW*hI5?CV?(mihlWM~Rx5 zt_DTuv8_-s8-}geR8yNd~8>;;2 z>im3kNRCtK$rN=p73k9r_Ay$Fp2)3|@1{s@FV6k~gzHtHOx$t$X`7Nd7&%xix6pAd zE`Y>8oXmhC_3YeDM1DSsjsmxxcp;ZN!dR5a#VX|z;|XeJmFk$7R3%nfh|yznBzVEi zA^hrw+_9Y#BxQMDgC418z55zE&55|&Yv(}@7Qy+fOCA2LjHw81oy#Og=e{N6!EG1- z59mzM9qmZoUh0j_pn6)@U|^(eOsdAJcCVIyZ^!50!>T2WOxI0a(UQe9p^4*VA+kz4f?3GIBh zHZhR3%PboDEz;oy>xL*`5M}zgQ`JqvCq1=O7#B8HX%k zU|kYMn!rjFPGxEmFy5Q_>Ew%-bEo^7nN~G$DvAM4*H{tIUpTi`T2Dzje*2P&;lv zf7_7gcq*v}^9PiyFxJfCe{V!vjqskT&*bMP6Cp49B#l&EM1|B1fgMq~p^r-NzTxqg z0mJ%^q3f-w*-^vyOBaoTmwpP+`|j9YL7QpT(z^+}C&pK6f$MOiL7#QfRwoq_(S8^N0FKs9p|vipNd?<-4f|U_Ugob_h@m9Cx~I@G zL|%U}x)yfAu-NQXQi9QQ2AElaz(5*lB14#b0{WVCXz8DEigp@~KmAwD) zT{9(*f&QS7uk+bTl6`%{J-64Nd%35D#fidK+$bJ-;5^1o-5+0kn|YSi$14`V1SoG& zUA6c+M_XPn@fEO_iid#yWDJjv_TsY40Fcg26#}k{{_^3n_Brt-@OT#eB}M3DE|-2Y zyyiGKZL(TNEuA+AJve0J_jv*&R|(j7CPJPLGhslt!U*`^OgZ}*u>&R*Ii%gke}7aD z#^O#UjUn{K4ic8FhPc!(M9OuiKLjG8ZmUr7hwc3K07Z(RNu_HDYY~Y<&~KDAkK-y@ zDjat!%scGD%?py*zVK-NTDNE0)&3t73W<_{up|M!+#~kD{7$uN{A>wM9x4lOJOTTg z6f=D}m(gG0Is?O(r018++stztDwg=10>s!J!k%u>m{`Z=urqcWno8*3sTC!9I>^x1 z7clq+@!z6&#dt_bXLOa7HLF8W1dl3gT(TPqBK95aI))bg{!4MMRe6eRzlgwz9rCx! zI{OrnYHE9NwX?MN+@#@*tHhU}$1Z_uzHujLv`1cx#8_vP{i9B=wSGqqsqaJme{wOj zgUiM4z7Dlt=>p$Yg~@g9K4MvP@9Ms}$$s+8@7Fkc1eC%R0!*@^;3aIFE=rjM{wz3K?=1KT_6gT22M5O|+)t3POSX@dIN25aejk4f? zA~PVge3imlfE{hW9C_fG!;VI|nr_F~WqO)V(0^U&NfON%XqepbF1tgPUH2jTX1WA} z?)z`U$Ui~hmjo8YPeM%D3B2H^U3VX)BUVB_o6jdfj<>(ihL_9yeY)H*iC4Z_90o6W zg*;9yjcl=P*!UT}*}9IYPQ!4!PeSDQpqRP8<-;*9_D@p$*m6Hez)VeOU|a(5!~z(^BWS9x&c7_Q&$yh zm1Wz&0qP3sheh8|1PdGS-wN-l?2Ue(4q9Wl8rRHhJ24WfxTQn#>s58G0400>uZ`tx zc6r-DZ?j%;?=L?#1WA?@!OP0ij1=Uavx-9MrFScsLyC1pxar=}ZSsA!6#Sj=>aO{! z4$V`_M{_=jzN5K1FG#5TXoZo{-R&9xi;LTU3x3$m9CA8OwKG#q^1nx(Q`&PiLlq%?UM&0k8Dm|xSIo9 zPO%zz29>K(zdj%oM^dH`|)N*3lu}&TuRnCR*`@g%At4e_PXVgju#2>)c1o zcy2QMu$FSjaPJ6LB>A-7(xZ{UtA1d1IH3PnQ9vto^kuWy1gI)a@yd)3yv|F1E1FP9 z%2R^`qI;1{>m7RPc?uq`S{z}V|wdX@YN1(4NpS|@xms+ptuRA55;8eps z8kYY~-Srq8Erj;Rt%=kOfs{0+xLv_HDS~ zapaM)%3K*4yJBRDGkfR-o0^zB#VIEU7hLqWEdOxE@F4Okn^fI%iF5C3eelWntyg;# zLAOd^;tD4(YIF`{_knq0d9@@5g^jq@8^jBLURQoCF?!cya^IbVd#e8Y>REDi+M$mj zf%L}QY#$ZDoOJa8pv_1>^IKXZ-Ny#?u^P{JKikc8T$XeP2OxsN8DsukHH8P{T1L5+ zTDFW@$69`h4AxbXFYQg8_elq>(^mAkIa8v&yC<`V4V5AMOZydi2#@rzO40KZ%pit7 z3za(�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 B+y?*v literal 0 HcmV?d00001 diff --git a/apps/web/public/brand-symbol-maskable.svg b/apps/web/public/brand-symbol-maskable.svg new file mode 100644 index 0000000..d15bf10 --- /dev/null +++ b/apps/web/public/brand-symbol-maskable.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand-symbol.svg b/apps/web/public/brand-symbol.svg new file mode 100644 index 0000000..737db9a --- /dev/null +++ b/apps/web/public/brand-symbol.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg new file mode 100644 index 0000000..0bc249e --- /dev/null +++ b/apps/web/public/favicon.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/icons.svg b/apps/web/public/icons.svg new file mode 100644 index 0000000..4367741 --- /dev/null +++ b/apps/web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/manifest.webmanifest b/apps/web/public/manifest.webmanifest new file mode 100644 index 0000000..fc66d16 --- /dev/null +++ b/apps/web/public/manifest.webmanifest @@ -0,0 +1,35 @@ +{ + "name": "Xdrop", + "short_name": "Xdrop", + "description": "Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.", + "theme_color": "#12140f", + "background_color": "#f7f2e8", + "display": "standalone", + "start_url": "/", + "icons": [ + { + "src": "/brand-symbol.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/brand-symbol-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/brand-symbol-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/brand-symbol-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js new file mode 100644 index 0000000..66f90b1 --- /dev/null +++ b/apps/web/public/sw.js @@ -0,0 +1,83 @@ +const CACHE_NAME = 'xdrop-static-v4' +const STATIC_ASSETS = [ + '/manifest.webmanifest', + '/brand-symbol.svg', + '/favicon.svg', + '/brand-symbol-maskable.svg', + '/brand-symbol-192.png', + '/brand-symbol-512.png', + '/brand-symbol-maskable-512.png', + '/brand-lockup-horizontal.svg', + '/brand-lockup-horizontal.png', + '/apple-touch-icon.png', + '/icons.svg', +] + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(STATIC_ASSETS)) + .then(() => self.skipWaiting()), + ) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), + ) + .then(() => self.clients.claim()), + ) +}) + +self.addEventListener('fetch', (event) => { + const request = event.request + if (request.method !== 'GET') { + return + } + + const url = new URL(request.url) + if ( + url.origin !== self.location.origin || + url.pathname.startsWith('/api/') || + url.pathname.startsWith('/xdrop/') + ) { + return + } + + if (request.mode === 'navigate') { + event.respondWith(fetch(request)) + return + } + + if (!shouldCache(url.pathname)) { + return + } + + event.respondWith( + caches.match(request).then((cached) => { + if (cached) { + return cached + } + + return fetch(request) + .then((response) => { + if (!response.ok || response.type === 'opaque') { + return response + } + + const copy = response.clone() + void caches.open(CACHE_NAME).then((cache) => cache.put(request, copy)) + return response + }) + .catch(() => cached) + }), + ) +}) + +function shouldCache(pathname) { + return pathname.startsWith('/assets/') || STATIC_ASSETS.includes(pathname) +} diff --git a/apps/web/scripts/generate-seo-assets.mjs b/apps/web/scripts/generate-seo-assets.mjs new file mode 100644 index 0000000..a1bb550 --- /dev/null +++ b/apps/web/scripts/generate-seo-assets.mjs @@ -0,0 +1,278 @@ +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { JSDOM } from 'jsdom' + +const currentDirectory = dirname(fileURLToPath(import.meta.url)) +const distDirectory = join(currentDirectory, '..', 'dist') +const siteUrl = process.env.VITE_SITE_URL?.trim().replace(/\/+$/u, '') || '' + +const SITE_NAME = 'Xdrop' +const DEFAULT_OG_IMAGE_PATH = '/brand-lockup-horizontal.png' +const DEFAULT_LOGO_PATH = '/brand-symbol-512.png' +const DEFAULT_OG_IMAGE_ALT = 'Xdrop horizontal brand lockup' +const DEFAULT_OG_TYPE = 'website' +const REPOSITORY_URL = 'https://github.com/xixu-me/xdrop' +const DEFAULT_ROBOTS = + 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1' +const PRIVATE_ROBOTS = 'noindex, nofollow, noarchive, nosnippet' +const STRUCTURED_DATA_ID = 'xdrop-structured-data-static' + +const HOME_TITLE = 'Open Source Encrypted File Transfer in the Browser | Xdrop' +const HOME_DESCRIPTION = + 'Xdrop is an open source file transfer app that encrypts files in your browser and keeps plaintext file names, contents, and keys off the server.' + +const routeShells = [ + { + outputPath: 'index.html', + pagePath: '/', + title: HOME_TITLE, + description: HOME_DESCRIPTION, + robots: DEFAULT_ROBOTS, + structuredData: getHomeStructuredData(), + }, + { + outputPath: 'transfers/index.html', + pagePath: '/transfers', + title: 'Manage Transfers on This Device | Xdrop', + description: + 'Manage encrypted transfers stored in this browser. There is no account or cross-device history.', + robots: PRIVATE_ROBOTS, + exposeUrl: false, + }, + { + outputPath: 'share/index.html', + pagePath: '/share/', + title: 'Share the Full Link | Xdrop', + description: + 'Review upload status and copy the full share link for a browser-encrypted transfer.', + robots: PRIVATE_ROBOTS, + exposeUrl: false, + }, + { + outputPath: 't/index.html', + pagePath: '/t/', + title: 'Download and Decrypt in the Browser | Xdrop', + description: + 'Download files from this transfer and decrypt them in the browser. The decryption key stays in the share link fragment.', + robots: PRIVATE_ROBOTS, + exposeUrl: false, + }, + { + outputPath: 'not-found/index.html', + title: 'Page Not Found | Xdrop', + description: + 'The address does not map to a page in Xdrop. If this came from a shared transfer, ask for the full URL, including the #k=... decryption fragment.', + robots: PRIVATE_ROBOTS, + exposeUrl: false, + }, +] + +const robotsLines = [ + 'User-agent: *', + 'Allow: /', + 'Disallow: /share/', + 'Disallow: /t/', + 'Disallow: /transfers', +] + +if (siteUrl) { + robotsLines.push(`Sitemap: ${siteUrl}/sitemap.xml`) +} + +await writeFile(join(distDirectory, 'robots.txt'), `${robotsLines.join('\n')}\n`, 'utf8') + +if (!siteUrl) { + await rm(join(distDirectory, 'sitemap.xml'), { force: true }) + console.warn('[seo] Skipped sitemap.xml generation because VITE_SITE_URL is not set.') +} + +const sitemap = siteUrl + ? ` + + + ${siteUrl}/ + weekly + 1.0 + + +` + : '' + +if (sitemap) { + await writeFile(join(distDirectory, 'sitemap.xml'), sitemap, 'utf8') +} + +const template = await readFile(join(distDirectory, 'index.html'), 'utf8') + +for (const shell of routeShells) { + const outputFile = join(distDirectory, shell.outputPath) + await mkdir(dirname(outputFile), { recursive: true }) + await writeFile(outputFile, renderRouteShell(template, shell), 'utf8') +} + +console.info( + `[seo] Generated route HTML shells${siteUrl ? `, robots.txt, and sitemap.xml for ${siteUrl}` : ' and robots.txt'}.`, +) + +function renderRouteShell(template, shell) { + const dom = new JSDOM(template) + const { document } = dom.window + const canonicalUrl = + shell.exposeUrl === false || !shell.pagePath ? undefined : toAbsoluteUrl(shell.pagePath) + const imageUrl = toAbsoluteUrl(DEFAULT_OG_IMAGE_PATH) + + document.title = shell.title + setMetaByName(document, 'description', shell.description) + setMetaByName(document, 'robots', shell.robots) + setMetaByName(document, 'application-name', SITE_NAME) + setMetaByName(document, 'apple-mobile-web-app-title', SITE_NAME) + setMetaByName(document, 'twitter:card', 'summary_large_image') + setMetaByName(document, 'twitter:title', shell.title) + setMetaByName(document, 'twitter:description', shell.description) + setMetaByName(document, 'twitter:image', imageUrl) + + setMetaByProperty(document, 'og:site_name', SITE_NAME) + setMetaByProperty(document, 'og:type', DEFAULT_OG_TYPE) + setMetaByProperty(document, 'og:title', shell.title) + setMetaByProperty(document, 'og:description', shell.description) + setMetaByProperty(document, 'og:image', imageUrl) + setMetaByProperty(document, 'og:image:alt', DEFAULT_OG_IMAGE_ALT) + + if (canonicalUrl) { + setMetaByProperty(document, 'og:url', canonicalUrl) + ensureCanonicalLink(document, canonicalUrl) + } else { + removeCanonicalLink(document) + removeMetaByProperty(document, 'og:url') + } + + syncStructuredData(document, shell.structuredData) + + return `\n${document.documentElement.outerHTML}\n` +} + +function setMetaByName(document, name, content) { + let meta = document.head.querySelector(`meta[name="${name}"]`) + + if (!meta) { + meta = document.createElement('meta') + meta.setAttribute('name', name) + document.head.append(meta) + } + + meta.setAttribute('content', content) +} + +function setMetaByProperty(document, property, content) { + let meta = document.head.querySelector(`meta[property="${property}"]`) + + if (!meta) { + meta = document.createElement('meta') + meta.setAttribute('property', property) + document.head.append(meta) + } + + meta.setAttribute('content', content) +} + +function removeMetaByProperty(document, property) { + document.head.querySelector(`meta[property="${property}"]`)?.remove() +} + +function ensureCanonicalLink(document, href) { + let link = document.head.querySelector('link[rel="canonical"]') + + if (!link) { + link = document.createElement('link') + link.setAttribute('rel', 'canonical') + document.head.append(link) + } + + link.setAttribute('href', href) +} + +function removeCanonicalLink(document) { + document.head.querySelector('link[rel="canonical"]')?.remove() +} + +function syncStructuredData(document, structuredData) { + const existing = document.getElementById(STRUCTURED_DATA_ID) + + if (!structuredData) { + existing?.remove() + return + } + + const script = existing ?? document.createElement('script') + script.id = STRUCTURED_DATA_ID + script.setAttribute('type', 'application/ld+json') + script.textContent = JSON.stringify(structuredData) + + if (!existing) { + document.head.append(script) + } +} + +function getHomeStructuredData() { + const homeUrl = toAbsoluteUrl('/') + const organizationId = `${homeUrl}#organization` + const logoUrl = toAbsoluteUrl(DEFAULT_LOGO_PATH) + const imageUrl = toAbsoluteUrl(DEFAULT_OG_IMAGE_PATH) + + return [ + { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: SITE_NAME, + url: homeUrl, + description: HOME_DESCRIPTION, + image: imageUrl, + publisher: { + '@id': organizationId, + }, + }, + { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: SITE_NAME, + applicationCategory: 'UtilitiesApplication', + operatingSystem: 'Any', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + }, + description: HOME_DESCRIPTION, + url: homeUrl, + image: imageUrl, + publisher: { + '@id': organizationId, + }, + sameAs: [REPOSITORY_URL], + }, + { + '@context': 'https://schema.org', + '@id': organizationId, + '@type': 'Organization', + name: SITE_NAME, + url: homeUrl, + sameAs: [REPOSITORY_URL], + image: imageUrl, + logo: { + '@type': 'ImageObject', + url: logoUrl, + width: 512, + height: 512, + }, + }, + ] +} + +function toAbsoluteUrl(path) { + if (!siteUrl) { + return path + } + + return new URL(path, `${siteUrl}/`).toString() +} diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx new file mode 100644 index 0000000..9184217 --- /dev/null +++ b/apps/web/src/app/App.tsx @@ -0,0 +1,83 @@ +/** + * Root router for the browser application. + */ + +import { createBrowserRouter, RouterProvider } from 'react-router-dom' + +import { PageStateCard } from '@/components/ui/PageStateCard' +import { Shell } from './Shell' + +function RouteHydrateFallback() { + return ( + + ) +} + +function lazyRoute(load: () => Promise) { + return { + lazy: load, + hydrateFallbackElement: , + } +} + +/** The router keeps navigation declarative and co-locates each page with its route. */ +const router = createBrowserRouter([ + { + path: '/', + Component: Shell, + children: [ + { + ...lazyRoute(async () => { + const { SenderRouteLayout } = await import('./routes/SenderRouteLayout') + return { Component: SenderRouteLayout } + }), + children: [ + { + index: true, + ...lazyRoute(async () => { + const { HomePage } = await import('./routes/HomePage') + return { Component: HomePage } + }), + }, + { + path: 'share/:transferId', + ...lazyRoute(async () => { + const { SharePage } = await import('./routes/SharePage') + return { Component: SharePage } + }), + }, + { + path: 'transfers', + ...lazyRoute(async () => { + const { HistoryPage } = await import('./routes/HistoryPage') + return { Component: HistoryPage } + }), + }, + ], + }, + { + path: 't/:transferId', + ...lazyRoute(async () => { + const { ReceivePage } = await import('./routes/ReceivePage') + return { Component: ReceivePage } + }), + }, + { + path: '*', + ...lazyRoute(async () => { + const { NotFoundPage } = await import('./routes/NotFoundPage') + return { Component: NotFoundPage } + }), + }, + ], + }, +]) + +/** App renders the router provider used by the rest of the browser UI. */ +export function App() { + return +} diff --git a/apps/web/src/app/Shell.test.tsx b/apps/web/src/app/Shell.test.tsx new file mode 100644 index 0000000..3012787 --- /dev/null +++ b/apps/web/src/app/Shell.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' +import { describe, expect, it } from 'vitest' + +import { AUTHOR_NAME, AUTHOR_URL, LICENSE_URL, REPOSITORY_URL } from '@/lib/seo/site' + +import { Shell } from './Shell' + +describe('Shell', () => { + it('renders navigation and the nested outlet', () => { + render( + + + }> + Transfers content} /> + + + , + ) + + expect(screen.getByRole('link', { name: 'Xdrop' })).toHaveAttribute('href', '/') + expect(screen.getByRole('link', { name: 'Send' })).toHaveAttribute('href', '/') + expect(screen.getByRole('link', { name: 'Transfers' })).toHaveAttribute('href', '/transfers') + expect(screen.getByText('Transfers content')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'View the license' }).closest('p')).toHaveTextContent( + `Developed by ${AUTHOR_NAME} and released under the GNU Affero General Public License v3.0 only.`, + ) + expect(screen.getByRole('link', { name: 'View the license' }).closest('p')).toHaveTextContent( + 'more information and source code on GitHub.', + ) + expect(screen.getByRole('link', { name: 'View the license' })).toHaveAttribute( + 'href', + LICENSE_URL, + ) + expect(screen.getByRole('link', { name: 'GitHub' })).toHaveAttribute('href', REPOSITORY_URL) + expect(screen.getByRole('link', { name: AUTHOR_NAME })).toHaveAttribute('href', AUTHOR_URL) + }) +}) diff --git a/apps/web/src/app/Shell.tsx b/apps/web/src/app/Shell.tsx new file mode 100644 index 0000000..e303949 --- /dev/null +++ b/apps/web/src/app/Shell.tsx @@ -0,0 +1,55 @@ +import { NavLink, Outlet } from 'react-router-dom' + +import { AUTHOR_NAME, AUTHOR_URL, LICENSE_URL, REPOSITORY_URL } from '@/lib/seo/site' + +/** Shell provides the shared navigation and decorative chrome around route content. */ +export function Shell() { + return ( +

+