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

@@ -1,5 +1,5 @@
import { z } from "zod";
import { SECRET_PROVIDERS } from "./constants.js";
import { SECRET_PROVIDERS, STORAGE_PROVIDERS } from "./constants.js";
export const configMetaSchema = z.object({
version: z.literal(1),
@@ -29,6 +29,31 @@ export const serverConfigSchema = z.object({
serveUi: z.boolean().default(true),
});
export const storageLocalDiskConfigSchema = z.object({
baseDir: z.string().default("~/.paperclip/instances/default/data/storage"),
});
export const storageS3ConfigSchema = z.object({
bucket: z.string().min(1).default("paperclip"),
region: z.string().min(1).default("us-east-1"),
endpoint: z.string().optional(),
prefix: z.string().default(""),
forcePathStyle: z.boolean().default(false),
});
export const storageConfigSchema = z.object({
provider: z.enum(STORAGE_PROVIDERS).default("local_disk"),
localDisk: storageLocalDiskConfigSchema.default({
baseDir: "~/.paperclip/instances/default/data/storage",
}),
s3: storageS3ConfigSchema.default({
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
}),
});
export const secretsLocalEncryptedConfigSchema = z.object({
keyFilePath: z.string().default("~/.paperclip/instances/default/secrets/master.key"),
});
@@ -47,6 +72,18 @@ export const paperclipConfigSchema = z.object({
database: databaseConfigSchema,
logging: loggingConfigSchema,
server: serverConfigSchema,
storage: storageConfigSchema.default({
provider: "local_disk",
localDisk: {
baseDir: "~/.paperclip/instances/default/data/storage",
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
}),
secrets: secretsConfigSchema.default({
provider: "local_encrypted",
strictMode: false,
@@ -61,6 +98,9 @@ export type LlmConfig = z.infer<typeof llmConfigSchema>;
export type DatabaseConfig = z.infer<typeof databaseConfigSchema>;
export type LoggingConfig = z.infer<typeof loggingConfigSchema>;
export type ServerConfig = z.infer<typeof serverConfigSchema>;
export type StorageConfig = z.infer<typeof storageConfigSchema>;
export type StorageLocalDiskConfig = z.infer<typeof storageLocalDiskConfigSchema>;
export type StorageS3Config = z.infer<typeof storageS3ConfigSchema>;
export type SecretsConfig = z.infer<typeof secretsConfigSchema>;
export type SecretsLocalEncryptedConfig = z.infer<typeof secretsLocalEncryptedConfigSchema>;
export type ConfigMeta = z.infer<typeof configMetaSchema>;