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:
Forgotten
2026-02-20 10:31:56 -06:00
parent 32119f5c2f
commit fdd2ea6157
36 changed files with 1683 additions and 32 deletions

View File

@@ -14,3 +14,4 @@ export { llmCheck } from "./llm-check.js";
export { logCheck } from "./log-check.js";
export { portCheck } from "./port-check.js";
export { secretsCheck } from "./secrets-check.js";
export { storageCheck } from "./storage-check.js";

View File

@@ -0,0 +1,60 @@
import fs from "node:fs";
import type { PaperclipConfig } from "../config/schema.js";
import type { CheckResult } from "./index.js";
import { resolveRuntimeLikePath } from "./path-resolver.js";
export function storageCheck(config: PaperclipConfig, configPath?: string): CheckResult {
if (config.storage.provider === "local_disk") {
const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath);
if (!fs.existsSync(baseDir)) {
return {
name: "Storage",
status: "warn",
message: `Local storage directory does not exist: ${baseDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(baseDir, { recursive: true });
},
repairHint: "Run with --repair to create local storage directory",
};
}
try {
fs.accessSync(baseDir, fs.constants.W_OK);
return {
name: "Storage",
status: "pass",
message: `Local disk storage is writable: ${baseDir}`,
};
} catch {
return {
name: "Storage",
status: "fail",
message: `Local storage directory is not writable: ${baseDir}`,
canRepair: false,
repairHint: "Check file permissions for storage.localDisk.baseDir",
};
}
}
const bucket = config.storage.s3.bucket.trim();
const region = config.storage.s3.region.trim();
if (!bucket || !region) {
return {
name: "Storage",
status: "fail",
message: "S3 storage requires non-empty bucket and region",
canRepair: false,
repairHint: "Run `paperclip configure --section storage`",
};
}
return {
name: "Storage",
status: "warn",
message: `S3 storage configured (bucket=${bucket}, region=${region}). Reachability check is skipped in doctor.`,
canRepair: false,
repairHint: "Verify credentials and endpoint in deployment environment",
};
}