Support downloading directories as a zip archive (#106)

* Add directory download as zip feature

Implements the ability for users to download directories as zip files if enabled in config. Adds a new API route for directory zipping, updates UI components to show a download button for directories, and introduces related config and type changes. Also includes a new download icon.

* Windows path bugfix

* Include empty directories in zip archive

* Address feedback

- `isDirectoryDownloadsAllowed` -> `areDirectoryDownloadsAllowed`
- send `parentPath` & `name` to API instead of resolving `fullPath` on client
- call `ensureUserPathIsValidAndSecurelyAccessible` before zipping
- set config `allowDirectoryDownloads` default to `false`
- add `zip` to Dockerfile and replace in-house zip algorithm
- replace `download.svg` with heroicon's `arrow-down-tray`
- `replace` with glob -> `replaceAll` with string

* Cleanup apt-get command

* Remove unused zip archive and directory functions
This commit is contained in:
Tilman
2025-10-08 15:32:45 +02:00
committed by GitHub
parent c81ef77370
commit c4a5166e3b
13 changed files with 153 additions and 6 deletions

View File

@@ -22,6 +22,7 @@ export class AppConfig {
files: {
rootPath: 'data-files',
allowPublicSharing: false,
allowDirectoryDownloads: true,
},
core: {
enabledApps: ['news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'],
@@ -179,6 +180,12 @@ export class AppConfig {
return this.config.files.allowPublicSharing;
}
static async areDirectoryDownloadsAllowed(): Promise<boolean> {
await this.loadConfig();
return this.config.files.allowDirectoryDownloads;
}
static async getFilesRootPath(): Promise<string> {
await this.loadConfig();

View File

@@ -598,7 +598,11 @@ export async function ensureUserPathIsValidAndSecurelyAccessible(userId: string,
const resolvedFullPath = `${resolve(fullPath)}/`;
if (!resolvedFullPath.startsWith(userRootPath)) {
// Normalize path separators for consistent comparison on Windows
const normalizedUserRootPath = userRootPath.replaceAll('\\', '/');
const normalizedResolvedFullPath = resolvedFullPath.replaceAll('\\', '/');
if (!normalizedResolvedFullPath.startsWith(normalizedUserRootPath)) {
throw new Error('Invalid file path');
}
}

View File

@@ -184,6 +184,8 @@ export interface Config {
rootPath: string;
/** If true, public file sharing will be allowed (still requires a user to enable sharing for a given file or directory) */
allowPublicSharing: boolean;
/** If true, directories can be downloaded as zip files */
allowDirectoryDownloads: boolean;
};
core: {
/** dashboard and files cannot be disabled */