mirror of
https://github.com/bewcloud/bewcloud.git
synced 2026-03-11 08:54:49 +00:00
parent
bfd4851098
commit
fb2a7d5cce
8 changed files with 136 additions and 14 deletions
|
|
@ -2,7 +2,7 @@ FROM denoland/deno:ubuntu-2.5.6
|
||||||
|
|
||||||
EXPOSE 8000
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export default function ListFiles(
|
||||||
</td>
|
</td>
|
||||||
{isShowingNotes || isShowingPhotos ? null : (
|
{isShowingNotes || isShowingPhotos ? null : (
|
||||||
<td class='px-6 py-4 text-slate-200'>
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
-
|
{humanFileSize(directory.size_in_bytes)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{isShowingPhotos || fileShareId ? null : (
|
{isShowingPhotos || fileShareId ? null : (
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default function ListPhotos(
|
||||||
</td>
|
</td>
|
||||||
{isShowingNotes ? null : (
|
{isShowingNotes ? null : (
|
||||||
<td class='px-6 py-4 text-slate-200'>
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
-
|
{humanFileSize(directory.size_in_bytes)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td class='px-6 py-4'>
|
<td class='px-6 py-4'>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ services:
|
||||||
|
|
||||||
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
|
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
|
||||||
radicale:
|
radicale:
|
||||||
image: tomsquest/docker-radicale:3.5.8.2
|
image: tomsquest/docker-radicale:3.6.0.0
|
||||||
ports:
|
ports:
|
||||||
- 5232:5232
|
- 5232:5232
|
||||||
init: true
|
init: true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
website:
|
website:
|
||||||
image: ghcr.io/bewcloud/bewcloud:v3.5.1
|
image: ghcr.io/bewcloud/bewcloud:v3.6.0
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8000:8000
|
- 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.
|
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
|
||||||
radicale:
|
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
|
# NOTE: uncomment below only if you need to connect to the CardDav/CalDav servers from outside the container
|
||||||
# ports:
|
# ports:
|
||||||
# - 127.0.0.1:5232:5232
|
# - 127.0.0.1:5232:5232
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ import { Cookie, getCookies, setCookie } from '@std/http';
|
||||||
|
|
||||||
import { AppConfig } from '/lib/config.ts';
|
import { AppConfig } from '/lib/config.ts';
|
||||||
import { Directory, DirectoryFile, FileShare } from '/lib/types.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 Database, { sql } from '/lib/interfaces/database.ts';
|
||||||
import {
|
import {
|
||||||
COOKIE_NAME as AUTH_COOKIE_NAME,
|
COOKIE_NAME as AUTH_COOKIE_NAME,
|
||||||
|
|
@ -39,12 +45,14 @@ export class DirectoryModel {
|
||||||
for (const entry of directoryEntries) {
|
for (const entry of directoryEntries) {
|
||||||
const stat = await Deno.stat(join(rootPath, entry.name));
|
const stat = await Deno.stat(join(rootPath, entry.name));
|
||||||
|
|
||||||
|
const directorySize = await getDirectorySize(join(rootPath, entry.name));
|
||||||
|
|
||||||
const directory: Directory = {
|
const directory: Directory = {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
parent_path: path,
|
parent_path: path,
|
||||||
directory_name: entry.name,
|
directory_name: entry.name,
|
||||||
has_write_access: true,
|
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,
|
file_share_id: fileShares.find((fileShare) => fileShare.file_path === `${join(path, entry.name)}/`)?.id || null,
|
||||||
updated_at: stat.mtime || new Date(),
|
updated_at: stat.mtime || new Date(),
|
||||||
created_at: stat.birthtime || new Date(),
|
created_at: stat.birthtime || new Date(),
|
||||||
|
|
@ -141,12 +149,14 @@ export class DirectoryModel {
|
||||||
parentPath = '/';
|
parentPath = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const directorySize = await getDirectorySize(join(rootPath, relativeDirectoryPath));
|
||||||
|
|
||||||
const directory: Directory = {
|
const directory: Directory = {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
parent_path: parentPath,
|
parent_path: parentPath,
|
||||||
directory_name: directoryName,
|
directory_name: directoryName,
|
||||||
has_write_access: true,
|
has_write_access: true,
|
||||||
size_in_bytes: stat.size,
|
size_in_bytes: directorySize || stat.size,
|
||||||
file_share_id: fileShares.find((fileShare) =>
|
file_share_id: fileShares.find((fileShare) =>
|
||||||
fileShare.file_path === `${join(relativeDirectoryPath, directoryName)}/`
|
fileShare.file_path === `${join(relativeDirectoryPath, directoryName)}/`
|
||||||
)?.id || null,
|
)?.id || null,
|
||||||
|
|
@ -764,3 +774,45 @@ export async function getPathInfo(userId: string, path: string): Promise<{ isDir
|
||||||
isFile: stat.isFile,
|
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<number> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,21 @@ export function humanFileSize(bytes: number) {
|
||||||
++unitIndex;
|
++unitIndex;
|
||||||
} while (Math.round(Math.abs(bytes) * roundedPower) / roundedPower >= 1024 && unitIndex < units.length - 1);
|
} 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]}`;
|
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) {
|
export function sortEntriesByName(entryA: Deno.DirEntry, entryB: Deno.DirEntry) {
|
||||||
const nameA = entryA.name.toLocaleLowerCase();
|
const nameA = entryA.name.toLocaleLowerCase();
|
||||||
const nameB = entryB.name.toLocaleLowerCase();
|
const nameB = entryB.name.toLocaleLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { humanFileSize } from './files.ts';
|
import { bytesFromHumanFileSize, humanFileSize } from './files.ts';
|
||||||
|
|
||||||
Deno.test('that humanFileSize works', () => {
|
Deno.test('that humanFileSize works', () => {
|
||||||
const tests: { input: number; expected: string }[] = [
|
const tests: { input: number; expected: string }[] = [
|
||||||
|
|
@ -10,7 +10,11 @@ Deno.test('that humanFileSize works', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 1024,
|
input: 1024,
|
||||||
expected: '1.00 KB',
|
expected: '1 KB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 1100,
|
||||||
|
expected: '1.07 KB',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 10000,
|
input: 10000,
|
||||||
|
|
@ -22,16 +26,70 @@ Deno.test('that humanFileSize works', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 1048576,
|
input: 1048576,
|
||||||
expected: '1.00 MB',
|
expected: '1 MB',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 1073741824,
|
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) {
|
for (const test of tests) {
|
||||||
const output = humanFileSize(test.input);
|
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}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue