diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0a24535..1aa4f31 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -132,12 +132,38 @@ const getDownloadUrl = (path: string, forceDownload?: boolean) => `/api/download const FileDownload = ({ url }: { url: string }) => ; const ZipDownload = ({ url, title = 'Download folder as ZIP', style }: { url: string, title?: string, style?: CSSProperties }) => ; +let sizeCounter: number = 0; + +const sizeMeter: { [key: number]: string } = { + 0: 'KB', + 1: 'MB', + 2: 'GB', + 3: 'TB' +}; + +// Format the byte +const manageByte = (num: number): string | number => { + if (!num) return 0; + let res: number = num / 1024; + + if (res > 1000) { + sizeCounter++; + return manageByte(res); + } else { + const value: number = sizeCounter; + sizeCounter = 0; + return res.toFixed(2) + sizeMeter[value]; + } +} + + const FileRow = ({ path, isDir, fileName, onCheckedChange, checked }: { path: string, isDir: boolean, fileName: string, onCheckedChange?: ChangeEventHandler, checked?: boolean | undefined, + size?: number | undefined, }) => { const Icon = isDir ? FaFolder : FaFileAlt; @@ -148,12 +174,14 @@ const FileRow = ({ path, isDir, fileName, onCheckedChange, checked }: { <> {fileName} {fileName === '..' && (parent dir)}
+ {size && manageByte(size)} ) : ( <> {fileName}
+ {size && manageByte(size)} {onCheckedChange != null && } @@ -179,6 +207,8 @@ const Browser = () => { try { const response = await axios.get('/api/browse', { params: { p: currentPath } }); setCurrentDirFiles(response.data); + let responseWithSize = await axios.get('/api/browse-withsize', { params: { p: currentPath } }); + setCurrentDirFiles(responseWithSize.data); } catch (err) { console.error(err); } diff --git a/src/app.ts b/src/app.ts index 82d6d6e..e73f124 100644 --- a/src/app.ts +++ b/src/app.ts @@ -228,6 +228,76 @@ export default ({ sharedPath: sharedPathIn, port, maxUploadSize, zipCompressionL })); + async function getFolderSize(folderPath: string): Promise { + let totalSize = 0; + + async function calculateSize(dirPath: string): Promise { + const files = await fs.readdir(dirPath); // Async readdir + for (const file of files) { + const filePath = join(dirPath, file); + const stats = await fs.stat(filePath); // Async stat + + if (stats.isDirectory()) { + await calculateSize(filePath); // Recursively get size for subdirectories + } else { + totalSize += stats.size; // Add file size + } + } + } + + await calculateSize(folderPath); // Start calculation from the root folder + + return totalSize; + } + + app.get('/api/browse-withsize', asyncHandler(async (req, res) => { + const browseRelPath = req.query['p'] || '/'; + assert(typeof browseRelPath === 'string'); + const browseAbsPath = await getFileAbsPath(browseRelPath); + + let readdirEntries = await fs.readdir(browseAbsPath, { withFileTypes: true }); + readdirEntries = readdirEntries.sort(({ name: a }, { name: b }) => new Intl.Collator(undefined, { numeric: true }).compare(a, b)); + + const entries = (await pMap(readdirEntries, async (entry) => { + try { + // TODO what if a file called ".." + const entryRelPath = join(browseRelPath, entry.name); + const entryAbsPath = join(browseAbsPath, entry.name); + const entryRealPath = await fs.realpath(entryAbsPath); + + if (!entryRealPath.startsWith(sharedPath)) { + console.warn('Ignoring symlink pointing outside shared path', entryRealPath); + return []; + } + + const stat = await fs.lstat(entryRealPath); + const isDir = stat.isDirectory(); + const size = isDir ? await getFolderSize(entryRealPath) : stat.size + + return [{ + path: entryRelPath, + isDir, + fileName: entry.name, + size: size + }]; + } catch (err) { + console.warn((err as Error).message); + // https://github.com/mifi/ezshare/issues/29 + return []; + } + }, { concurrency: 10 })).flat(); + + res.send({ + files: [ + { path: join(browseRelPath, '..'), fileName: '..', isDir: true }, + ...entries, + ], + cwd: browseRelPath, + sharedPath, + }); + })); + + app.get('/api/zip-files', asyncHandler(async (req, res) => { const zipFileName = `${new Date().toISOString().replace(/^(\d+-\d+-\d+)T(\d+):(\d+):(\d+).*$/, '$1 $2.$3.$3')}.zip`; const { files: filesJson } = req.query;