feat: add storage system with local disk and S3 providers
Introduces a provider-agnostic storage subsystem for file attachments. Includes local disk and S3 backends, asset/attachment DB schemas, issue attachment CRUD routes with multer upload, CLI configure/doctor/env integration, and enriched issue ancestors with project/goal resolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
146
cli/src/prompts/storage.ts
Normal file
146
cli/src/prompts/storage.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import type { StorageConfig } from "../config/schema.js";
|
||||
import { resolveDefaultStorageDir, resolvePaperclipInstanceId } from "../config/home.js";
|
||||
|
||||
function defaultStorageBaseDir(): string {
|
||||
return resolveDefaultStorageDir(resolvePaperclipInstanceId());
|
||||
}
|
||||
|
||||
export function defaultStorageConfig(): StorageConfig {
|
||||
return {
|
||||
provider: "local_disk",
|
||||
localDisk: {
|
||||
baseDir: defaultStorageBaseDir(),
|
||||
},
|
||||
s3: {
|
||||
bucket: "paperclip",
|
||||
region: "us-east-1",
|
||||
endpoint: undefined,
|
||||
prefix: "",
|
||||
forcePathStyle: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function promptStorage(current?: StorageConfig): Promise<StorageConfig> {
|
||||
const base = current ?? defaultStorageConfig();
|
||||
|
||||
const provider = await p.select({
|
||||
message: "Storage provider",
|
||||
options: [
|
||||
{
|
||||
value: "local_disk" as const,
|
||||
label: "Local disk (recommended)",
|
||||
hint: "best for single-user local deployments",
|
||||
},
|
||||
{
|
||||
value: "s3" as const,
|
||||
label: "S3 compatible",
|
||||
hint: "for cloud/object storage backends",
|
||||
},
|
||||
],
|
||||
initialValue: base.provider,
|
||||
});
|
||||
|
||||
if (p.isCancel(provider)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (provider === "local_disk") {
|
||||
const baseDir = await p.text({
|
||||
message: "Local storage base directory",
|
||||
defaultValue: base.localDisk.baseDir || defaultStorageBaseDir(),
|
||||
placeholder: defaultStorageBaseDir(),
|
||||
validate: (value) => {
|
||||
if (!value || value.trim().length === 0) return "Storage base directory is required";
|
||||
},
|
||||
});
|
||||
|
||||
if (p.isCancel(baseDir)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return {
|
||||
provider: "local_disk",
|
||||
localDisk: {
|
||||
baseDir: baseDir.trim(),
|
||||
},
|
||||
s3: base.s3,
|
||||
};
|
||||
}
|
||||
|
||||
const bucket = await p.text({
|
||||
message: "S3 bucket",
|
||||
defaultValue: base.s3.bucket || "paperclip",
|
||||
placeholder: "paperclip",
|
||||
validate: (value) => {
|
||||
if (!value || value.trim().length === 0) return "Bucket is required";
|
||||
},
|
||||
});
|
||||
|
||||
if (p.isCancel(bucket)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const region = await p.text({
|
||||
message: "S3 region",
|
||||
defaultValue: base.s3.region || "us-east-1",
|
||||
placeholder: "us-east-1",
|
||||
validate: (value) => {
|
||||
if (!value || value.trim().length === 0) return "Region is required";
|
||||
},
|
||||
});
|
||||
|
||||
if (p.isCancel(region)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const endpoint = await p.text({
|
||||
message: "S3 endpoint (optional for compatible backends)",
|
||||
defaultValue: base.s3.endpoint ?? "",
|
||||
placeholder: "https://s3.amazonaws.com",
|
||||
});
|
||||
|
||||
if (p.isCancel(endpoint)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const prefix = await p.text({
|
||||
message: "Object key prefix (optional)",
|
||||
defaultValue: base.s3.prefix ?? "",
|
||||
placeholder: "paperclip/",
|
||||
});
|
||||
|
||||
if (p.isCancel(prefix)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const forcePathStyle = await p.confirm({
|
||||
message: "Use S3 path-style URLs?",
|
||||
initialValue: base.s3.forcePathStyle ?? false,
|
||||
});
|
||||
|
||||
if (p.isCancel(forcePathStyle)) {
|
||||
p.cancel("Setup cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return {
|
||||
provider: "s3",
|
||||
localDisk: base.localDisk,
|
||||
s3: {
|
||||
bucket: bucket.trim(),
|
||||
region: region.trim(),
|
||||
endpoint: endpoint.trim() || undefined,
|
||||
prefix: prefix.trim(),
|
||||
forcePathStyle,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user