Files
wekan/models/lib/attachmentStoragePath.js
T
Lauri Ojansivu 65bf215cbe Fix #6473: attachments missing after the MongoDB -> FerretDB migration.
- Migration importers: resolve the CollectionFS GridFS id from
  copies.<bucket>.key too (the REAL old-WeKan layout; only
  original.gridFsFileId / gridFsFileId / copies.gridfs.key were tried,
  none of which exist there) via a shared resolveCfsGridFsId() in
  releases/migrate-mongodb-to-ferretdb.mjs,
  snap-src/bin/migrate-mongo3-to-ferretdb.mjs and
  snap-src/bin/migrate-gridfs-to-fs.mjs. The modern importer now drives
  CollectionFS extraction from cfs_gridfs.<bucket>.files itself, so a
  missing/empty cfs.<bucket>.filerecord no longer silently skips every
  file; binaries without a filerecord are still extracted to disk, and
  filerecords whose binary cannot be located are reported on the
  dashboard instead of dropped.
- Meteor-Files phase: match versions.*.meta.gridFsFileId without a
  storage flag (WeKan's own getFileStrategy rule), handle every version
  (not just 'original'), and add a bucket-driven sweep of <bucket>.files
  by metadata.fileId so binaries are recovered even when the record's
  flags say nothing about GridFS. GridFS-flagged versions with no
  locatable binary are reported.
- mongo3 importer: copy the CollectionFS top-level boardId/cardId/
  listId/swimlaneId/userId into meta on the created records - an
  attachment without meta.cardId shows on no card.
- Admin Panel > Attachments: the Filesystem Storage paths were computed
  from process.env.WRITABLE_PATH IN THE BROWSER, so they always showed
  "/data". New admin-only getAttachmentStoragePaths method returns the
  real Snap-aware paths (shared models/lib/attachmentStoragePath.js),
  also used for the settings document default (was /data/attachments,
  actual Docker path is /data/files/attachments).
- Snap: `snap run wekan.database ferretdb` now says it does NOT migrate
  data, says when nothing was switched, and points at
  `snap run wekan.migrate` for re-running the full migration.
- Tests with negative cases: tests/migrationAttachmentExtraction.test.cjs
  (behavioral resolveCfsGridFsId from all three importers + wiring
  guards) and tests/attachmentStoragePath.test.cjs, wired into
  test:unit:node.
- Companion fix in wekan/FerretDB: accept documents with literal dotted
  field names like MongoDB 3.6+, so migrated documents containing them
  are no longer silently dropped.
- CHANGELOG.md: new Upcoming section with these changes.

If attachments are already missing after an earlier migration, run
`snap run wekan.migrate` - the source MongoDB data was never modified,
and the fixed importers re-create the attachment records and files.

Thanks to mueschel and xet7.
2026-07-17 01:43:53 +03:00

35 lines
1.6 KiB
JavaScript

// Pure, dependency-free storage path computation (no Meteor imports) so it can
// be unit tested directly with plain Node. Used by the attachment storage
// settings (Admin Panel > Attachments) and kept in sync with the path logic in
// models/attachments.js, models/avatars.js and server/initializeDirs.js.
//
// #6473: the Admin Panel used to compute these paths ON THE CLIENT from
// process.env.WRITABLE_PATH, which never exists in a browser, so it always
// showed "/data" — a path that does not exist on a Snap install
// (WRITABLE_PATH=$SNAP_COMMON/files there). The real paths are:
// - Docker (WRITABLE_PATH=/data): /data/files/{attachments,avatars}
// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/{attachments,avatars}
// - Dev (WRITABLE_PATH unset): <cwd>/files/{attachments,avatars}
//
// Snap already appends '/files' to WRITABLE_PATH, so that case is detected and
// '/files' is not appended twice.
const path = require('path');
/**
* Compute the real attachment/avatar storage paths for a given base path.
* @param {string} [basePath] WRITABLE_PATH (falls back to process.cwd())
* @return {{writablePath: string, attachments: string, avatars: string}}
*/
function computeStoragePaths(basePath) {
const base = basePath || process.cwd();
const endsWithFiles = base.endsWith('/files') || base.endsWith('\\files');
const root = endsWithFiles ? base : path.join(base, 'files');
return {
writablePath: base,
attachments: path.join(root, 'attachments'),
avatars: path.join(root, 'avatars'),
};
}
module.exports = { computeStoragePaths };