Export collections to FTP for depositing and archiving#3654
Open
gabestein wants to merge 23 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces configurable FTP deposit targets (managed by superadmins) and a new collection export workflow that packages released pubs into a ZIP (PDF + JATS) and can optionally deposit that ZIP to one or more configured targets.
Changes:
- Adds an
FtpTargetsdata model + superadmin CRUD UI/API with SFTP credential testing and encrypted credential storage. - Adds a collection export API/permission, dashboard “Export” tab UI, and a worker task that generates ZIPs, uploads them to S3, and optionally deposits via SFTP.
- Updates local dev ergonomics (auto-decrypt env, dev seeding, docker-compose adjustments, README refresh).
Reviewed changes
Copilot reviewed 33 out of 34 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| workers/worker.ts | Registers collectionExport in the worker task map. |
| workers/tasks/collectionExport.ts | Implements ZIP generation, S3 upload, and optional SFTP deposit for collection exports. |
| utils/superAdmin.ts | Adds ftpTargets to the superadmin tab kind list. |
| utils/api/contracts/collection.ts | Adds POST /api/collections/export contract definition. |
| tools/migrations/2026_06_30_createFtpTargets.js | Creates FtpTargets table (host/path/credentials fields, etc.). |
| tools/migrations/2026_07_01_addNameToFtpTargets.js | Adds name column to FtpTargets. |
| server/utils/workers.ts | Allows collectionExport as a worker task type. |
| server/utils/sftp.ts | Adds SFTP helper functions (test connection + upload). |
| server/sequelize.ts | Seeds dev data in non-production during sequelize sync. |
| server/seed.ts | Adds dev seed routine for a demo community + related records. |
| server/routes/superAdminDashboard.tsx | Adds FTP target CRUD + extends community search exclusion logic for FTP targets. |
| server/routes/dashboardSettings.tsx | Fetches collectionExports + ftpTargets for the collection settings export tab. |
| server/models.ts | Registers and exports the FtpTarget Sequelize model. |
| server/kf/provisionLocalUser.ts | Syncs User.isSuperAdmin from an OIDC role claim during provisioning. |
| server/kf/oidc.server.ts | Adds OIDC_ROLE_CLAIM configuration export. |
| server/ftpTarget/model.ts | Defines the FtpTarget Sequelize model. |
| server/community/model.ts | Adds Community.hasMany(FtpTarget) association. |
| server/collection/queries.ts | Adds getCollectionExports query (WorkerTask history). |
| server/collection/permissions.ts | Adds collectionExport permission flag. |
| server/collection/api.ts | Adds the export endpoint for triggering collection exports. |
| README.md | Updates local dev setup instructions and troubleshooting notes. |
| pnpm-lock.yaml | Locks new dependencies (notably ssh2-sftp-client and types). |
| package.json | Adds ssh2-sftp-client and its types. |
| infra/docker-compose.dev.yml | Updates dev compose configuration (env + volume changes). |
| infra/dev.sh | Auto-decrypts infra/.env.local.enc into infra/.env.local if needed. |
| infra/.env.local.enc | Updates encrypted local env contents. |
| infra/.env.dev.enc | Updates encrypted dev env contents. |
| client/containers/SuperAdminDashboard/tabs.tsx | Registers the new “FTP Targets” superadmin tab. |
| client/containers/SuperAdminDashboard/FtpTargets/index.ts | Exports the FTP Targets tab component. |
| client/containers/SuperAdminDashboard/FtpTargets/FtpTargets.tsx | Implements the superadmin UI for managing FTP targets. |
| client/containers/SuperAdminDashboard/FtpTargets/ftpTargets.scss | Styles for the FTP targets tab. |
| client/containers/DashboardSettings/CollectionSettings/ExportCollectionButton.tsx | Adds collection export UI, polling, history, and FTP target selection. |
| client/containers/DashboardSettings/CollectionSettings/exportCollectionButton.scss | Styles for the collection export UI. |
| client/containers/DashboardSettings/CollectionSettings/CollectionSettings.tsx | Adds the “Export” tab to collection settings and wires initial data into the export UI. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+20
to
+26
| type PastExport = { | ||
| id: string; | ||
| createdAt: string; | ||
| isProcessing: boolean; | ||
| output: { downloadUrl: string; ftpUploaded: boolean } | null; | ||
| error: string | null; | ||
| }; |
Comment on lines
+107
to
+114
| const workerTask = await addWorkerTask({ | ||
| type: 'collectionExport', | ||
| input: { | ||
| collectionId, | ||
| communityId: collection.communityId, | ||
| ftpTargetIds: ftpTargetIds ?? [], | ||
| }, | ||
| }); |
Comment on lines
+601
to
+603
| if (!ftpType || !['sftp', 'ftps'].includes(ftpType)) { | ||
| throw new BadRequestError(new Error('ftpType must be "sftp" or "ftps"')); | ||
| } |
Comment on lines
+663
to
+668
| if (ftpType !== undefined) { | ||
| if (!['sftp', 'ftps'].includes(ftpType)) { | ||
| throw new BadRequestError(new Error('ftpType must be "sftp" or "ftps"')); | ||
| } | ||
| updates.ftpType = ftpType; | ||
| } |
| updates.password = encryptedText; | ||
| updates.passwordInitVec = initVec; | ||
| } | ||
| updates.username = username; |
Comment on lines
+705
to
+709
| } else if (password) { | ||
| const { encryptedText, initVec } = aes256Encrypt(password, env.AES_ENCRYPTION_KEY!); | ||
| updates.password = encryptedText; | ||
| updates.passwordInitVec = initVec; | ||
| } |
Comment on lines
+768
to
+774
| const createData: Record<string, any> = { | ||
| communityId: destCommunity.id, | ||
| ftpType: source.ftpType, | ||
| port: source.port, | ||
| host: source.host, | ||
| filePath: source.filePath, | ||
| }; |
| >, | ||
| userInfo: UserInfoFields, | ||
| ): Promise<InstanceType<typeof User>> { | ||
| const isSuperAdmin = userInfo[OIDC_ROLE_CLAIM] === 'admin'; |
Comment on lines
+21
to
+27
| await client.connect({ | ||
| host: params.host, | ||
| port: params.port ?? 22, | ||
| username: params.username, | ||
| password: params.password, | ||
| readyTimeout: 10000, | ||
| }); |
Comment on lines
+104
to
+108
| const archiveStream = archiver('zip', { zlib: { level: 6 } }); | ||
| const chunks: Buffer[] = []; | ||
|
|
||
| archiveStream.on('data', (chunk: Buffer) => chunks.push(chunk)); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FTP Targets & Collection Export
Overview
This branch adds two related features: a superadmin UI for managing SFTP/FTPS deposit targets (with credential testing and AES-256 encrypted storage), and a collection export feature that lets community admins bundle all published pubs in a collection into a ZIP of PDFs and JATS XML files — with optional upload to one or more configured FTP targets.
Changed files
Testing plan
FTP Targets (superadmin)
/superadmin?tab=ftpTargets. Confirm the tab renders with an empty state.hasCredentialsshows as false.Copy credentials. Confirm the new row appears for the correct community.Collection Export (collection settings)
host (path)for unnamed targets).{subdomain}-{collectionSlug}-{timestamp}/with per-pub subdirectories each containing a.pdfand.xml.Todo