From edc6b18b5f77320e966e56c888575bcb764096dc Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Fri, 3 Apr 2026 20:16:32 +0300 Subject: [PATCH] Fix Bug: WeKan v8.50 WRITABLE_PATH. Thanks to snowsky and xet7 ! Fixes #6242 --- models/attachments.js | 19 +++++++++++ models/attachments.server.js | 10 +++++- models/avatars.js | 19 +++++++++++ models/avatars.server.js | 10 +++++- server/imports.js | 4 +++ server/initializeDirs.js | 64 ++++++++++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 server/initializeDirs.js diff --git a/models/attachments.js b/models/attachments.js index 55e381d98..fb980a5c4 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,14 +1,33 @@ import { Meteor } from 'meteor/meteor'; import { FilesCollection } from 'meteor/ostrio:files'; import { generateUniversalAttachmentUrl } from '/models/lib/universalUrlGenerator'; +import path from 'path'; // XXX Enforce a schema for the Attachments FilesCollection // see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema +// Compute storage path: +// - Docker (WRITABLE_PATH=/data): /data/files/attachments +// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/attachments +const computeAttachmentStoragePath = () => { + const basePath = process.env.WRITABLE_PATH || process.cwd(); + const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files'); + if (endsWithFiles) { + // Snap: WRITABLE_PATH already includes /files + return basePath + '/attachments'; + } else { + // Docker & Dev: append /files/attachments + return basePath + '/files/attachments'; + } +}; + +const storagePath = Meteor.isServer ? computeAttachmentStoragePath() : 'assets/app/uploads/attachments'; + const Attachments = new FilesCollection({ debug: false, // Change to `true` for debugging collectionName: 'attachments', allowClientCode: true, + storagePath: storagePath, namingFunction(opts) { let filenameWithoutExtension = "" let fileId = ""; diff --git a/models/attachments.server.js b/models/attachments.server.js index d86c83e20..a79eadb71 100644 --- a/models/attachments.server.js +++ b/models/attachments.server.js @@ -22,7 +22,15 @@ let attachmentUploadMimeTypes = []; let attachmentUploadSize = 0; const attachmentBucket = createBucket('attachments'); -const storagePath = path.join(process.env.WRITABLE_PATH || process.cwd(), 'files', 'attachments'); + +// Compute storage path: +// - Docker (WRITABLE_PATH=/data): /data/files/attachments +// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/attachments +const basePath = process.env.WRITABLE_PATH || process.cwd(); +const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files'); +const storagePath = endsWithFiles + ? path.join(basePath, 'attachments') + : path.join(basePath, 'files', 'attachments'); if (process.env.ATTACHMENTS_UPLOAD_MIME_TYPES) { attachmentUploadMimeTypes = process.env.ATTACHMENTS_UPLOAD_MIME_TYPES.split(','); diff --git a/models/avatars.js b/models/avatars.js index f44ff7380..7e6d40ece 100644 --- a/models/avatars.js +++ b/models/avatars.js @@ -1,16 +1,35 @@ import { Meteor } from 'meteor/meteor'; import { FilesCollection } from 'meteor/ostrio:files'; import { generateUniversalAvatarUrl } from '/models/lib/universalUrlGenerator'; +import path from 'path'; const { filesize } = require('filesize'); const getTAPi18n = () => require('/imports/i18n').TAPi18n; let avatarsUploadSize = 72000; +// Compute storage path: +// - Docker (WRITABLE_PATH=/data): /data/files/avatars +// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/avatars +const computeAvatarStoragePath = () => { + const basePath = process.env.WRITABLE_PATH || process.cwd(); + const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files'); + if (endsWithFiles) { + // Snap: WRITABLE_PATH already includes /files + return basePath + '/avatars'; + } else { + // Docker & Dev: append /files/avatars + return basePath + '/files/avatars'; + } +}; + +const storagePath = Meteor.isServer ? computeAvatarStoragePath() : 'assets/app/uploads/avatars'; + const Avatars = new FilesCollection({ debug: false, // Change to `true` for debugging collectionName: 'avatars', allowClientCode: true, + storagePath: storagePath, namingFunction(opts) { let filenameWithoutExtension = "" let fileId = ""; diff --git a/models/avatars.server.js b/models/avatars.server.js index 5679ae4ed..2fa3406e9 100644 --- a/models/avatars.server.js +++ b/models/avatars.server.js @@ -40,7 +40,15 @@ if (process.env.AVATARS_UPLOAD_EXTERNAL_PROGRAM) { } const avatarsBucket = createBucket('avatars'); -const storagePath = path.join(process.env.WRITABLE_PATH || process.cwd(), 'files', 'avatars'); + +// Compute storage path: +// - Docker (WRITABLE_PATH=/data): /data/files/avatars +// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/avatars +const basePath = process.env.WRITABLE_PATH || process.cwd(); +const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files'); +const storagePath = endsWithFiles + ? path.join(basePath, 'avatars') + : path.join(basePath, 'files', 'avatars'); // --------------------------------------------------------------------------- // File store strategy factory diff --git a/server/imports.js b/server/imports.js index 97f79ea24..3b7338d9d 100644 --- a/server/imports.js +++ b/server/imports.js @@ -3,6 +3,10 @@ // Loaded by server/main.js after bootstrap (collectionHelpers) completes. // ============================================================================ +// ****IMPORTANT**** Initialize upload directories BEFORE models are loaded +// This ensures ostrio:files can create necessary directories without permission errors +import '/server/initializeDirs'; + // ---------------------------------------------------------------------------- // 0. API middleware & auth routes (must register before model routes) // ---------------------------------------------------------------------------- diff --git a/server/initializeDirs.js b/server/initializeDirs.js new file mode 100644 index 000000000..a2af55a43 --- /dev/null +++ b/server/initializeDirs.js @@ -0,0 +1,64 @@ +// ============================================================================ +// Initialize Upload Directories +// Runs at server startup before models are loaded to ensure ostrio:files +// can create necessary upload directories without permission errors. +// ============================================================================ + +import fs from 'fs'; +import path from 'path'; + +// Compute upload subdirectory path: +// - On Docker (WRITABLE_PATH=/data): /data/files/attachments, /data/files/avatars +// - On Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/attachments, $SNAP_COMMON/files/avatars +// - On dev (WRITABLE_PATH=..): ../files/attachments, ../files/avatars +// +// Snap already appends '/files' to WRITABLE_PATH, so we detect and handle that case. +function getUploadPaths() { + const basePath = process.env.WRITABLE_PATH || process.cwd(); + + // Check if basePath already ends with '/files' (Snap case) + const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files'); + + if (endsWithFiles) { + // Snap: WRITABLE_PATH is already $SNAP_COMMON/files, so append subdirectories directly + return { + attachments: path.join(basePath, 'attachments'), + avatars: path.join(basePath, 'avatars'), + }; + } else { + // Docker & Dev: WRITABLE_PATH is /data or .., so append /files/{subdir} + return { + attachments: path.join(basePath, 'files', 'attachments'), + avatars: path.join(basePath, 'files', 'avatars'), + }; + } +} + +const uploadPaths = getUploadPaths(); +const dirsToCreate = [uploadPaths.attachments, uploadPaths.avatars]; + +dirsToCreate.forEach(dirPath => { + try { + // Create directory recursively with proper permissions (0o755) + // Ignore error if it already exists + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true, mode: 0o755 }); + if (process.env.DEBUG === 'true') { + console.info(`[initializeDirs] Created directory: ${dirPath}`); + } + } else { + // Verify the directory is writable + try { + fs.accessSync(dirPath, fs.constants.W_OK); + if (process.env.DEBUG === 'true') { + console.info(`[initializeDirs] Directory exists and is writable: ${dirPath}`); + } + } catch (accessError) { + console.warn(`[initializeDirs] WARNING: Directory exists but may not be writable: ${dirPath}`, accessError.message); + } + } + } catch (error) { + console.error(`[initializeDirs] FAILED to initialize directory: ${dirPath}`, error.message); + throw new Error(`Cannot create or access upload directory: ${dirPath}\n${error.message}`); + } +});