diff --git a/client/src/App.tsx b/client/src/App.tsx index a4610362a..1106f25e1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -303,7 +303,7 @@ export default function App() { loadUser() } } - authApi.getAppConfig().then(async (config: { managed?: boolean; demo_mode?: boolean; dev_mode?: boolean; is_prerelease?: boolean; has_maps_key?: boolean; has_amap_key?: boolean; places_provider?: string; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; places_photos_enabled?: boolean; places_autocomplete_enabled?: boolean; places_details_enabled?: boolean; places_enrich_enabled?: boolean; place_shadow_enabled?: boolean; permissions?: Record }) => { + authApi.getAppConfig().then(async (config: { managed?: boolean; demo_mode?: boolean; dev_mode?: boolean; is_prerelease?: boolean; has_maps_key?: boolean; has_amap_key?: boolean; places_provider?: string; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; places_photos_enabled?: boolean; places_autocomplete_enabled?: boolean; places_details_enabled?: boolean; places_enrich_enabled?: boolean; place_shadow_enabled?: boolean; max_upload_mb?: number; permissions?: Record }) => { setManaged(!!config?.managed) setDemoMode(!!config?.demo_mode) if (config?.dev_mode) setDevMode(true) @@ -320,6 +320,7 @@ export default function App() { if (config?.places_details_enabled !== undefined) setPlacesDetailsEnabled(config.places_details_enabled) if (config?.places_enrich_enabled !== undefined) setPlacesEnrichEnabled(config.places_enrich_enabled) if (config?.place_shadow_enabled !== undefined) setPlaceShadowEnabled(config.place_shadow_enabled) + if (typeof config?.max_upload_mb === 'number' && config.max_upload_mb > 0) useAuthStore.getState().setMaxUploadMb(config.max_upload_mb) if (config?.permissions) usePermissionsStore.getState().setPermissions(config.permissions) // Last, since a new release reloads the page from here. await reconcileAppVersion(config?.version) diff --git a/client/src/components/Files/useFileManager.ts b/client/src/components/Files/useFileManager.ts index 9970931ba..590a8702f 100644 --- a/client/src/components/Files/useFileManager.ts +++ b/client/src/components/Files/useFileManager.ts @@ -43,6 +43,7 @@ export function useFileManager({ files = [], onUpload, onDelete, onUpdate, place const can = useCanDo() const trip = useTripStore((s) => s.trip) const currentUser = useAuthStore((s) => s.user) + const maxUploadMb = useAuthStore((s) => s.maxUploadMb) const canManageSync = canManageDocSync(currentUser, trip) const docSyncOffered = useDocSyncOffered(tripId, canManageSync) const { t, locale } = useTranslation() @@ -137,7 +138,13 @@ export function useFileManager({ files = [], onUpload, onDelete, onUpdate, place const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, - maxSize: 50 * 1024 * 1024, + maxSize: maxUploadMb * 1024 * 1024, + // A file over the limit used to vanish without a word; say why it was left out. + onDropRejected: rejections => { + if (rejections.some(r => r.errors.some(e => e.code === 'file-too-large'))) { + toast.error(t('files.uploadErrorSize', { max: maxUploadMb })) + } + }, noClick: false, }) diff --git a/client/src/mobile/screens/trip/tabs/MFilesTab.tsx b/client/src/mobile/screens/trip/tabs/MFilesTab.tsx index 6d79fe723..cd4d0f9b8 100644 --- a/client/src/mobile/screens/trip/tabs/MFilesTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MFilesTab.tsx @@ -20,8 +20,6 @@ import { matchesFileFilter, sortFilesStarredFirst, type FileFilterId, } from './filesModel' -const MAX_UPLOAD_BYTES = 50 * 1024 * 1024 - /** * Tab 5 — Dateien. Real `planner.files` (already non-deleted, §7.2), the * filter grid + starred-first sort from filesModel.ts, and a row per file @@ -40,6 +38,7 @@ export default function MFilesTab({ planner, shell }: MTabScreenProps) { const [menuFileId, setMenuFileId] = useState(null) const [linkFileId, setLinkFileId] = useState(null) const currentUser = useAuthStore(st => st.user) + const maxUploadMb = useAuthStore(st => st.maxUploadMb) const [trashOpen, setTrashOpen] = useState(false) const [docSyncOpen, setDocSyncOpen] = useState(false) const [lightboxIndex, setLightboxIndex] = useState(null) @@ -60,9 +59,10 @@ export default function MFilesTab({ planner, shell }: MTabScreenProps) { }, [shell.uploadFilesSignal]) const uploadFiles = async (list: File[]) => { - const tooBig = list.filter(f => f.size > MAX_UPLOAD_BYTES) - const okFiles = list.filter(f => f.size <= MAX_UPLOAD_BYTES) - if (tooBig.length > 0) planner.toast.error(t('files.uploadErrorSize')) + const maxBytes = maxUploadMb * 1024 * 1024 + const tooBig = list.filter(f => f.size > maxBytes) + const okFiles = list.filter(f => f.size <= maxBytes) + if (tooBig.length > 0) planner.toast.error(t('files.uploadErrorSize', { max: maxUploadMb })) if (okFiles.length === 0) return setUploading(true) let uploaded = 0 diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts index d6bbb6089..ff45d8d0e 100644 --- a/client/src/store/authStore.ts +++ b/client/src/store/authStore.ts @@ -65,6 +65,8 @@ interface AuthState { placesAutocompleteEnabled: boolean placesDetailsEnabled: boolean placesEnrichEnabled: boolean + /** FILE_UPLOAD_LIMIT_MB from the server (#1364); 50 until the config arrives. */ + maxUploadMb: number /** Server records which search result was picked (admin switch, default off). */ placeShadowEnabled: boolean @@ -94,6 +96,7 @@ interface AuthState { setPlacesAutocompleteEnabled: (val: boolean) => void setPlacesDetailsEnabled: (val: boolean) => void setPlacesEnrichEnabled: (val: boolean) => void + setMaxUploadMb: (val: number) => void setPlaceShadowEnabled: (val: boolean) => void demoLogin: () => Promise } @@ -149,6 +152,7 @@ export const useAuthStore = create()( placesAutocompleteEnabled: true, placesDetailsEnabled: true, placesEnrichEnabled: true, + maxUploadMb: 50, // Fail-closed: an old server sends no flag and nothing is logged. placeShadowEnabled: false, @@ -425,6 +429,7 @@ export const useAuthStore = create()( setPlacesAutocompleteEnabled: (val: boolean) => set({ placesAutocompleteEnabled: val }), setPlacesDetailsEnabled: (val: boolean) => set({ placesDetailsEnabled: val }), setPlacesEnrichEnabled: (val: boolean) => set({ placesEnrichEnabled: val }), + setMaxUploadMb: (val: number) => set({ maxUploadMb: val }), setPlaceShadowEnabled: (val: boolean) => set({ placeShadowEnabled: val }), demoLogin: async () => { diff --git a/client/tests/unit/mobile/trip/MFilesTab.test.tsx b/client/tests/unit/mobile/trip/MFilesTab.test.tsx index 1bdecc2d0..1231b5269 100644 --- a/client/tests/unit/mobile/trip/MFilesTab.test.tsx +++ b/client/tests/unit/mobile/trip/MFilesTab.test.tsx @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import MFilesTab from '../../../../src/mobile/screens/trip/tabs/MFilesTab' +import { useAuthStore } from '../../../../src/store/authStore' import { filesApi } from '../../../../src/api/client' import { openFile } from '../../../../src/utils/fileDownload' import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell' @@ -293,10 +294,21 @@ describe('MFilesTab', () => { fireEvent.change(hiddenInput(container), { target: { files: [makeFile('huge.pdf', 'application/pdf', 60 * 1024 * 1024)] }, }) - expect(planner.toast.error).toHaveBeenCalledWith('files.uploadErrorSize') + expect(planner.toast.error).toHaveBeenCalledWith('files.uploadErrorSize:50') expect(planner.tripActions.addFile).not.toHaveBeenCalled() }) + it('FE-MOB-FTAB-019b: a raised FILE_UPLOAD_LIMIT_MB lets the same file through (#1364)', async () => { + useAuthStore.setState({ maxUploadMb: 100 }) + const { container, planner } = renderTab() + fireEvent.change(hiddenInput(container), { + target: { files: [makeFile('huge.pdf', 'application/pdf', 60 * 1024 * 1024)] }, + }) + expect(planner.toast.error).not.toHaveBeenCalled() + await waitFor(() => expect(planner.tripActions.addFile).toHaveBeenCalledTimes(1)) + useAuthStore.setState({ maxUploadMb: 50 }) + }) + it('FE-MOB-FTAB-020: an oversized file in a batch is skipped, the rest still uploads', async () => { const { container, planner } = renderTab() fireEvent.change(hiddenInput(container), { @@ -307,7 +319,7 @@ describe('MFilesTab', () => { ], }, }) - expect(planner.toast.error).toHaveBeenCalledWith('files.uploadErrorSize') + expect(planner.toast.error).toHaveBeenCalledWith('files.uploadErrorSize:50') await waitFor(() => expect(planner.toast.success).toHaveBeenCalledWith('files.uploaded:1')) expect(planner.tripActions.addFile).toHaveBeenCalledTimes(1) }) diff --git a/server/.env.example b/server/.env.example index 3db7b8ce1..ccf4332fd 100644 --- a/server/.env.example +++ b/server/.env.example @@ -44,6 +44,7 @@ OIDC_SCOPE=openid email profile # Fully overrides the default. Add extra scopes DEMO_MODE=false # Demo mode - resets data hourly # RESTORE_FROM_BACKUP=/app/data/backup.zip # Restore this backup on the first start, before any setup. Ignored once a database exists. +# FILE_UPLOAD_LIMIT_MB=50 # Max size (MB) of a file uploaded to a trip, a booking or a note. Videos keep their own 500 MB cap. Raise your reverse proxy's upload limit too. # BACKUP_UPLOAD_LIMIT_MB=500 # Max size (MB) of a backup archive you can upload when restoring. Raise it if your backup exceeds 500 MB. If you sit behind a reverse proxy, raise its upload limit too (e.g. nginx client_max_body_size). # TREK_DB_JOURNAL_MODE=WAL # SQLite journal mode (DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF). WAL is fastest and the right choice on local disk. Set DELETE if the data directory lives on network storage (Azure App Service, an SMB/NFS volume on a NAS) — WAL coordinates writers through a shared-memory file that those filesystems do not implement reliably. The mode is stored inside the database file, so it survives a restart; changing this applies on the next boot. diff --git a/server/src/app-config/derive.ts b/server/src/app-config/derive.ts index 68e996a49..7c9fdcf84 100644 --- a/server/src/app-config/derive.ts +++ b/server/src/app-config/derive.ts @@ -301,6 +301,13 @@ export function deriveIntegrations(raw: RawEnv) { }; } +export function deriveFiles(raw: RawEnv) { + return { + /** Largest document a user may upload to a trip, a booking or a note (#1364). Videos keep their own cap. */ + uploadLimitMb: positiveNumberOr(raw.FILE_UPLOAD_LIMIT_MB, 50), + }; +} + export function deriveBackup(raw: RawEnv) { return { uploadLimitMb: positiveNumberOr(raw.BACKUP_UPLOAD_LIMIT_MB, 500), @@ -365,6 +372,7 @@ export function deriveAll(raw: RawEnv) { webauthn: deriveWebauthn(raw), integrations: deriveIntegrations(raw), backup: deriveBackup(raw), + files: deriveFiles(raw), db: deriveDb(raw), paths: derivePaths(raw), net: deriveNet(raw), diff --git a/server/src/app-config/env.schema.ts b/server/src/app-config/env.schema.ts index b13dcd2a3..17b67cd80 100644 --- a/server/src/app-config/env.schema.ts +++ b/server/src/app-config/env.schema.ts @@ -161,6 +161,7 @@ export const envSchema = z.object({ TREK_WIKI_DIR: anyString, TREK_PLACE_PHOTO_DIR: anyString, BACKUP_UPLOAD_LIMIT_MB: positiveNumber, + FILE_UPLOAD_LIMIT_MB: positiveNumber, BACKUP_MAX_DECOMPRESSED_MB: positiveNumber, // A backup archive to restore on the very first start, before any setup (#1089). RESTORE_FROM_BACKUP: anyString, diff --git a/server/src/app-config/index.ts b/server/src/app-config/index.ts index f43499d84..ed2c9a5a6 100644 --- a/server/src/app-config/index.ts +++ b/server/src/app-config/index.ts @@ -17,6 +17,7 @@ export { deriveWebauthn, deriveIntegrations, deriveBackup, + deriveFiles, deriveDb, derivePaths, deriveNet, diff --git a/server/src/nest/auth/auth.service.ts b/server/src/nest/auth/auth.service.ts index 57811084d..a21327c0f 100644 --- a/server/src/nest/auth/auth.service.ts +++ b/server/src/nest/auth/auth.service.ts @@ -334,6 +334,8 @@ export class AuthService { // server's acceptance can never drift (the historical inline copy here // dropped pkpass, pkpasses, md and markdown). allowed_file_types: this.allowedFileTypes.get(), + // FILE_UPLOAD_LIMIT_MB, so the pickers refuse a file before the upload starts (#1364). + max_upload_mb: readEnv().files.uploadLimitMb, // Whether the configuration belongs to whoever operates this install // rather than to its admin. The client uses it to stop offering settings // the server would refuse anyway — it is an honesty flag for the UI, never diff --git a/server/src/nest/collab/collab.controller.ts b/server/src/nest/collab/collab.controller.ts index 0c62eb9fc..d55904dfe 100644 --- a/server/src/nest/collab/collab.controller.ts +++ b/server/src/nest/collab/collab.controller.ts @@ -35,10 +35,11 @@ import { import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CurrentUser } from '../auth/current-user.decorator'; import { RequirePermission, TripAccessGuard } from '../permissions/trip-access.guard'; -import { BLOCKED_EXTENSIONS } from '../files/files.constants'; +import { BLOCKED_EXTENSIONS, MAX_FILE_SIZE } from '../files/files.constants'; import { SpoolCleanupInterceptor } from '../common/spool-cleanup.interceptor'; -export const MAX_NOTE_FILE_SIZE = 50 * 1024 * 1024; +// Note attachments follow the same limit as the file manager (#1364). +export const MAX_NOTE_FILE_SIZE = MAX_FILE_SIZE; const MAX_CHAT_IMAGES = 4; const CHAT_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']); const CHAT_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']); diff --git a/server/src/nest/files/files.constants.ts b/server/src/nest/files/files.constants.ts index ea336244b..5178f18c9 100644 --- a/server/src/nest/files/files.constants.ts +++ b/server/src/nest/files/files.constants.ts @@ -1,4 +1,5 @@ import path from 'path'; +import { readEnv } from '../../app-config'; /** * File-domain constants and pure helpers, split out of FilesService because @@ -9,7 +10,9 @@ import path from 'path'; * files.bridge.ts. */ -export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB +// FILE_UPLOAD_LIMIT_MB, 50 MB by default (#1364). Read once at load, like the +// backup cap: the multer configs that use it are built before the container. +export const MAX_FILE_SIZE = readEnv().files.uploadLimitMb * 1024 * 1024; export const DEFAULT_ALLOWED_EXTENSIONS = 'jpg,jpeg,png,gif,webp,heic,pdf,doc,docx,xls,xlsx,txt,csv,pkpass,pkpasses,md,markdown'; // Video support (#823). Gallery/media uploads accept these in addition to images, diff --git a/server/src/nest/files/files.module.ts b/server/src/nest/files/files.module.ts index 6542f0230..5637bdd92 100644 --- a/server/src/nest/files/files.module.ts +++ b/server/src/nest/files/files.module.ts @@ -18,7 +18,7 @@ import { StorageModule } from '../storage/storage.module'; import { StorageService } from '../storage/storage.service'; import { buildStorageUploadOptions } from '../storage/storage-upload.factory'; import { filesUploadFileFilter } from './files.controller'; -import { MAX_VIDEO_SIZE } from './files.constants'; +import { MAX_FILE_SIZE, MAX_VIDEO_SIZE } from './files.constants'; @Module({ imports: [ @@ -30,7 +30,8 @@ import { MAX_VIDEO_SIZE } from './files.constants'; category: 'files', // Allow up to the video cap; non-video files are still held to // MAX_FILE_SIZE by the per-type guard in the upload handler (#823). - maxSize: MAX_VIDEO_SIZE, + // An operator may raise the document limit past the video cap. + maxSize: Math.max(MAX_VIDEO_SIZE, MAX_FILE_SIZE), defParamCharset: 'utf8', // parity with legacy routes/files.ts — preserve non-ASCII original filenames fileFilter: filesUploadFileFilter(allowedTypes), }), diff --git a/server/tests/unit/app-config/derive.test.ts b/server/tests/unit/app-config/derive.test.ts index 81a1694eb..1db692f54 100644 --- a/server/tests/unit/app-config/derive.test.ts +++ b/server/tests/unit/app-config/derive.test.ts @@ -14,6 +14,7 @@ import { derivePlugins, deriveIntegrations, deriveBackup, + deriveFiles, deriveNet, derivePaths, derivePush, @@ -245,6 +246,13 @@ describe('deriveIntegrations', () => { }); }); +describe('deriveFiles (#1364)', () => { + it('defaults the upload limit to 50 MB and takes FILE_UPLOAD_LIMIT_MB', () => { + expect(deriveFiles({}).uploadLimitMb).toBe(50); + expect(deriveFiles({ FILE_UPLOAD_LIMIT_MB: '200' }).uploadLimitMb).toBe(200); + }); +}); + describe('deriveBackup', () => { it('pins the backupService limit parsing (positive number or default)', () => { expect(deriveBackup({}).uploadLimitMb).toBe(500); diff --git a/shared/src/i18n/ar/files.ts b/shared/src/i18n/ar/files.ts index 7cc6f2ba7..5ac929d2a 100644 --- a/shared/src/i18n/ar/files.ts +++ b/shared/src/i18n/ar/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'ربط الملف', 'files.linkEmpty': 'لا توجد أماكن أو حجوزات للربط بعد', 'files.menu': 'المزيد من الخيارات', - 'files.uploadErrorSize': 'الملف كبير جدًا (الحد الأقصى 50 ميغابايت)', + 'files.uploadErrorSize': 'الملف كبير جدًا (الحد الأقصى {max} ميغابايت)', 'files.title': 'الملفات', 'files.pageTitle': 'الملفات والمستندات', 'files.subtitle': '{count} ملف لـ {trip}', diff --git a/shared/src/i18n/az/files.ts b/shared/src/i18n/az/files.ts index 10a641be3..87f605fbf 100644 --- a/shared/src/i18n/az/files.ts +++ b/shared/src/i18n/az/files.ts @@ -9,7 +9,7 @@ const files: TranslationStrings = { 'files.menu': 'Daha çox seçim', 'files.uploadErrorSize': - 'Fayl həddindən çox böyükdür (maksimum 50 MB)', + 'Fayl həddindən çox böyükdür (maksimum {max} MB)', 'files.title': 'Fayllar', 'files.pageTitle': 'Fayllar və sənədlər', 'files.subtitle': '{trip} üçün {count} fayl', diff --git a/shared/src/i18n/br/files.ts b/shared/src/i18n/br/files.ts index d012bbfde..838f927c6 100644 --- a/shared/src/i18n/br/files.ts +++ b/shared/src/i18n/br/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Vincular arquivo', 'files.linkEmpty': 'Ainda não há lugares ou reservas para vincular', 'files.menu': 'Mais opções', - 'files.uploadErrorSize': 'O arquivo é muito grande (máx. 50 MB)', + 'files.uploadErrorSize': 'O arquivo é muito grande (máx. {max} MB)', 'files.title': 'Arquivos', 'files.pageTitle': 'Arquivos e documentos', 'files.subtitle': '{count} arquivos para {trip}', diff --git a/shared/src/i18n/ca/files.ts b/shared/src/i18n/ca/files.ts index e653a2246..a71c63b71 100644 --- a/shared/src/i18n/ca/files.ts +++ b/shared/src/i18n/ca/files.ts @@ -62,6 +62,6 @@ const files: TranslationStrings = { 'files.linkTitle': 'Enllaça un fitxer', 'files.linkEmpty': 'Encara no hi ha llocs ni reserves per enllaçar', 'files.menu': 'Més opcions', - 'files.uploadErrorSize': 'El fitxer és massa gran (màx. 50 MB)', + 'files.uploadErrorSize': 'El fitxer és massa gran (màx. {max} MB)', }; export default files; diff --git a/shared/src/i18n/cs/files.ts b/shared/src/i18n/cs/files.ts index f46d34b68..5083bda14 100644 --- a/shared/src/i18n/cs/files.ts +++ b/shared/src/i18n/cs/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Propojit soubor', 'files.linkEmpty': 'Zatím nejsou žádná místa ani rezervace k propojení', 'files.menu': 'Další možnosti', - 'files.uploadErrorSize': 'Soubor je příliš velký (max. 50 MB)', + 'files.uploadErrorSize': 'Soubor je příliš velký (max. {max} MB)', 'files.title': 'Soubory', 'files.pageTitle': 'Soubory a dokumenty', 'files.subtitle': '{count} souborů pro {trip}', diff --git a/shared/src/i18n/de/files.ts b/shared/src/i18n/de/files.ts index 9f1c5c638..e34343b8b 100644 --- a/shared/src/i18n/de/files.ts +++ b/shared/src/i18n/de/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Datei verknüpfen', 'files.linkEmpty': 'Keine Orte oder Buchungen zum Verknüpfen vorhanden', 'files.menu': 'Weitere Optionen', - 'files.uploadErrorSize': 'Datei ist zu groß (max. 50 MB)', + 'files.uploadErrorSize': 'Datei ist zu groß (max. {max} MB)', 'files.title': 'Dateien', 'files.pageTitle': 'Dateien & Dokumente', 'files.subtitle': '{count} Dateien für {trip}', diff --git a/shared/src/i18n/en/files.ts b/shared/src/i18n/en/files.ts index 0b99aa24c..dc9b3a0a6 100644 --- a/shared/src/i18n/en/files.ts +++ b/shared/src/i18n/en/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Link file', 'files.linkEmpty': 'No places or bookings to link yet', 'files.menu': 'More options', - 'files.uploadErrorSize': 'File is too large (max 50 MB)', + 'files.uploadErrorSize': 'File is too large (max {max} MB)', 'files.title': 'Files', 'files.pageTitle': 'Files & Documents', 'files.subtitle': '{count} files for {trip}', diff --git a/shared/src/i18n/es/files.ts b/shared/src/i18n/es/files.ts index 7ceb95990..c6ae6c614 100644 --- a/shared/src/i18n/es/files.ts +++ b/shared/src/i18n/es/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Vincular archivo', 'files.linkEmpty': 'Aún no hay lugares ni reservas para vincular', 'files.menu': 'Más opciones', - 'files.uploadErrorSize': 'El archivo es demasiado grande (máx. 50 MB)', + 'files.uploadErrorSize': 'El archivo es demasiado grande (máx. {max} MB)', 'files.title': 'Archivos', 'files.pageTitle': 'Archivos y documentos', 'files.subtitle': '{count} archivos para {trip}', diff --git a/shared/src/i18n/et/files.ts b/shared/src/i18n/et/files.ts index 9d55ca7be..cc834ab64 100644 --- a/shared/src/i18n/et/files.ts +++ b/shared/src/i18n/et/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Seo fail', 'files.linkEmpty': 'Seostamiseks pole veel kohti ega broneeringuid', 'files.menu': 'Veel valikuid', - 'files.uploadErrorSize': 'Fail on liiga suur (kuni 50 MB)', + 'files.uploadErrorSize': 'Fail on liiga suur (kuni {max} MB)', 'files.title': 'Failid', 'files.pageTitle': 'Failid ja dokumendid', 'files.subtitle': '{count} faili reisile {trip}', diff --git a/shared/src/i18n/fr/files.ts b/shared/src/i18n/fr/files.ts index 7e74f2cd6..3e989d3b8 100644 --- a/shared/src/i18n/fr/files.ts +++ b/shared/src/i18n/fr/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Lier le fichier', 'files.linkEmpty': 'Aucun lieu ni réservation à lier pour le moment', 'files.menu': "Plus d'options", - 'files.uploadErrorSize': 'Le fichier est trop volumineux (max 50 Mo)', + 'files.uploadErrorSize': 'Le fichier est trop volumineux (max {max} Mo)', 'files.title': 'Fichiers', 'files.pageTitle': 'Fichiers et documents', 'files.subtitle': '{count} fichiers pour {trip}', diff --git a/shared/src/i18n/gr/files.ts b/shared/src/i18n/gr/files.ts index 723bd242f..3392e44fb 100644 --- a/shared/src/i18n/gr/files.ts +++ b/shared/src/i18n/gr/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Σύνδεση αρχείου', 'files.linkEmpty': 'Δεν υπάρχουν ακόμη μέρη ή κρατήσεις για σύνδεση', 'files.menu': 'Περισσότερες επιλογές', - 'files.uploadErrorSize': 'Το αρχείο είναι πολύ μεγάλο (μέγ. 50 MB)', + 'files.uploadErrorSize': 'Το αρχείο είναι πολύ μεγάλο (μέγ. {max} MB)', 'files.title': 'Αρχεία', 'files.pageTitle': 'Αρχεία & Έγγραφα', 'files.subtitle': '{count} αρχεία για {trip}', diff --git a/shared/src/i18n/hu/files.ts b/shared/src/i18n/hu/files.ts index 789840c94..96920723c 100644 --- a/shared/src/i18n/hu/files.ts +++ b/shared/src/i18n/hu/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Fájl összekapcsolása', 'files.linkEmpty': 'Még nincs összekapcsolható hely vagy foglalás', 'files.menu': 'További lehetőségek', - 'files.uploadErrorSize': 'A fájl túl nagy (max. 50 MB)', + 'files.uploadErrorSize': 'A fájl túl nagy (max. {max} MB)', 'files.title': 'Fájlok', 'files.pageTitle': 'Fájlok és dokumentumok', 'files.subtitle': '{count} fájl a következőhöz: {trip}', diff --git a/shared/src/i18n/id/files.ts b/shared/src/i18n/id/files.ts index 12632d3d4..97b786cf4 100644 --- a/shared/src/i18n/id/files.ts +++ b/shared/src/i18n/id/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Tautkan file', 'files.linkEmpty': 'Belum ada tempat atau reservasi untuk ditautkan', 'files.menu': 'Opsi lainnya', - 'files.uploadErrorSize': 'File terlalu besar (maks. 50 MB)', + 'files.uploadErrorSize': 'File terlalu besar (maks. {max} MB)', 'files.title': 'File', 'files.pageTitle': 'File & Dokumen', 'files.subtitle': '{count} file untuk {trip}', diff --git a/shared/src/i18n/it/files.ts b/shared/src/i18n/it/files.ts index ef4f4139d..9d54fcf7c 100644 --- a/shared/src/i18n/it/files.ts +++ b/shared/src/i18n/it/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Collega file', 'files.linkEmpty': 'Nessun luogo o prenotazione da collegare per ora', 'files.menu': 'Altre opzioni', - 'files.uploadErrorSize': 'Il file è troppo grande (max 50 MB)', + 'files.uploadErrorSize': 'Il file è troppo grande (max {max} MB)', 'files.title': 'File', 'files.pageTitle': 'File e documenti', 'files.subtitle': '{count} file per {trip}', diff --git a/shared/src/i18n/ja/files.ts b/shared/src/i18n/ja/files.ts index a66a7692e..fb848944d 100644 --- a/shared/src/i18n/ja/files.ts +++ b/shared/src/i18n/ja/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'ファイルをリンク', 'files.linkEmpty': 'リンクできる場所や予約がまだありません', 'files.menu': 'その他のオプション', - 'files.uploadErrorSize': 'ファイルが大きすぎます(最大50MB)', + 'files.uploadErrorSize': 'ファイルが大きすぎます(最大{max}MB)', 'files.title': 'ファイル', 'files.pageTitle': 'ファイル・ドキュメント', 'files.subtitle': '{trip} のファイル {count} 件', diff --git a/shared/src/i18n/ko/files.ts b/shared/src/i18n/ko/files.ts index 2eaa84978..ddec8c741 100644 --- a/shared/src/i18n/ko/files.ts +++ b/shared/src/i18n/ko/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': '파일 연결', 'files.linkEmpty': '연결할 장소나 예약이 아직 없습니다', 'files.menu': '추가 옵션', - 'files.uploadErrorSize': '파일이 너무 큽니다 (최대 50MB)', + 'files.uploadErrorSize': '파일이 너무 큽니다 (최대 {max}MB)', 'files.title': '파일', 'files.pageTitle': '파일 및 서류', 'files.subtitle': '{trip}의 파일 {count}개', diff --git a/shared/src/i18n/nl/files.ts b/shared/src/i18n/nl/files.ts index 1b6a1ab68..21c1f0720 100644 --- a/shared/src/i18n/nl/files.ts +++ b/shared/src/i18n/nl/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Bestand koppelen', 'files.linkEmpty': 'Nog geen plaatsen of boekingen om te koppelen', 'files.menu': 'Meer opties', - 'files.uploadErrorSize': 'Bestand is te groot (max. 50 MB)', + 'files.uploadErrorSize': 'Bestand is te groot (max. {max} MB)', 'files.title': 'Bestanden', 'files.pageTitle': 'Bestanden en documenten', 'files.subtitle': '{count} bestanden voor {trip}', diff --git a/shared/src/i18n/pl/files.ts b/shared/src/i18n/pl/files.ts index 3d9ff0565..72bf38912 100644 --- a/shared/src/i18n/pl/files.ts +++ b/shared/src/i18n/pl/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Połącz plik', 'files.linkEmpty': 'Brak miejsc lub rezerwacji do połączenia', 'files.menu': 'Więcej opcji', - 'files.uploadErrorSize': 'Plik jest zbyt duży (maks. 50 MB)', + 'files.uploadErrorSize': 'Plik jest zbyt duży (maks. {max} MB)', 'files.title': 'Pliki', 'files.pageTitle': 'Pliki i dokumenty', 'files.subtitle': '{count} plików dla {trip}', diff --git a/shared/src/i18n/ru/files.ts b/shared/src/i18n/ru/files.ts index 4df5fff65..324e6e21a 100644 --- a/shared/src/i18n/ru/files.ts +++ b/shared/src/i18n/ru/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Связать файл', 'files.linkEmpty': 'Пока нет мест или бронирований для связи', 'files.menu': 'Дополнительные параметры', - 'files.uploadErrorSize': 'Файл слишком большой (макс. 50 МБ)', + 'files.uploadErrorSize': 'Файл слишком большой (макс. {max} МБ)', 'files.title': 'Файлы', 'files.pageTitle': 'Файлы и документы', 'files.subtitle': '{count} файлов для {trip}', diff --git a/shared/src/i18n/sk/files.ts b/shared/src/i18n/sk/files.ts index 360056f29..063e4ae0e 100644 --- a/shared/src/i18n/sk/files.ts +++ b/shared/src/i18n/sk/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Prepojiť súbor', 'files.linkEmpty': 'Zatiaľ nie sú žiadne miesta ani rezervácie na prepojenie', 'files.menu': 'Ďalšie možnosti', - 'files.uploadErrorSize': 'Súbor je príliš veľký (max. 50 MB)', + 'files.uploadErrorSize': 'Súbor je príliš veľký (max. {max} MB)', 'files.title': 'Súbory', 'files.pageTitle': 'Súbory a dokumenty', 'files.subtitle': '{count} súborov pre {trip}', diff --git a/shared/src/i18n/sv/files.ts b/shared/src/i18n/sv/files.ts index fd6bb355d..cc00d5f12 100644 --- a/shared/src/i18n/sv/files.ts +++ b/shared/src/i18n/sv/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Länka fil', 'files.linkEmpty': 'Inga platser eller bokningar att länka ännu', 'files.menu': 'Fler alternativ', - 'files.uploadErrorSize': 'Filen är för stor (max 50 MB)', + 'files.uploadErrorSize': 'Filen är för stor (max {max} MB)', 'files.title': 'Filer', 'files.pageTitle': 'Filer & Dokument', 'files.subtitle': '{count} filer för {trip}', diff --git a/shared/src/i18n/th/files.ts b/shared/src/i18n/th/files.ts index 48bf31e2e..39ee9ffc9 100644 --- a/shared/src/i18n/th/files.ts +++ b/shared/src/i18n/th/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': "ลิงก์ไฟล์", 'files.linkEmpty': "ยังไม่มีสถานที่หรือการจองที่จะลิงก์", 'files.menu': "ตัวเลือกเพิ่มเติม", - 'files.uploadErrorSize': "ไฟล์มีขนาดใหญ่เกินไป (สูงสุด 50 MB)", + 'files.uploadErrorSize': "ไฟล์มีขนาดใหญ่เกินไป (สูงสุด {max} MB)", 'files.title': "ไฟล์", 'files.pageTitle': "ไฟล์และเอกสาร", 'files.subtitle': "{count} ไฟล์สำหรับ {trip}", diff --git a/shared/src/i18n/tr/files.ts b/shared/src/i18n/tr/files.ts index e71360d56..abb43d4ee 100644 --- a/shared/src/i18n/tr/files.ts +++ b/shared/src/i18n/tr/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Dosyayı bağla', 'files.linkEmpty': 'Bağlanacak yer veya rezervasyon yok', 'files.menu': 'Diğer seçenekler', - 'files.uploadErrorSize': 'Dosya çok büyük (maks. 50 MB)', + 'files.uploadErrorSize': 'Dosya çok büyük (maks. {max} MB)', 'files.title': 'Dosyalar', 'files.pageTitle': 'Dosyalar ve Belgeler', 'files.subtitle': '{trip} için {count} dosya', diff --git a/shared/src/i18n/uk/files.ts b/shared/src/i18n/uk/files.ts index 2ed850745..23f088a38 100644 --- a/shared/src/i18n/uk/files.ts +++ b/shared/src/i18n/uk/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Пов’язати файл', 'files.linkEmpty': 'Поки немає місць чи бронювань для зв’язування', 'files.menu': 'Більше опцій', - 'files.uploadErrorSize': 'Файл завеликий (макс. 50 МБ)', + 'files.uploadErrorSize': 'Файл завеликий (макс. {max} МБ)', 'files.title': 'Файли', 'files.pageTitle': 'Файли та документи', 'files.subtitle': '{count} файлів для {trip}', diff --git a/shared/src/i18n/vi/files.ts b/shared/src/i18n/vi/files.ts index d7e4812b1..bdea060d3 100644 --- a/shared/src/i18n/vi/files.ts +++ b/shared/src/i18n/vi/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': 'Liên kết tập tin', 'files.linkEmpty': 'Chưa có địa điểm hoặc đặt chỗ nào để liên kết', 'files.menu': 'Tùy chọn khác', - 'files.uploadErrorSize': 'Tệp quá lớn (tối đa 50 MB)', + 'files.uploadErrorSize': 'Tệp quá lớn (tối đa {max} MB)', 'files.uploadErrorType': 'Loại tập tin này không được hỗ trợ', 'files.title': 'Tập tin', 'files.pageTitle': 'Tập tin & Tài liệu', diff --git a/shared/src/i18n/zh-TW/files.ts b/shared/src/i18n/zh-TW/files.ts index 17d67f877..cc885f9e3 100644 --- a/shared/src/i18n/zh-TW/files.ts +++ b/shared/src/i18n/zh-TW/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': '關聯檔案', 'files.linkEmpty': '暫無可關聯的地點或預訂', 'files.menu': '更多選項', - 'files.uploadErrorSize': '檔案過大(最大 50 MB)', + 'files.uploadErrorSize': '檔案過大(最大 {max} MB)', 'files.title': '檔案', 'files.pageTitle': '檔案與文件', 'files.subtitle': '{trip} 的 {count} 個檔案', diff --git a/shared/src/i18n/zh/files.ts b/shared/src/i18n/zh/files.ts index 554ca9867..bc3008346 100644 --- a/shared/src/i18n/zh/files.ts +++ b/shared/src/i18n/zh/files.ts @@ -6,7 +6,7 @@ const files: TranslationStrings = { 'files.linkTitle': '关联文件', 'files.linkEmpty': '暂无可关联的地点或预订', 'files.menu': '更多选项', - 'files.uploadErrorSize': '文件过大(最大 50 MB)', + 'files.uploadErrorSize': '文件过大(最大 {max} MB)', 'files.title': '文件', 'files.pageTitle': '文件与文档', 'files.subtitle': '{trip} 的 {count} 个文件', diff --git a/wiki/Environment-Variables.md b/wiki/Environment-Variables.md index c2da00ca6..4bdf01f27 100644 --- a/wiki/Environment-Variables.md +++ b/wiki/Environment-Variables.md @@ -453,6 +453,7 @@ through environment variables. `TREK_PLACE_PHOTO_DIR` below is unaffected. | Variable | Description | Default | |------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| | `TREK_PLACE_PHOTO_DIR` | Directory where cached Google place photos are stored. Created recursively on boot. Set this to point photo storage at a dedicated mounted volume. | `uploads/photos/google` | +| `FILE_UPLOAD_LIMIT_MB` | Maximum size (in MB) of a file uploaded to a trip, a booking or a collab note. The upload dialogs use the same limit. Videos in Journey keep their own 500 MB cap. If you sit behind a reverse proxy, raise its upload limit as well. | `50` | | `BACKUP_UPLOAD_LIMIT_MB` | Maximum **compressed** size (in MB) of a restore-backup archive that may be uploaded. Raise it if your backups (which include the `uploads/` directory) exceed the default. Non-positive or invalid values abort startup. | `500` | | `RESTORE_FROM_BACKUP` | Path to a backup ZIP to restore on the **first** start, before any setup. Only acts while no database exists; afterwards it is ignored with a warning. A backup that fails the restore checks stops the start. See [Backups](Backups#restoring-on-a-new-install-before-setup). | none | | `BACKUP_MAX_DECOMPRESSED_MB` | Maximum **decompressed** size (in MB) of a restore-backup archive — the zip-bomb guard. Independent of `BACKUP_UPLOAD_LIMIT_MB` and enforced on both restore paths, so a restore that fits the upload cap can still be refused with `Backup exceeds the maximum decompressed size.` Raise both when restoring a very large instance. | `5120` (5 GB) |