diff --git a/app/client/modules/repositories/components/repository-forms/s3-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/s3-repository-form.tsx index 40328c4a9..264d49064 100644 --- a/app/client/modules/repositories/components/repository-forms/s3-repository-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/s3-repository-form.tsx @@ -46,6 +46,22 @@ export const S3RepositoryForm = ({ form }: Props) => { )} /> + ( + + Storage class (optional) + + + + + Passed to restic as s3.storage-class. Use a class supported by your provider. + + + + )} + /> const config = repository.config as RepositoryConfig; const hasLocalPath = Boolean(effectiveLocalPath); + const hasStorageClass = Boolean(config.storageClass); const hasCaCert = Boolean(config.cacert); const hasInsecureTlsConfig = config.insecureTls !== undefined; @@ -95,6 +96,7 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) => {hasLocalPath && ( } label="Local Path" value={effectiveLocalPath!} mono /> )} + {hasStorageClass && } label="Storage Class" value={config.storageClass!} mono />} } label="Compression Mode" diff --git a/packages/core/src/restic/commands/__tests__/copy.test.ts b/packages/core/src/restic/commands/__tests__/copy.test.ts index 3ff19dcbb..3c03418d8 100644 --- a/packages/core/src/restic/commands/__tests__/copy.test.ts +++ b/packages/core/src/restic/commands/__tests__/copy.test.ts @@ -96,6 +96,31 @@ describe("copy command", () => { expect(getArgs()).not.toContain("--ignore-inode"); }); + test("passes storage class option for S3 repositories", async () => { + const { getArgs } = setup(); + + await Effect.runPromise( + copy( + sourceConfig, + { + backend: "s3", + endpoint: "https://s3.example.com", + bucket: "backups", + accessKeyId: "ak", + secretAccessKey: "sk", + storageClass: "STANDARD_IA", + isExistingRepository: true, + customPassword: "dest-password", + }, + { organizationId: "org-1", tag: "daily" }, + mockDeps, + ), + ); + + expect(getArgs()).toContain("-o"); + expect(getArgs()).toContain("s3.storage-class=STANDARD_IA"); + }); + test("passes multiple snapshot IDs after separator", async () => { const { getArgs } = setup(); diff --git a/packages/core/src/restic/helpers/add-common-args.ts b/packages/core/src/restic/helpers/add-common-args.ts index 5ed72177d..4e94d72b0 100644 --- a/packages/core/src/restic/helpers/add-common-args.ts +++ b/packages/core/src/restic/helpers/add-common-args.ts @@ -16,6 +16,10 @@ export const addCommonArgs = ( args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`); } + if (config?.backend === "s3" && config.storageClass) { + args.push("-o", `s3.storage-class=${config.storageClass}`); + } + if (env.AWS_S3_BUCKET_LOOKUP === "dns") { args.push("-o", "s3.bucket-lookup=dns"); } diff --git a/packages/core/src/restic/schemas.ts b/packages/core/src/restic/schemas.ts index c56fbf341..3a0c81fab 100644 --- a/packages/core/src/restic/schemas.ts +++ b/packages/core/src/restic/schemas.ts @@ -45,6 +45,7 @@ export const s3RepositoryConfigSchema = z backend: z.literal("s3"), endpoint: z.string().min(1), bucket: z.string().min(1), + storageClass: z.string().min(1).optional(), accessKeyId: z.string().min(1), secretAccessKey: z.string().min(1), })