diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index a36e74b..44f86a5 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -756,6 +756,15 @@ export type ListRepositoriesResponses = { password?: string; path?: string; username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; }; createdAt: number; id: string; @@ -763,7 +772,7 @@ export type ListRepositoriesResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; updatedAt: number; }>; }; @@ -823,6 +832,15 @@ export type CreateRepositoryData = { password?: string; path?: string; username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; }; name: string; compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off'; @@ -952,6 +970,15 @@ export type GetRepositoryResponses = { password?: string; path?: string; username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; }; createdAt: number; id: string; @@ -959,7 +986,7 @@ export type GetRepositoryResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; updatedAt: number; }; }; @@ -1208,6 +1235,15 @@ export type ListBackupSchedulesResponses = { password?: string; path?: string; username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; }; createdAt: number; id: string; @@ -1215,7 +1251,7 @@ export type ListBackupSchedulesResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; updatedAt: number; }; repositoryId: string; @@ -1430,6 +1466,15 @@ export type GetBackupScheduleResponses = { password?: string; path?: string; username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; }; createdAt: number; id: string; @@ -1437,7 +1482,7 @@ export type GetBackupScheduleResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; updatedAt: number; }; repositoryId: string; @@ -1633,6 +1678,15 @@ export type GetBackupScheduleForVolumeResponses = { password?: string; path?: string; username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; }; createdAt: number; id: string; @@ -1640,7 +1694,7 @@ export type GetBackupScheduleForVolumeResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; updatedAt: number; }; repositoryId: string; diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index ee4d1d8..436e0af 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -29,6 +29,7 @@ const internalFormSchema = type({ frequency: "string", dailyTime: "string?", weeklyDay: "string?", + limitUploadKbps: "number?", keepLast: "number?", keepHourly: "number?", keepDaily: "number?", @@ -86,6 +87,7 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu weeklyDay, includePatterns: schedule.includePatterns || undefined, excludePatternsText: schedule.excludePatterns?.join("\n") || undefined, + limitUploadKbps: schedule.limitUploadKbps || undefined, ...schedule.retentionPolicy, }; }; @@ -247,6 +249,29 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: )} /> )} + + ( + + Upload speed limit (KB/s) + + field.onChange(v.target.value ? Number(v.target.value) : undefined)} + /> + + + Limit upload bandwidth in kilobytes per second. Leave empty for unlimited speed. + + + + )} + /> @@ -482,6 +507,12 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: {repositoriesData?.find((r) => r.id === formValues.repositoryId)?.name || "—"}

+ {formValues.limitUploadKbps && ( +
+

Upload speed limit

+

{formValues.limitUploadKbps} KB/s

+
+ )} {formValues.includePatterns && formValues.includePatterns.length > 0 && (

Include paths

diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index 0be7a2a..0bc0873 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -156,6 +156,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined, includePatterns: formValues.includePatterns, excludePatterns: formValues.excludePatterns, + limitUploadKbps: formValues.limitUploadKbps, }, }); }; @@ -170,6 +171,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon retentionPolicy: schedule.retentionPolicy || undefined, includePatterns: schedule.includePatterns || undefined, excludePatterns: schedule.excludePatterns || undefined, + limitUploadKbps: schedule.limitUploadKbps || undefined, }, }); }; diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx index f982086..17ccc73 100644 --- a/app/client/modules/backups/routes/create-backup.tsx +++ b/app/client/modules/backups/routes/create-backup.tsx @@ -90,6 +90,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) { retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined, includePatterns: formValues.includePatterns, excludePatterns: formValues.excludePatterns, + limitUploadKbps: formValues.limitUploadKbps, }, }); }; diff --git a/app/drizzle/0011_lazy_havok.sql b/app/drizzle/0011_lazy_havok.sql new file mode 100644 index 0000000..064f951 --- /dev/null +++ b/app/drizzle/0011_lazy_havok.sql @@ -0,0 +1 @@ +ALTER TABLE `backup_schedules_table` ADD `limit_upload_kbps` integer; \ No newline at end of file diff --git a/app/drizzle/meta/0011_snapshot.json b/app/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..ce8603b --- /dev/null +++ b/app/drizzle/meta/0011_snapshot.json @@ -0,0 +1,466 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3ad94485-0846-44f1-8430-44d75bf16f69", + "prevId": "17f234ba-4123-4951-a39f-6002d537435f", + "tables": { + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "limit_upload_kbps": { + "name": "limit_upload_kbps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index abdba02..fefae8f 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,83 +1,90 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1755765658194, - "tag": "0000_known_madelyne_pryor", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1755775437391, - "tag": "0001_far_frank_castle", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1756930554198, - "tag": "0002_cheerful_randall", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1758653407064, - "tag": "0003_mature_hellcat", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1758961535488, - "tag": "0004_wealthy_tomas", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1759416698274, - "tag": "0005_simple_alice", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1760734377440, - "tag": "0006_secret_micromacro", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1761224911352, - "tag": "0007_watery_sersi", - "breakpoints": true - }, - { - "idx": 8, - "version": "6", - "when": 1761414054481, - "tag": "0008_silent_lady_bullseye", - "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1762095226041, - "tag": "0009_little_adam_warlock", - "breakpoints": true - }, - { - "idx": 10, - "version": "6", - "when": 1762610065889, - "tag": "0010_perfect_proemial_gods", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1755765658194, + "tag": "0000_known_madelyne_pryor", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1755775437391, + "tag": "0001_far_frank_castle", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1756930554198, + "tag": "0002_cheerful_randall", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1758653407064, + "tag": "0003_mature_hellcat", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1758961535488, + "tag": "0004_wealthy_tomas", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1759416698274, + "tag": "0005_simple_alice", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1760734377440, + "tag": "0006_secret_micromacro", + "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1761224911352, + "tag": "0007_watery_sersi", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1761414054481, + "tag": "0008_silent_lady_bullseye", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1762095226041, + "tag": "0009_little_adam_warlock", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1762610065889, + "tag": "0010_perfect_proemial_gods", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1763728410318, + "tag": "0011_lazy_havok", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index dbb2fff..0fb7653 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -83,6 +83,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { }>(), excludePatterns: text("exclude_patterns", { mode: "json" }).$type().default([]), includePatterns: text("include_patterns", { mode: "json" }).$type().default([]), + limitUploadKbps: int("limit_upload_kbps", { mode: "number" }), lastBackupAt: int("last_backup_at", { mode: "number" }), lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress">(), lastBackupError: text("last_backup_error"), diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts index 8e11e77..4fc6142 100644 --- a/app/server/modules/backups/backups.dto.ts +++ b/app/server/modules/backups/backups.dto.ts @@ -24,6 +24,7 @@ const backupScheduleSchema = type({ retentionPolicy: retentionPolicySchema.or("null"), excludePatterns: "string[] | null", includePatterns: "string[] | null", + limitUploadKbps: "number | null", lastBackupAt: "number | null", lastBackupStatus: "'success' | 'error' | 'in_progress' | null", lastBackupError: "string | null", @@ -114,6 +115,7 @@ export const createBackupScheduleBody = type({ retentionPolicy: retentionPolicySchema.optional(), excludePatterns: "string[]?", includePatterns: "string[]?", + limitUploadKbps: "number?", tags: "string[]?", }); @@ -149,6 +151,7 @@ export const updateBackupScheduleBody = type({ retentionPolicy: retentionPolicySchema.optional(), excludePatterns: "string[]?", includePatterns: "string[]?", + limitUploadKbps: "number?", tags: "string[]?", }); diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 9e98459..934ef45 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -88,6 +88,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { retentionPolicy: data.retentionPolicy ?? null, excludePatterns: data.excludePatterns ?? [], includePatterns: data.includePatterns ?? [], + limitUploadKbps: data.limitUploadKbps ?? null, nextBackupAt: nextBackupAt, }) .returning(); @@ -212,9 +213,11 @@ const executeBackup = async (scheduleId: number, manual = false) => { exclude?: string[]; include?: string[]; tags?: string[]; + limitUploadKbps?: number | null; signal?: AbortSignal; } = { tags: [schedule.id.toString()], + limitUploadKbps: schedule.limitUploadKbps, signal: abortController.signal, }; diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 97146d8..726afa7 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -234,6 +234,7 @@ const backup = async ( exclude?: string[]; include?: string[]; tags?: string[]; + limitUploadKbps?: number | null; signal?: AbortSignal; onProgress?: (progress: BackupProgress) => void; }, @@ -243,6 +244,10 @@ const backup = async ( const args: string[] = ["--repo", repoUrl, "backup", "--one-file-system"]; + if (options?.limitUploadKbps) { + args.push("--limit-upload", String(options.limitUploadKbps)); + } + if (options?.tags && options.tags.length > 0) { for (const tag of options.tags) { args.push("--tag", tag);