diff --git a/Dockerfile b/Dockerfile
index 7320f29..9498af7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@ FROM denoland/deno:ubuntu-2.5.6
EXPOSE 8000
-RUN apt-get update && apt-get install -y make zip
+RUN apt-get update && apt-get install -y make zip coreutils
WORKDIR /app
diff --git a/components/files/ListFiles.tsx b/components/files/ListFiles.tsx
index b4077a9..7b002c7 100644
--- a/components/files/ListFiles.tsx
+++ b/components/files/ListFiles.tsx
@@ -158,7 +158,7 @@ export default function ListFiles(
{isShowingNotes || isShowingPhotos ? null : (
- -
+ {humanFileSize(directory.size_in_bytes)}
|
)}
{isShowingPhotos || fileShareId ? null : (
diff --git a/components/files/ListPhotos.tsx b/components/files/ListPhotos.tsx
index 40b00b4..7a15153 100644
--- a/components/files/ListPhotos.tsx
+++ b/components/files/ListPhotos.tsx
@@ -79,7 +79,7 @@ export default function ListPhotos(
{isShowingNotes ? null : (
- -
+ {humanFileSize(directory.size_in_bytes)}
|
)}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 9f337cb..a89cdce 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -18,7 +18,7 @@ services:
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
radicale:
- image: tomsquest/docker-radicale:3.5.8.2
+ image: tomsquest/docker-radicale:3.6.0.0
ports:
- 5232:5232
init: true
diff --git a/docker-compose.yml b/docker-compose.yml
index b60fe71..58b0212 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
website:
- image: ghcr.io/bewcloud/bewcloud:v3.5.1
+ image: ghcr.io/bewcloud/bewcloud:v3.6.0
restart: always
ports:
- 127.0.0.1:8000:8000
@@ -32,7 +32,7 @@ services:
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
radicale:
- image: tomsquest/docker-radicale:3.5.8.2
+ image: tomsquest/docker-radicale:3.6.0.0
# NOTE: uncomment below only if you need to connect to the CardDav/CalDav servers from outside the container
# ports:
# - 127.0.0.1:5232:5232
diff --git a/lib/models/files.ts b/lib/models/files.ts
index ee96ab3..beec564 100644
--- a/lib/models/files.ts
+++ b/lib/models/files.ts
@@ -4,7 +4,13 @@ import { Cookie, getCookies, setCookie } from '@std/http';
import { AppConfig } from '/lib/config.ts';
import { Directory, DirectoryFile, FileShare } from '/lib/types.ts';
-import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
+import {
+ bytesFromHumanFileSize,
+ sortDirectoriesByName,
+ sortEntriesByName,
+ sortFilesByName,
+ TRASH_PATH,
+} from '/lib/utils/files.ts';
import Database, { sql } from '/lib/interfaces/database.ts';
import {
COOKIE_NAME as AUTH_COOKIE_NAME,
@@ -39,12 +45,14 @@ export class DirectoryModel {
for (const entry of directoryEntries) {
const stat = await Deno.stat(join(rootPath, entry.name));
+ const directorySize = await getDirectorySize(join(rootPath, entry.name));
+
const directory: Directory = {
user_id: userId,
parent_path: path,
directory_name: entry.name,
has_write_access: true,
- size_in_bytes: stat.size,
+ size_in_bytes: directorySize || stat.size,
file_share_id: fileShares.find((fileShare) => fileShare.file_path === `${join(path, entry.name)}/`)?.id || null,
updated_at: stat.mtime || new Date(),
created_at: stat.birthtime || new Date(),
@@ -141,12 +149,14 @@ export class DirectoryModel {
parentPath = '/';
}
+ const directorySize = await getDirectorySize(join(rootPath, relativeDirectoryPath));
+
const directory: Directory = {
user_id: userId,
parent_path: parentPath,
directory_name: directoryName,
has_write_access: true,
- size_in_bytes: stat.size,
+ size_in_bytes: directorySize || stat.size,
file_share_id: fileShares.find((fileShare) =>
fileShare.file_path === `${join(relativeDirectoryPath, directoryName)}/`
)?.id || null,
@@ -764,3 +774,45 @@ export async function getPathInfo(userId: string, path: string): Promise<{ isDir
isFile: stat.isFile,
};
}
+
+// NOTE: We're using `-h` (human readable) and parsing the output because that's more stable than `-B 1B` across different systems for a reliable byte size.
+async function getDirectorySize(path: string): Promise {
+ try {
+ const controller = new AbortController();
+ const commandTimeout = setTimeout(() => controller.abort(), 5_000);
+
+ const command = new Deno.Command(`du`, {
+ args: [
+ `-sh`,
+ path,
+ ],
+ signal: controller.signal,
+ });
+
+ const { code, stdout, stderr } = await command.output();
+
+ if (commandTimeout) {
+ clearTimeout(commandTimeout);
+ }
+
+ if (code !== 0) {
+ if (stderr) {
+ throw new Error(new TextDecoder().decode(stderr));
+ }
+
+ throw new Error(`Unknown error running "du"`);
+ }
+
+ const output = new TextDecoder().decode(stdout);
+
+ const value = output.split('\t')[0].trim();
+
+ const number = Number.parseFloat(value.match(/\d+(\.\d+)?/)?.[0] || '0');
+ const unit = value.match(/[A-Z]+/)?.[0] || 'B'.toUpperCase();
+
+ return bytesFromHumanFileSize(`${number} ${unit}B`);
+ } catch (error) {
+ console.error(error);
+ return 0;
+ }
+}
diff --git a/lib/utils/files.ts b/lib/utils/files.ts
index f555901..3972e3d 100644
--- a/lib/utils/files.ts
+++ b/lib/utils/files.ts
@@ -17,9 +17,21 @@ export function humanFileSize(bytes: number) {
++unitIndex;
} while (Math.round(Math.abs(bytes) * roundedPower) / roundedPower >= 1024 && unitIndex < units.length - 1);
+ if (bytes.toFixed(2).split('.')[1] === '00') {
+ return `${bytes.toFixed(0)} ${units[unitIndex]}`;
+ }
+
return `${bytes.toFixed(2)} ${units[unitIndex]}`;
}
+export function bytesFromHumanFileSize(humanFileSize: string) {
+ const [numberString, unit] = humanFileSize.split(' ');
+ const number = Number.parseFloat(numberString);
+
+ return number *
+ (unit === 'B' ? 1 : 1024 ** (['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'].indexOf(unit) + 1));
+}
+
export function sortEntriesByName(entryA: Deno.DirEntry, entryB: Deno.DirEntry) {
const nameA = entryA.name.toLocaleLowerCase();
const nameB = entryB.name.toLocaleLowerCase();
diff --git a/lib/utils/files_test.ts b/lib/utils/files_test.ts
index c9f3cb3..0faf087 100644
--- a/lib/utils/files_test.ts
+++ b/lib/utils/files_test.ts
@@ -1,6 +1,6 @@
import { assertEquals } from '@std/assert';
-import { humanFileSize } from './files.ts';
+import { bytesFromHumanFileSize, humanFileSize } from './files.ts';
Deno.test('that humanFileSize works', () => {
const tests: { input: number; expected: string }[] = [
@@ -10,7 +10,11 @@ Deno.test('that humanFileSize works', () => {
},
{
input: 1024,
- expected: '1.00 KB',
+ expected: '1 KB',
+ },
+ {
+ input: 1100,
+ expected: '1.07 KB',
},
{
input: 10000,
@@ -22,16 +26,70 @@ Deno.test('that humanFileSize works', () => {
},
{
input: 1048576,
- expected: '1.00 MB',
+ expected: '1 MB',
},
{
input: 1073741824,
- expected: '1.00 GB',
+ expected: '1 GB',
+ },
+ {
+ input: 1150976,
+ expected: '1.10 MB',
+ },
+ {
+ input: 1085276160,
+ expected: '1.01 GB',
},
];
for (const test of tests) {
const output = humanFileSize(test.input);
- assertEquals(output, test.expected);
+ assertEquals(output, test.expected, `Expected ${test.input} to be ${test.expected} but got ${output}`);
+ }
+});
+
+Deno.test('that bytesFromHumanFileSize works', () => {
+ const tests: { input: string; expected: number }[] = [
+ {
+ input: '1000 B',
+ expected: 1000,
+ },
+ {
+ input: '1024 KB',
+ expected: 1024 * 1024,
+ },
+ {
+ input: '10000 KB',
+ expected: 10000 * 1024,
+ },
+ {
+ input: '1 B',
+ expected: 1,
+ },
+ {
+ input: '1048576 MB',
+ expected: 1048576 * 1024 * 1024,
+ },
+ {
+ input: '1073741824 GB',
+ expected: 1073741824 * 1024 * 1024 * 1024,
+ },
+ {
+ input: '1.01 B',
+ expected: 1.01,
+ },
+ {
+ input: '1048576.15 MB',
+ expected: 1048576.15 * 1024 * 1024,
+ },
+ {
+ input: '1073741824.37 GB',
+ expected: 1073741824.37 * 1024 * 1024 * 1024,
+ },
+ ];
+
+ for (const test of tests) {
+ const output = bytesFromHumanFileSize(test.input);
+ assertEquals(output, test.expected, `Expected ${test.input} to be ${test.expected} but got ${output}`);
}
});
|