feat(repositories): add google cloud storage support

This commit is contained in:
Nicolas Meienberger
2025-11-10 21:04:08 +01:00
parent d31fa8d464
commit e98c0af8ca
6 changed files with 74 additions and 1 deletions

View File

@@ -30,6 +30,7 @@ type Props = {
const defaultValuesForType = { const defaultValuesForType = {
local: { backend: "local" as const, compressionMode: "auto" as const }, local: { backend: "local" as const, compressionMode: "auto" as const },
s3: { backend: "s3" as const, compressionMode: "auto" as const }, s3: { backend: "s3" as const, compressionMode: "auto" as const },
gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
}; };
export const CreateRepositoryForm = ({ export const CreateRepositoryForm = ({
@@ -100,6 +101,7 @@ export const CreateRepositoryForm = ({
<SelectContent> <SelectContent>
<SelectItem value="local">Local</SelectItem> <SelectItem value="local">Local</SelectItem>
<SelectItem value="s3">S3</SelectItem> <SelectItem value="s3">S3</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription>Choose the storage backend for this repository.</FormDescription> <FormDescription>Choose the storage backend for this repository.</FormDescription>
@@ -195,6 +197,53 @@ export const CreateRepositoryForm = ({
</> </>
)} )}
{watchedBackend === "gcs" && (
<>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="my-backup-bucket" {...field} />
</FormControl>
<FormDescription>GCS bucket name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="projectId"
render={({ field }) => (
<FormItem>
<FormLabel>Project ID</FormLabel>
<FormControl>
<Input placeholder="my-gcp-project-123" {...field} />
</FormControl>
<FormDescription>Google Cloud project ID.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="credentialsJson"
render={({ field }) => (
<FormItem>
<FormLabel>Service Account JSON</FormLabel>
<FormControl>
<Input type="password" placeholder="Paste service account JSON key..." {...field} />
</FormControl>
<FormDescription>Service account JSON credentials for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{mode === "update" && ( {mode === "update" && (
<Button type="submit" className="w-full" loading={loading}> <Button type="submit" className="w-full" loading={loading}>
Save Changes Save Changes

View File

@@ -12,6 +12,8 @@ export const RepositoryIcon = ({ backend, className = "h-4 w-4" }: Props) => {
return <HardDrive className={className} />; return <HardDrive className={className} />;
case "s3": case "s3":
return <Cloud className={className} />; return <Cloud className={className} />;
case "gcs":
return <Cloud className={className} />;
default: default:
return <Database className={className} />; return <Database className={className} />;
} }

View File

@@ -102,6 +102,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
<SelectItem value="local">Local</SelectItem> <SelectItem value="local">Local</SelectItem>
<SelectItem value="sftp">SFTP</SelectItem> <SelectItem value="sftp">SFTP</SelectItem>
<SelectItem value="s3">S3</SelectItem> <SelectItem value="s3">S3</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{(searchQuery || statusFilter || backendFilter) && ( {(searchQuery || statusFilter || backendFilter) && (

View File

@@ -22,6 +22,9 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId); encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey); encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
break; break;
case "gcs":
encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
break;
} }
return encryptedConfig as RepositoryConfig; return encryptedConfig as RepositoryConfig;

View File

@@ -74,6 +74,8 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
return `${REPOSITORY_BASE}/${config.name}`; return `${REPOSITORY_BASE}/${config.name}`;
case "s3": case "s3":
return `s3:${config.endpoint}/${config.bucket}`; return `s3:${config.endpoint}/${config.bucket}`;
case "gcs":
return `gs:${config.bucket}:/`;
default: { default: {
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`); throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
} }
@@ -91,6 +93,14 @@ const buildEnv = async (config: RepositoryConfig) => {
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId); env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey); env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
break; break;
case "gcs": {
const decryptedCredentials = await cryptoUtils.decrypt(config.credentialsJson);
const credentialsPath = path.join("/tmp", `gcs-credentials-${crypto.randomBytes(8).toString("hex")}.json`);
await fs.writeFile(credentialsPath, decryptedCredentials, { mode: 0o600 });
env.GOOGLE_PROJECT_ID = config.projectId;
env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
break;
}
} }
return env; return env;

View File

@@ -3,6 +3,7 @@ import { type } from "arktype";
export const REPOSITORY_BACKENDS = { export const REPOSITORY_BACKENDS = {
local: "local", local: "local",
s3: "s3", s3: "s3",
gcs: "gcs",
} as const; } as const;
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
@@ -20,7 +21,14 @@ export const localRepositoryConfigSchema = type({
name: "string", name: "string",
}); });
export const repositoryConfigSchema = s3RepositoryConfigSchema.or(localRepositoryConfigSchema); export const gcsRepositoryConfigSchema = type({
backend: "'gcs'",
bucket: "string",
projectId: "string",
credentialsJson: "string",
});
export const repositoryConfigSchema = s3RepositoryConfigSchema.or(localRepositoryConfigSchema).or(gcsRepositoryConfigSchema);
export type RepositoryConfig = typeof repositoryConfigSchema.infer; export type RepositoryConfig = typeof repositoryConfigSchema.infer;