refactoring(bw-limit): stylistic refactoring

This commit is contained in:
Nicolas Meienberger 2026-01-15 17:57:38 +01:00
parent cb72e6c073
commit 1622e34b0d
9 changed files with 1657 additions and 470 deletions

2
.gitignore vendored
View file

@ -31,3 +31,5 @@ node_modules/
playwright/.auth playwright/.auth
playwright/temp playwright/temp
.idea/

View file

@ -10,13 +10,7 @@ import {
import { Input } from "../../../../components/ui/input"; import { Input } from "../../../../components/ui/input";
import { Textarea } from "../../../../components/ui/textarea"; import { Textarea } from "../../../../components/ui/textarea";
import { Checkbox } from "../../../../components/ui/checkbox"; import { Checkbox } from "../../../../components/ui/checkbox";
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "../../../../components/ui/select";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible";
import type { RepositoryFormValues } from "../create-repository-form"; import type { RepositoryFormValues } from "../create-repository-form";
@ -30,26 +24,15 @@ type Props = {
export const AdvancedForm = ({ form }: Props) => { export const AdvancedForm = ({ form }: Props) => {
const insecureTls = form.watch("insecureTls"); const insecureTls = form.watch("insecureTls");
const cacert = form.watch("cacert"); const cacert = form.watch("cacert");
const uploadEnabled = form.watch("uploadLimit.enabled"); const uploadLimitEnabled = form.watch("uploadLimit.enabled");
const downloadEnabled = form.watch("downloadLimit.enabled"); const downloadLimitEnabled = form.watch("downloadLimit.enabled");
return ( return (
<Collapsible> <Collapsible>
<CollapsibleTrigger className="">Advanced Settings</CollapsibleTrigger> <CollapsibleTrigger className="">Advanced Settings</CollapsibleTrigger>
<CollapsibleContent className="pb-4 space-y-4"> <CollapsibleContent className="pb-4 space-y-4">
{/* Bandwidth Limit Controls */} <div className="space-y-4 mt-4">
<div className="space-y-6 rounded-lg border bg-card p-6"> <div className="grid gap-6">
<div className="flex items-center gap-3">
<div>
<h3 className="text-base font-semibold">Bandwidth Limits</h3>
<p className="text-sm text-muted-foreground">
Control upload and download speeds to prevent saturating network bandwidth
</p>
</div>
</div>
<div className="grid gap-6 sm:grid-cols-2">
{/* Upload Limit */}
<div className="space-y-4 rounded-lg border bg-background/50 p-4"> <div className="space-y-4 rounded-lg border bg-background/50 p-4">
<FormField <FormField
control={form.control} control={form.control}
@ -57,155 +40,149 @@ export const AdvancedForm = ({ form }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> <FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl> <FormControl>
<Checkbox <Checkbox checked={field.value ?? false} onCheckedChange={field.onChange} />
checked={field.value ?? false}
onCheckedChange={field.onChange}
/>
</FormControl> </FormControl>
<div className="space-y-1 leading-none"> <div className="space-y-1">
<FormLabel>Enable upload speed limit</FormLabel> <FormLabel>Enable upload speed limit</FormLabel>
<FormDescription className="text-xs"> <FormDescription className="text-xs">Limit upload speed to the repository</FormDescription>
Limit upload speed to the repository
</FormDescription>
</div> </div>
</FormItem> </FormItem>
)} )}
/> />
<div className="flex items-center gap-2">
{uploadEnabled && ( <FormField
<div className="space-y-3 pt-2"> control={form.control}
<div className="flex items-center gap-2"> name="uploadLimit.value"
<FormField render={({ field }) => (
control={form.control} <FormItem className="flex-1">
name="uploadLimit.value" <FormControl>
render={({ field }) => ( <div className="relative">
<FormItem className="flex-1"> <Input
<FormControl> disabled={!uploadLimitEnabled}
<div className="relative"> type="number"
<Input min={1}
type="number" step={1}
placeholder="10" max={999999}
min="0" placeholder="10"
step="0.1" className="pr-12"
className="pr-12" {...field}
{...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10) || 1)}
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)} />
/> <div className="absolute inset-y-0 right-0 flex items-center pr-3">
<div className="absolute inset-y-0 right-0 flex items-center pr-3"> <div className="h-4 w-px bg-border" />
<div className="h-4 w-px bg-border" /> </div>
</div>
</div>
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </div>
)} </FormControl>
/> </FormItem>
<FormField )}
control={form.control} />
name="uploadLimit.unit" <FormField
render={({ field }) => ( control={form.control}
<FormItem className="w-24"> name="uploadLimit.unit"
<Select onValueChange={field.onChange} defaultValue={field.value || "Mbps"} value={field.value || "Mbps"}> render={({ field }) => (
<FormControl> <FormItem className="w-24">
<SelectTrigger className="text-xs"> <Select
<SelectValue /> onValueChange={field.onChange}
</SelectTrigger> defaultValue={field.value || "Mbps"}
</FormControl> value={field.value || "Mbps"}
<SelectContent> disabled={!uploadLimitEnabled}
{Object.keys(BANDWIDTH_UNITS).map((unit) => ( >
<SelectItem key={unit} value={unit} className="text-xs"> <FormControl>
{unit} <SelectTrigger className="text-xs">
</SelectItem> <SelectValue />
))} </SelectTrigger>
</SelectContent> </FormControl>
</Select> <SelectContent>
<FormMessage /> {Object.keys(BANDWIDTH_UNITS).map((unit) => (
</FormItem> <SelectItem key={unit} value={unit} className="text-xs">
)} {unit}
/> </SelectItem>
</div> ))}
</div> </SelectContent>
)} </Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div> </div>
</div>
{/* Download Limit */} <div className="rounded-lg border bg-background/50 p-4">
<div className="space-y-4 rounded-lg border bg-background/50 p-4"> <FormField
control={form.control}
<FormField name="downloadLimit.enabled"
control={form.control} render={({ field }) => (
name="downloadLimit.enabled" <FormItem className="flex flex-row items-start space-x-3 space-y-0">
render={({ field }) => ( <FormControl>
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> <Checkbox checked={field.value ?? false} onCheckedChange={field.onChange} />
<FormControl> </FormControl>
<Checkbox <div className="space-y-1">
checked={field.value ?? false} <FormLabel>Enable download speed limit</FormLabel>
onCheckedChange={field.onChange} <FormDescription className="text-xs">Limit download speed from the repository</FormDescription>
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Enable download speed limit</FormLabel>
<FormDescription className="text-xs">
Limit download speed from the repository
</FormDescription>
</div>
</FormItem>
)}
/>
{downloadEnabled && (
<div className="space-y-3 pt-2">
<div className="flex items-center gap-2">
<FormField
control={form.control}
name="downloadLimit.value"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<div className="relative">
<Input
type="number"
placeholder="10"
min="0"
step="0.1"
className="pr-12"
{...field}
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<div className="h-4 w-px bg-border" />
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="downloadLimit.unit"
render={({ field }) => (
<FormItem className="w-24">
<Select onValueChange={field.onChange} defaultValue={field.value || "Mbps"} value={field.value || "Mbps"}>
<FormControl>
<SelectTrigger className="text-xs">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(BANDWIDTH_UNITS).map((unit) => (
<SelectItem key={unit} value={unit} className="text-xs">
{unit}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</div> </FormItem>
)} )}
/>
<div className="space-y-3 pt-2">
<div className="flex items-center gap-2">
<FormField
control={form.control}
name="downloadLimit.value"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<div className="relative">
<Input
placeholder="10"
type="number"
min={1}
step={1}
max={999999}
disabled={!downloadLimitEnabled}
className="pr-12"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || 1)}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<div className="h-4 w-px bg-border" />
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="downloadLimit.unit"
render={({ field }) => (
<FormItem className="w-24">
<Select
onValueChange={field.onChange}
defaultValue={field.value || "Mbps"}
value={field.value || "Mbps"}
>
<FormControl>
<SelectTrigger className="text-xs">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(BANDWIDTH_UNITS).map((unit) => (
<SelectItem key={unit} value={unit} className="text-xs">
{unit}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
ALTER TABLE `repositories_table` ADD `upload_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint ALTER TABLE `repositories_table` ADD `upload_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE `repositories_table` ADD `upload_limit_value` integer DEFAULT 0 NOT NULL;--> statement-breakpoint ALTER TABLE `repositories_table` ADD `upload_limit_value` real DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `repositories_table` ADD `upload_limit_unit` text DEFAULT 'MB/s' NOT NULL;--> statement-breakpoint ALTER TABLE `repositories_table` ADD `upload_limit_unit` text DEFAULT 'Mbps' NOT NULL;--> statement-breakpoint
ALTER TABLE `repositories_table` ADD `download_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint ALTER TABLE `repositories_table` ADD `download_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE `repositories_table` ADD `download_limit_value` integer DEFAULT 0 NOT NULL;--> statement-breakpoint ALTER TABLE `repositories_table` ADD `download_limit_value` real DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `repositories_table` ADD `download_limit_unit` text DEFAULT 'MB/s' NOT NULL; ALTER TABLE `repositories_table` ADD `download_limit_unit` text DEFAULT 'Mbps' NOT NULL;

File diff suppressed because it is too large Load diff

View file

@ -1,237 +1,244 @@
{ {
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"entries": [ "entries": [
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1755765658194, "when": 1755765658194,
"tag": "0000_known_madelyne_pryor", "tag": "0000_known_madelyne_pryor",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "6", "version": "6",
"when": 1755775437391, "when": 1755775437391,
"tag": "0001_far_frank_castle", "tag": "0001_far_frank_castle",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "6", "version": "6",
"when": 1756930554198, "when": 1756930554198,
"tag": "0002_cheerful_randall", "tag": "0002_cheerful_randall",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "6", "version": "6",
"when": 1758653407064, "when": 1758653407064,
"tag": "0003_mature_hellcat", "tag": "0003_mature_hellcat",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 4, "idx": 4,
"version": "6", "version": "6",
"when": 1758961535488, "when": 1758961535488,
"tag": "0004_wealthy_tomas", "tag": "0004_wealthy_tomas",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 5, "idx": 5,
"version": "6", "version": "6",
"when": 1759416698274, "when": 1759416698274,
"tag": "0005_simple_alice", "tag": "0005_simple_alice",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 6, "idx": 6,
"version": "6", "version": "6",
"when": 1760734377440, "when": 1760734377440,
"tag": "0006_secret_micromacro", "tag": "0006_secret_micromacro",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 7, "idx": 7,
"version": "6", "version": "6",
"when": 1761224911352, "when": 1761224911352,
"tag": "0007_watery_sersi", "tag": "0007_watery_sersi",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 8, "idx": 8,
"version": "6", "version": "6",
"when": 1761414054481, "when": 1761414054481,
"tag": "0008_silent_lady_bullseye", "tag": "0008_silent_lady_bullseye",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 9, "idx": 9,
"version": "6", "version": "6",
"when": 1762095226041, "when": 1762095226041,
"tag": "0009_little_adam_warlock", "tag": "0009_little_adam_warlock",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 10, "idx": 10,
"version": "6", "version": "6",
"when": 1762610065889, "when": 1762610065889,
"tag": "0010_perfect_proemial_gods", "tag": "0010_perfect_proemial_gods",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 11, "idx": 11,
"version": "6", "version": "6",
"when": 1763644043601, "when": 1763644043601,
"tag": "0011_familiar_stone_men", "tag": "0011_familiar_stone_men",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 12, "idx": 12,
"version": "6", "version": "6",
"when": 1764100562084, "when": 1764100562084,
"tag": "0012_add_short_ids", "tag": "0012_add_short_ids",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 13, "idx": 13,
"version": "6", "version": "6",
"when": 1764182159797, "when": 1764182159797,
"tag": "0013_elite_sprite", "tag": "0013_elite_sprite",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 14, "idx": 14,
"version": "6", "version": "6",
"when": 1764182405089, "when": 1764182405089,
"tag": "0014_wild_echo", "tag": "0014_wild_echo",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 15, "idx": 15,
"version": "6", "version": "6",
"when": 1764182465287, "when": 1764182465287,
"tag": "0015_jazzy_sersi", "tag": "0015_jazzy_sersi",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 16, "idx": 16,
"version": "6", "version": "6",
"when": 1764194697035, "when": 1764194697035,
"tag": "0016_fix-timestamps-to-ms", "tag": "0016_fix-timestamps-to-ms",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 17, "idx": 17,
"version": "6", "version": "6",
"when": 1764357897219, "when": 1764357897219,
"tag": "0017_fix-compression-modes", "tag": "0017_fix-compression-modes",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 18, "idx": 18,
"version": "6", "version": "6",
"when": 1764794371040, "when": 1764794371040,
"tag": "0018_breezy_invaders", "tag": "0018_breezy_invaders",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 19, "idx": 19,
"version": "6", "version": "6",
"when": 1764839917446, "when": 1764839917446,
"tag": "0019_secret_nomad", "tag": "0019_secret_nomad",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 20, "idx": 20,
"version": "6", "version": "6",
"when": 1764847918249, "when": 1764847918249,
"tag": "0020_even_dexter_bennett", "tag": "0020_even_dexter_bennett",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 21, "idx": 21,
"version": "6", "version": "6",
"when": 1765307881092, "when": 1765307881092,
"tag": "0021_steady_viper", "tag": "0021_steady_viper",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 22, "idx": 22,
"version": "6", "version": "6",
"when": 1765794552191, "when": 1765794552191,
"tag": "0022_woozy_shen", "tag": "0022_woozy_shen",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 23, "idx": 23,
"version": "6", "version": "6",
"when": 1766320570509, "when": 1766320570509,
"tag": "0023_special_thor", "tag": "0023_special_thor",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 24, "idx": 24,
"version": "6", "version": "6",
"when": 1766325504548, "when": 1766325504548,
"tag": "0024_schedules-one-fs", "tag": "0024_schedules-one-fs",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 25, "idx": 25,
"version": "6", "version": "6",
"when": 1766431021321, "when": 1766431021321,
"tag": "0025_remarkable_pete_wisdom", "tag": "0025_remarkable_pete_wisdom",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 26, "idx": 26,
"version": "6", "version": "6",
"when": 1766765013108, "when": 1766765013108,
"tag": "0026_migrate-local-repo-paths", "tag": "0026_migrate-local-repo-paths",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 27, "idx": 27,
"version": "6", "version": "6",
"when": 1766778073418, "when": 1766778073418,
"tag": "0027_careful_cammi", "tag": "0027_careful_cammi",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 28, "idx": 28,
"version": "6", "version": "6",
"when": 1766778162985, "when": 1766778162985,
"tag": "0028_third_amazoness", "tag": "0028_third_amazoness",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 29, "idx": 29,
"version": "6", "version": "6",
"when": 1767819883495, "when": 1767819883495,
"tag": "0029_boring_luke_cage", "tag": "0029_boring_luke_cage",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 30, "idx": 30,
"version": "6", "version": "6",
"when": 1767821088612, "when": 1767821088612,
"tag": "0030_lower-trim-username", "tag": "0030_lower-trim-username",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 31, "idx": 31,
"version": "6", "version": "6",
"when": 1767863951955, "when": 1767863951955,
"tag": "0031_graceful_squadron_supreme", "tag": "0031_graceful_squadron_supreme",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 32, "idx": 32,
"version": "6", "version": "6",
"when": 1768136755563, "when": 1768136755563,
"tag": "0032_lowercase-email", "tag": "0032_lowercase-email",
"breakpoints": true "breakpoints": true
} },
] {
"idx": 33,
"version": "6",
"when": 1768490085610,
"tag": "0033_curved_warhawk",
"breakpoints": true
}
]
} }

View file

@ -14,17 +14,16 @@ export const REPOSITORY_BACKENDS = {
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
export const BANDWIDTH_UNITS = { export const BANDWIDTH_UNITS = {
"Kbps": "Kbps", Kbps: "Kbps",
"Mbps": "Mbps", Mbps: "Mbps",
"Gbps": "Gbps", Gbps: "Gbps",
} as const; } as const;
export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS;
// Bandwidth limit configuration
export const bandwidthLimitSchema = type({ export const bandwidthLimitSchema = type({
enabled: "boolean = false", enabled: "boolean = false",
value: "number >= 0 = 0", value: "number > 0 = 1",
unit: type.valueOf(BANDWIDTH_UNITS).default("Mbps"), unit: type.valueOf(BANDWIDTH_UNITS).default("Mbps"),
}); });

View file

@ -1,6 +1,12 @@
import { relations, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { index, int, integer, sqliteTable, text, real, primaryKey, unique } from "drizzle-orm/sqlite-core"; import { index, int, integer, sqliteTable, text, real, primaryKey, unique } from "drizzle-orm/sqlite-core";
import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus, BandwidthUnit } from "~/schemas/restic"; import type {
CompressionMode,
RepositoryBackend,
repositoryConfigSchema,
RepositoryStatus,
BandwidthUnit,
} from "~/schemas/restic";
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
@ -159,7 +165,6 @@ export const repositoriesTable = sqliteTable("repositories_table", {
status: text().$type<RepositoryStatus>().default("unknown"), status: text().$type<RepositoryStatus>().default("unknown"),
lastChecked: int("last_checked", { mode: "number" }), lastChecked: int("last_checked", { mode: "number" }),
lastError: text("last_error"), lastError: text("last_error"),
// Bandwidth limit fields
uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false), uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false),
uploadLimitValue: real("upload_limit_value").notNull().default(0), uploadLimitValue: real("upload_limit_value").notNull().default(0),
uploadLimitUnit: text("upload_limit_unit").$type<BandwidthUnit>().notNull().default("Mbps"), uploadLimitUnit: text("upload_limit_unit").$type<BandwidthUnit>().notNull().default("Mbps"),

View file

@ -846,30 +846,13 @@ const copy = async (
args.push("latest"); args.push("latest");
} }
// Apply common args without automatic bandwidth limit injection to prevent duplication
addCommonArgs(args, env, destConfig, { skipBandwidth: true }); addCommonArgs(args, env, destConfig, { skipBandwidth: true });
// Manually handle bandwidth limits with correct copy semantics:
// --limit-download uses sourceConfig.downloadLimit (limiting downloads from source repo)
// --limit-upload uses destConfig.uploadLimit (limiting uploads to destination repo)
const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit);
const destUploadLimit = formatBandwidthLimit(destConfig.uploadLimit); const destUploadLimit = formatBandwidthLimit(destConfig.uploadLimit);
if (sourceConfig.backend === "rclone") { if (sourceDownloadLimit) {
// For rclone source backends, use rclone.from.bwlimit with source download limit only args.push("--limit-download", sourceDownloadLimit);
let effectiveSourceLimit = "";
if (sourceDownloadLimit) {
effectiveSourceLimit = sourceDownloadLimit;
}
if (effectiveSourceLimit) {
args.push("-o", `rclone.from.bwlimit=${effectiveSourceLimit}`);
}
} else {
// For restic source backends, apply download limit from source
if (sourceDownloadLimit) {
args.push("--limit-download", sourceDownloadLimit);
}
} }
// Apply upload limit to destination for all backends // Apply upload limit to destination for all backends
@ -899,13 +882,11 @@ const copy = async (
}; };
}; };
// Helper function to convert bandwidth limit to restic/rclone format
const formatBandwidthLimit = (limit?: BandwidthLimit): string => { const formatBandwidthLimit = (limit?: BandwidthLimit): string => {
if (!limit || !limit.enabled || limit.value <= 0) { if (!limit || !limit.enabled || limit.value <= 0) {
return ""; return "";
} }
// Convert to KiB/s for restic compatibility, or use suffixed format for rclone
let kibibytesPerSecond: number; let kibibytesPerSecond: number;
switch (limit.unit) { switch (limit.unit) {
case "Kbps": case "Kbps":
@ -924,11 +905,15 @@ const formatBandwidthLimit = (limit?: BandwidthLimit): string => {
return ""; return "";
} }
// Return as integer KiB/s for restic
return `${Math.floor(kibibytesPerSecond)}`; return `${Math.floor(kibibytesPerSecond)}`;
}; };
export const addCommonArgs = (args: string[], env: Record<string, string>, config?: RepositoryConfig, options?: { skipBandwidth?: boolean }) => { export const addCommonArgs = (
args: string[],
env: Record<string, string>,
config?: RepositoryConfig,
options?: { skipBandwidth?: boolean },
) => {
args.push("--json"); args.push("--json");
if (env._SFTP_SSH_ARGS) { if (env._SFTP_SSH_ARGS) {
@ -943,37 +928,15 @@ export const addCommonArgs = (args: string[], env: Record<string, string>, confi
args.push("--cacert", env.RESTIC_CACERT); args.push("--cacert", env.RESTIC_CACERT);
} }
// Add bandwidth limits if configuration is provided and not skipped
if (config && !options?.skipBandwidth) { if (config && !options?.skipBandwidth) {
if (config.backend === "rclone") { const uploadLimit = formatBandwidthLimit(config.uploadLimit);
// For rclone backends, consolidate both upload and download limits into one bwlimit if (uploadLimit) {
const uploadLimit = formatBandwidthLimit(config.uploadLimit); args.push("--limit-upload", uploadLimit);
const downloadLimit = formatBandwidthLimit(config.downloadLimit); }
// Determine effective limit (choose more restrictive when both exist) const downloadLimit = formatBandwidthLimit(config.downloadLimit);
let effectiveLimit = ""; if (downloadLimit) {
if (uploadLimit && downloadLimit) { args.push("--limit-download", downloadLimit);
effectiveLimit = parseInt(uploadLimit, 10) < parseInt(downloadLimit, 10) ? uploadLimit : downloadLimit;
} else if (uploadLimit) {
effectiveLimit = uploadLimit;
} else if (downloadLimit) {
effectiveLimit = downloadLimit;
}
if (effectiveLimit) {
args.push("-o", `rclone.bwlimit=${effectiveLimit}`);
}
} else {
// For restic backends, handle upload and download limits separately
const uploadLimit = formatBandwidthLimit(config.uploadLimit);
if (uploadLimit) {
args.push("--limit-upload", uploadLimit);
}
const downloadLimit = formatBandwidthLimit(config.downloadLimit);
if (downloadLimit) {
args.push("--limit-download", downloadLimit);
}
} }
} }
}; };
@ -995,26 +958,17 @@ export const restic = {
copy, copy,
}; };
// Helper function to clean up temporary files
export const cleanupTemporaryKeys = async (env: Record<string, string>) => { export const cleanupTemporaryKeys = async (env: Record<string, string>) => {
const keysToClean = ["_SFTP_KEY_PATH", "_SFTP_KNOWN_HOSTS_PATH", "RESTIC_CACERT", "GOOGLE_APPLICATION_CREDENTIALS"]; const keysToClean = ["_SFTP_KEY_PATH", "_SFTP_KNOWN_HOSTS_PATH", "RESTIC_CACERT", "GOOGLE_APPLICATION_CREDENTIALS"];
for (const key of keysToClean) { for (const key of keysToClean) {
if (env[key]) { if (env[key]) {
try { await fs.unlink(env[key]).catch(() => {});
await fs.unlink(env[key]);
} catch (_error) {
// Ignore errors when cleaning up temporary files
}
} }
} }
// Clean up custom password files // Clean up custom password files
if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) {
try { await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {});
await fs.unlink(env.RESTIC_PASSWORD_FILE);
} catch (_error) {
// Ignore errors when cleaning up temporary files
}
} }
}; };