feat(files): FILE_UPLOAD_LIMIT_MB sets the upload limit for files and notes

The 50 MB limit for files, booking attachments and note attachments becomes FILE_UPLOAD_LIMIT_MB, 50 by default. The server hands it to the client through the app config, so the upload dialogs refuse a file over it up front and say what the limit is; on desktop a file over it no longer vanishes without a word. (#1364)
This commit is contained in:
Maurice
2026-10-01 11:56:14 +02:00
parent 750b178d5b
commit 885896aba2
42 changed files with 93 additions and 41 deletions
+2 -1
View File
@@ -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<string, PermissionLevel> }) => {
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<string, PermissionLevel> }) => {
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)
@@ -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,
})
@@ -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<number | null>(null)
const [linkFileId, setLinkFileId] = useState<number | null>(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<number | null>(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
+5
View File
@@ -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<AuthResponse>
}
@@ -149,6 +152,7 @@ export const useAuthStore = create<AuthState>()(
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<AuthState>()(
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 () => {
@@ -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)
})
+1
View File
@@ -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.
+8
View File
@@ -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),
+1
View File
@@ -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,
+1
View File
@@ -17,6 +17,7 @@ export {
deriveWebauthn,
deriveIntegrations,
deriveBackup,
deriveFiles,
deriveDb,
derivePaths,
deriveNet,
+2
View File
@@ -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
+3 -2
View File
@@ -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']);
+4 -1
View File
@@ -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,
+3 -2
View File
@@ -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),
}),
@@ -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);
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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} 件',
+1 -1
View File
@@ -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}개',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}",
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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} 個檔案',
+1 -1
View File
@@ -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} 个文件',
+1
View File
@@ -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) |