feat: limit upload speed during backups

This commit is contained in:
Nicolas Meienberger
2025-11-22 11:03:57 +01:00
parent 043f73ea87
commit 8ccece42f0
11 changed files with 661 additions and 87 deletions

View File

@@ -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;

View File

@@ -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 }:
)}
/>
)}
<FormField
control={form.control}
name="limitUploadKbps"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Upload speed limit (KB/s)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
min={0}
placeholder="Unlimited"
onChange={(v) => field.onChange(v.target.value ? Number(v.target.value) : undefined)}
/>
</FormControl>
<FormDescription>
Limit upload bandwidth in kilobytes per second. Leave empty for unlimited speed.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
@@ -482,6 +507,12 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
{repositoriesData?.find((r) => r.id === formValues.repositoryId)?.name || "—"}
</p>
</div>
{formValues.limitUploadKbps && (
<div>
<p className="text-xs uppercase text-muted-foreground">Upload speed limit</p>
<p className="font-medium">{formValues.limitUploadKbps} KB/s</p>
</div>
)}
{formValues.includePatterns && formValues.includePatterns.length > 0 && (
<div>
<p className="text-xs uppercase text-muted-foreground">Include paths</p>

View File

@@ -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,
},
});
};

View File

@@ -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,
},
});
};