diff --git a/Dockerfile b/Dockerfile index a350928..ddecde1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,7 +68,7 @@ RUN bun install --frozen-lockfile --ignore-scripts COPY . . -EXPOSE 4096 +EXPOSE 3000 CMD ["bun", "run", "dev"] diff --git a/app/client/modules/backups/components/backup-progress-card.tsx b/app/client/modules/backups/components/backup-progress-card.tsx index f15ae74..f654798 100644 --- a/app/client/modules/backups/components/backup-progress-card.tsx +++ b/app/client/modules/backups/components/backup-progress-card.tsx @@ -34,21 +34,10 @@ export const BackupProgressCard = ({ scheduleId }: Props) => { }; }, [addEventListener, scheduleId]); - if (!progress) { - return ( - -
-
- Backup in progress -
- - ); - } - - const percentDone = Math.round(progress.percent_done * 100); - const currentFile = progress.current_files[0] || ""; + const percentDone = progress ? Math.round(progress.percent_done * 100) : 0; + const currentFile = progress?.current_files[0] || ""; const fileName = currentFile.split("/").pop() || currentFile; - const speed = formatBytes(progress.bytes_done / progress.seconds_elapsed); + const speed = progress ? formatBytes(progress.bytes_done / progress.seconds_elapsed) : null; return ( @@ -57,7 +46,7 @@ export const BackupProgressCard = ({ scheduleId }: Props) => {
Backup in progress
- {percentDone}% + {progress ? `${percentDone}%` : "—"}
@@ -66,35 +55,45 @@ export const BackupProgressCard = ({ scheduleId }: Props) => {

Files

- {progress.files_done.toLocaleString()} / {progress.total_files.toLocaleString()} + {progress ? ( + <> + {progress.files_done.toLocaleString()} / {progress.total_files.toLocaleString()} + + ) : ( + "—" + )}

Data

- / + {progress ? ( + <> + / + + ) : ( + "—" + )}

Elapsed

-

{formatDuration(progress.seconds_elapsed)}

+

{progress ? formatDuration(progress.seconds_elapsed) : "—"}

Speed

- {progress.seconds_elapsed > 0 ? `${speed.text} ${speed.unit}/s` : "Calculating..."} + {progress ? (progress.seconds_elapsed > 0 ? `${speed?.text} ${speed?.unit}/s` : "Calculating...") : "—"}

- {fileName && ( -
-

Current file

-

- {fileName} -

-
- )} +
+

Current file

+

+ {fileName || "—"} +

+
); }; diff --git a/app/server/app.ts b/app/server/app.ts index 3cdfa80..fc46038 100644 --- a/app/server/app.ts +++ b/app/server/app.ts @@ -46,9 +46,6 @@ export const createApp = () => { if (config.environment === "production") { app.use(secureHeaders()); - } - - if (config.environment !== "test") { app.use(honoLogger()); } diff --git a/app/server/index.ts b/app/server/index.ts index 8faf6a9..bdd9e32 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -40,6 +40,7 @@ process.on("SIGINT", async () => { export default await createHonoServer({ app, port: config.port, + defaultLogger: false, customBunServer: { idleTimeout: config.serverIdleTimeout, error(err) { diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 6c91153..7b29081 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -20,7 +20,7 @@ import { getOrganizationId } from "~/server/core/request-context"; const runningBackups = new Map(); -const calculateNextRun = (cronExpression: string): number => { +const calculateNextRun = (cronExpression: string) => { try { const interval = CronExpressionParser.parse(cronExpression, { currentDate: new Date(), @@ -36,7 +36,7 @@ const calculateNextRun = (cronExpression: string): number => { } }; -const processPattern = (pattern: string, volumePath: string): string => { +const processPattern = (pattern: string, volumePath: string) => { let isNegated = false; let p = pattern; @@ -161,7 +161,11 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody if (data.name) { const existingName = await db.query.backupSchedulesTable.findFirst({ - where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), + where: and( + eq(backupSchedulesTable.name, data.name), + ne(backupSchedulesTable.id, scheduleId), + eq(backupSchedulesTable.organizationId, organizationId), + ), }); if (existingName) { @@ -310,7 +314,11 @@ const executeBackup = async (scheduleId: number, manual = false) => { backupOptions.include = schedule.includePatterns.map((p) => processPattern(p, volumePath)); } - const releaseBackupLock = await repoMutex.acquireShared(repository.id, `backup:${volume.name}`, abortController.signal); + const releaseBackupLock = await repoMutex.acquireShared( + repository.id, + `backup:${volume.name}`, + abortController.signal, + ); let exitCode: number; try { const result = await restic.backup(repository.config, volumePath, { @@ -318,13 +326,14 @@ const executeBackup = async (scheduleId: number, manual = false) => { compressionMode: repository.compressionMode ?? "auto", organizationId, onProgress: (progress) => { - serverEvents.emit("backup:progress", { + const progressData = { organizationId, scheduleId, volumeName: volume.name, repositoryName: repository.name, ...progress, - }); + }; + serverEvents.emit("backup:progress", progressData); }, }); exitCode = result.exitCode; @@ -501,7 +510,10 @@ const runForget = async (scheduleId: number, repositoryId?: string) => { } const repository = await db.query.repositoriesTable.findFirst({ - where: and(eq(repositoriesTable.id, repositoryId ?? schedule.repositoryId), eq(repositoriesTable.organizationId, organizationId)), + where: and( + eq(repositoriesTable.id, repositoryId ?? schedule.repositoryId), + eq(repositoriesTable.organizationId, organizationId), + ), }); if (!repository) { diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 738aa20..53b52b7 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -945,7 +945,8 @@ const formatBandwidthLimit = (limit?: BandwidthLimit): string => { return ""; } - return `${Math.floor(kibibytesPerSecond)}`; + const limitValue = Math.max(1, Math.floor(kibibytesPerSecond)); + return `${limitValue}`; }; export const addCommonArgs = ( diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..32bce78 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,49 @@ +# Scripts + +Utility scripts for Zerobyte development and testing. + +## create-test-files.ts + +Generates temporary test files with random content for testing Zerobyte backup functionality. + +### Usage + +```bash +bun scripts/create-test-files.ts [options] +``` + +### Options + +| Option | Description | Default | +|--------|-------------|---------| +| `-c, --count ` | Number of files to create | 10 | +| `--min-size ` | Minimum file size | 1K | +| `--max-size ` | Maximum file size | 1M | +| `-o, --out ` | Output directory | ./tmp/test-files | +| `-n, --nested` | Create files in nested subdirectories | false | +| `-h, --help` | Show help message | - | + +### Size Format + +Sizes can be specified as: `[K|M|G|T][B]` + +- `100` = 100 bytes +- `10K` = 10 kilobytes +- `5M` = 5 megabytes +- `1G` = 1 gigabyte + +### Examples + +```bash +# Create 10 test files (default) +bun scripts/create-test-files.ts + +# Create 50 files, 10K to 100K, with nested directories +bun scripts/create-test-files.ts -c 50 --min-size 10K --max-size 100K -n + +# Create 5 files, 1MB to 10MB +bun scripts/create-test-files.ts -c 5 --min-size 1M --max-size 10M -o ./data/test-backup + +# Create 100 small files in nested structure +bun scripts/create-test-files.ts -c 100 --min-size 100B --max-size 1K -n +``` diff --git a/scripts/create-test-files.ts b/scripts/create-test-files.ts new file mode 100755 index 0000000..27e194d --- /dev/null +++ b/scripts/create-test-files.ts @@ -0,0 +1,173 @@ +#!/usr/bin/env bun +/** + * Creates temporary files for testing Zerobyte backup functionality. + * Generates files with various sizes and content patterns. + */ + +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; + +interface TestFile { + name: string; + size: number; + content?: Buffer; +} + +interface Options { + count: number; + minSize: number; + maxSize: number; + outDir: string; + nested: boolean; +} + +function parseArgs(): Options { + const args = process.argv.slice(2); + const options: Options = { + count: 10, + minSize: 1024, + maxSize: 1024 * 1024, + outDir: "./tmp/test-files", + nested: false, + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + switch (arg) { + case "--count": + case "-c": + options.count = parseInt(args[++i], 10); + break; + case "--min-size": + case "--minsize": + options.minSize = parseSize(args[++i]); + break; + case "--max-size": + case "--maxsize": + options.maxSize = parseSize(args[++i]); + break; + case "--out": + case "-o": + options.outDir = args[++i]; + break; + case "--nested": + case "-n": + options.nested = true; + break; + case "--help": + case "-h": + printHelp(); + process.exit(0); + } + } + + return options; +} + +function parseSize(size: string): number { + const match = size.match(/^(\d+)([kmgt]?)b?$/i); + if (!match) { + throw new Error(`Invalid size format: ${size}`); + } + const num = parseInt(match[1], 10); + const unit = match[2].toLowerCase(); + const multipliers: Record = { + "": 1, + k: 1024, + m: 1024 * 1024, + g: 1024 * 1024 * 1024, + t: 1024 * 1024 * 1024 * 1024, + }; + return num * (multipliers[unit] || 1); +} + +function printHelp(): void { + console.info(` +Usage: bun create-test-files.ts [options] + +Options: + -c, --count Number of files to create (default: 10) + --min-size Minimum file size (default: 1K) + --max-size Maximum file size (default: 1M) + -o, --out Output directory (default: ./tmp/test-files) + -n, --nested Create files in nested subdirectories + -h, --help Show this help message + +Size format: [K|M|G|T][B] (e.g., 100K, 5M, 1G) +`); +} + +function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function generateContent(size: number): Buffer { + const content = Buffer.alloc(size); + for (let i = 0; i < size; i++) { + content[i] = randomInt(32, 126); + } + return content; +} + +async function createFiles(options: Options): Promise { + console.info(`Creating ${options.count} test files...`); + console.info(` Output directory: ${options.outDir}`); + console.info(` Size range: ${formatSize(options.minSize)} - ${formatSize(options.maxSize)}`); + console.info(` Nested: ${options.nested}`); + + await mkdir(options.outDir, { recursive: true }); + + const files: TestFile[] = []; + for (let i = 0; i < options.count; i++) { + const size = randomInt(options.minSize, options.maxSize); + const fileNum = i + 1; + + let dir = options.outDir; + if (options.nested) { + const depth = randomInt(1, 3); + const parts: string[] = []; + for (let d = 0; d < depth; d++) { + parts.push(`level${d + 1}`); + } + dir = join(options.outDir, ...parts); + } + + const name = join(dir, `test-file-${fileNum.toString().padStart(4, "0")}.txt`); + files.push({ name, size }); + } + + let totalSize = 0; + for (const file of files) { + await mkdir(file.name.substring(0, file.name.lastIndexOf("/")), { recursive: true }); + const content = generateContent(file.size); + await writeFile(file.name, content); + totalSize += file.size; + process.stdout.write(`\r Created ${files.indexOf(file) + 1}/${options.count} files`); + } + + console.info(`\nDone! Created ${options.count} files totaling ${formatSize(totalSize)}`); + console.info(`Location: ${options.outDir}`); +} + +function formatSize(bytes: number): string { + const units = ["B", "KB", "MB", "GB", "TB"]; + let size = bytes; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return `${size.toFixed(2)} ${units[unitIndex]}`; +} + +async function main(): Promise { + try { + const options = parseArgs(); + await createFiles(options); + } catch (error) { + console.error("Error:", error instanceof Error ? error.message : error); + process.exit(1); + } +} + +main();