|
| 1 | +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import type { SettingsManifest } from '@objectstack/spec/system'; |
| 4 | +import type { SettingsActionHandler } from '../settings-service.types.js'; |
| 5 | + |
| 6 | +// Mirrors the shape of `mail.manifest.ts`. The actual adapter rebuild |
| 7 | +// + `storage/test` probe live in `@objectstack/service-storage`; this |
| 8 | +// manifest only declares the form + acts as a safe fallback when the |
| 9 | +// storage plugin is not present. |
| 10 | +const manifest = { |
| 11 | + namespace: 'storage', |
| 12 | + version: 1, |
| 13 | + label: 'File Storage', |
| 14 | + icon: 'HardDrive', |
| 15 | + description: |
| 16 | + 'Backend used for attachments, exports, and user uploads. ' + |
| 17 | + '⚠ Switching adapter does not migrate existing files — files ' + |
| 18 | + 'uploaded under the previous adapter become unreachable through ' + |
| 19 | + 'the new one.', |
| 20 | + scope: 'global', |
| 21 | + readPermission: 'setup.access', |
| 22 | + writePermission: 'setup.write', |
| 23 | + category: 'Infrastructure', |
| 24 | + order: 20, |
| 25 | + specifiers: [ |
| 26 | + { type: 'group', id: 'adapter', label: 'Backend', required: false, |
| 27 | + description: 'Choose where uploaded files are stored.' }, |
| 28 | + { type: 'select', key: 'adapter', label: 'Adapter', required: true, default: 'local', |
| 29 | + options: [ |
| 30 | + { value: 'local', label: 'Local filesystem' }, |
| 31 | + { value: 's3', label: 'S3 / S3-compatible' }, |
| 32 | + ], |
| 33 | + }, |
| 34 | + |
| 35 | + { type: 'group', id: 'local', label: 'Local', required: false, |
| 36 | + visible: "${data.adapter === 'local'}" }, |
| 37 | + { type: 'text', key: 'local_root', label: 'Root directory', required: false, |
| 38 | + default: './.objectstack/data/uploads', |
| 39 | + description: 'Filesystem path under which files are stored. Relative paths resolve from the server CWD.', |
| 40 | + visible: "${data.adapter === 'local'}" }, |
| 41 | + |
| 42 | + { type: 'group', id: 's3', label: 'S3', required: false, |
| 43 | + visible: "${data.adapter === 's3'}" }, |
| 44 | + { type: 'text', key: 's3_bucket', label: 'Bucket', required: true, |
| 45 | + description: 'Shared host bucket. Per-project files are namespaced via the projects/<projectId>/ prefix.', |
| 46 | + visible: "${data.adapter === 's3'}" }, |
| 47 | + { type: 'text', key: 's3_region', label: 'Region', required: true, |
| 48 | + description: 'Example: us-east-1', |
| 49 | + visible: "${data.adapter === 's3'}" }, |
| 50 | + { type: 'text', key: 's3_endpoint', label: 'Endpoint', required: false, |
| 51 | + description: 'Custom endpoint for S3-compatible providers (R2, MinIO, Wasabi). Leave blank for AWS S3.', |
| 52 | + visible: "${data.adapter === 's3'}" }, |
| 53 | + { type: 'text', key: 's3_access_key_id', label: 'Access key ID', required: true, |
| 54 | + visible: "${data.adapter === 's3'}" }, |
| 55 | + { type: 'password', key: 's3_secret_access_key', label: 'Secret access key', |
| 56 | + required: true, encrypted: true, |
| 57 | + visible: "${data.adapter === 's3'}" }, |
| 58 | + { type: 'toggle', key: 's3_force_path_style', label: 'Force path-style URLs', |
| 59 | + required: false, default: false, |
| 60 | + description: 'Enable for MinIO and most S3-compatible providers; disable for AWS S3.', |
| 61 | + visible: "${data.adapter === 's3'}" }, |
| 62 | + |
| 63 | + { type: 'group', id: 'limits', label: 'Limits', required: false }, |
| 64 | + { type: 'number', key: 'presigned_ttl', label: 'Presigned URL TTL (seconds)', |
| 65 | + required: false, default: 3600, min: 60, max: 604800 }, |
| 66 | + { type: 'number', key: 'session_ttl', label: 'Upload session TTL (seconds)', |
| 67 | + required: false, default: 86400, min: 300, max: 604800, |
| 68 | + description: 'How long a chunked-upload session stays resumable.' }, |
| 69 | + { type: 'number', key: 'max_upload_mb', label: 'Max upload size (MB)', |
| 70 | + required: false, default: 100, min: 1, max: 10240 }, |
| 71 | + |
| 72 | + { type: 'action_button', id: 'test', label: 'Test connection', |
| 73 | + required: false, icon: 'Plug', |
| 74 | + handler: { kind: 'http', method: 'POST', url: '/api/settings/storage/test' } }, |
| 75 | + ], |
| 76 | +}; |
| 77 | + |
| 78 | +/** File Storage — local FS / S3-compatible backend configuration. */ |
| 79 | +export const storageSettingsManifest = manifest as unknown as SettingsManifest; |
| 80 | + |
| 81 | +/** |
| 82 | + * Built-in fallback action handler for `storage/test`. The real |
| 83 | + * implementation lives in `@objectstack/service-storage` and is |
| 84 | + * registered by `StorageServicePlugin` on `kernel:ready` (it overrides |
| 85 | + * this stub via `registerAction`). This fallback only validates the |
| 86 | + * form so the button is still useful when the storage plugin is |
| 87 | + * absent (e.g. in a unit-test kernel that mounts settings only). |
| 88 | + */ |
| 89 | +export const storageTestActionHandler: SettingsActionHandler = async ({ values }) => { |
| 90 | + const adapter = String(values.adapter ?? 'local'); |
| 91 | + if (adapter === 'local') { |
| 92 | + const root = values.local_root as string | undefined; |
| 93 | + if (!root) { |
| 94 | + return { ok: false, severity: 'error', message: 'Configure a root directory before testing.' }; |
| 95 | + } |
| 96 | + return { |
| 97 | + ok: true, |
| 98 | + severity: 'info', |
| 99 | + message: `Local adapter configured (root=${root}). Mount @objectstack/service-storage to exercise live I/O.`, |
| 100 | + }; |
| 101 | + } |
| 102 | + if (adapter === 's3') { |
| 103 | + const missing: string[] = []; |
| 104 | + if (!values.s3_bucket) missing.push('s3_bucket'); |
| 105 | + if (!values.s3_region) missing.push('s3_region'); |
| 106 | + if (!values.s3_access_key_id) missing.push('s3_access_key_id'); |
| 107 | + if (!values.s3_secret_access_key) missing.push('s3_secret_access_key'); |
| 108 | + if (missing.length) { |
| 109 | + return { ok: false, severity: 'error', message: `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}` }; |
| 110 | + } |
| 111 | + return { |
| 112 | + ok: true, |
| 113 | + severity: 'info', |
| 114 | + message: `S3 adapter configured (bucket=${values.s3_bucket}, region=${values.s3_region}). Mount @objectstack/service-storage to exercise live I/O.`, |
| 115 | + }; |
| 116 | + } |
| 117 | + return { ok: false, severity: 'error', message: `Unknown adapter: ${adapter}` }; |
| 118 | +}; |
0 commit comments