create-nexgen 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -0
- package/src/index.js +108 -0
- package/template/.dockerignore +14 -0
- package/template/.env +58 -0
- package/template/.env.example +59 -0
- package/template/.prettierignore +5 -0
- package/template/.prettierrc +8 -0
- package/template/README.md +447 -0
- package/template/drizzle.config.ts +29 -0
- package/template/eslint.config.js +52 -0
- package/template/gitignore-stub +24 -0
- package/template/package.json +96 -0
- package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
- package/template/public/assets/Button-_7aQ7gHL.js +1 -0
- package/template/public/assets/Input-CLNJXmKc.css +1 -0
- package/template/public/assets/Input-z8GI8Aqo.js +1 -0
- package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
- package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
- package/template/public/assets/Layout-DotR1sQC.js +1 -0
- package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
- package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
- package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
- package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- package/template/public/assets/dashboard-CwybEyLc.js +1 -0
- package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
- package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
- package/template/public/assets/index-Bleyx5dm.js +64 -0
- package/template/public/assets/index-DUw8E6Yg.css +1 -0
- package/template/public/assets/login-DC7PTlQF.js +1 -0
- package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
- package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
- package/template/public/assets/register-3O7Qs28C.js +1 -0
- package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
- package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
- package/template/public/index.html +17 -0
- package/template/src/database/migrations/mysql/0000_init.sql +73 -0
- package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
- package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
- package/template/src/database/schema.ts +4 -0
- package/template/src/env.ts +107 -0
- package/template/src/framework/cache/cache.ts +81 -0
- package/template/src/framework/database/connection.ts +168 -0
- package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
- package/template/src/framework/database/paginate.ts +200 -0
- package/template/src/framework/database/schema.ts +26 -0
- package/template/src/framework/database/seed.ts +33 -0
- package/template/src/framework/events/dispatcher.ts +57 -0
- package/template/src/framework/facade.ts +27 -0
- package/template/src/framework/http/app.ts +61 -0
- package/template/src/framework/http/cors.ts +19 -0
- package/template/src/framework/http/logger.ts +85 -0
- package/template/src/framework/http/openapi.ts +34 -0
- package/template/src/framework/http/ratelimiter.ts +13 -0
- package/template/src/framework/http/router.ts +76 -0
- package/template/src/framework/http/static.ts +33 -0
- package/template/src/framework/http/validation.ts +24 -0
- package/template/src/framework/kernel.ts +40 -0
- package/template/src/framework/maker-cli/src/index.mjs +51 -0
- package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
- package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
- package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
- package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
- package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
- package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
- package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
- package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
- package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
- package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
- package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
- package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
- package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
- package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
- package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
- package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
- package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
- package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
- package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
- package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
- package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
- package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
- package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
- package/template/src/framework/modules/discover.ts +54 -0
- package/template/src/framework/modules/routes.ts +26 -0
- package/template/src/framework/notification/index.ts +109 -0
- package/template/src/framework/queue/clear.ts +20 -0
- package/template/src/framework/queue/queue.ts +213 -0
- package/template/src/framework/queue/ui.ts +104 -0
- package/template/src/framework/queue/worker.ts +33 -0
- package/template/src/framework/realtime/broadcast.ts +27 -0
- package/template/src/framework/realtime/index.ts +1 -0
- package/template/src/framework/realtime/socket-cookie.ts +65 -0
- package/template/src/framework/realtime/socket.ts +132 -0
- package/template/src/framework/realtime/types.ts +6 -0
- package/template/src/framework/realtime/ui.ts +16 -0
- package/template/src/framework/redis/client.ts +126 -0
- package/template/src/framework/scheduler/lock.ts +124 -0
- package/template/src/framework/scheduler/run.ts +26 -0
- package/template/src/framework/scheduler/scheduler.ts +82 -0
- package/template/src/framework/server.ts +147 -0
- package/template/src/framework/session/session.ts +116 -0
- package/template/src/framework/storage/storage.ts +743 -0
- package/template/src/framework/support/cookie.ts +78 -0
- package/template/src/framework/support/jwt.ts +45 -0
- package/template/src/framework/support/lifecycle.ts +35 -0
- package/template/src/framework/support/logger.ts +102 -0
- package/template/src/framework/support/mail.ts +43 -0
- package/template/src/framework/support/password.ts +23 -0
- package/template/src/framework/support/url.ts +25 -0
- package/template/src/middlewares/auth-middleware.ts +98 -0
- package/template/src/middlewares/role-middleware.ts +24 -0
- package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
- package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
- package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
- package/template/src/modules/auth/controllers/role.controller.ts +25 -0
- package/template/src/modules/auth/database/models/notifications.ts +22 -0
- package/template/src/modules/auth/database/models/role.ts +14 -0
- package/template/src/modules/auth/database/models/user.ts +46 -0
- package/template/src/modules/auth/database/seeders/role.ts +19 -0
- package/template/src/modules/auth/database/seeders/user.ts +33 -0
- package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
- package/template/src/modules/auth/jobs/registeruser.ts +31 -0
- package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/template/src/modules/auth/routes/api.ts +151 -0
- package/template/src/modules/auth/routes/role.ts +39 -0
- package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
- package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
- package/template/src/modules/welcome/database/models/welcome.ts +6 -0
- package/template/src/modules/welcome/routes/api.ts +20 -0
- package/template/src/resources/index.html +16 -0
- package/template/src/resources/src/App.vue +5 -0
- package/template/src/resources/src/assets/css/styles.css +14934 -0
- package/template/src/resources/src/assets/css/styles.css.map +1 -0
- package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
- package/template/src/resources/src/assets/images/logo-1.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
- package/template/src/resources/src/assets/images/logo-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo1.png +0 -0
- package/template/src/resources/src/assets/images/logo2.png +0 -0
- package/template/src/resources/src/assets/scss/custom.css +217 -0
- package/template/src/resources/src/assets/scss/custom.css.map +1 -0
- package/template/src/resources/src/assets/scss/custom.scss +1100 -0
- package/template/src/resources/src/components/Button.vue +35 -0
- package/template/src/resources/src/components/Checkbox.vue +29 -0
- package/template/src/resources/src/components/FloatButton.vue +36 -0
- package/template/src/resources/src/components/Href.vue +32 -0
- package/template/src/resources/src/components/Input.vue +227 -0
- package/template/src/resources/src/components/InputGroup.vue +153 -0
- package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
- package/template/src/resources/src/components/Modal.vue +102 -0
- package/template/src/resources/src/components/Pagebar.vue +28 -0
- package/template/src/resources/src/components/Refresh.vue +26 -0
- package/template/src/resources/src/components/Select.vue +390 -0
- package/template/src/resources/src/components/Spinner.vue +42 -0
- package/template/src/resources/src/components/Switch.vue +65 -0
- package/template/src/resources/src/components/TextArea.vue +121 -0
- package/template/src/resources/src/components/Toast.vue +56 -0
- package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
- package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
- package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
- package/template/src/resources/src/components/datatable/index.vue +237 -0
- package/template/src/resources/src/composables/useAuth.ts +52 -0
- package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
- package/template/src/resources/src/composables/useDialog.ts +5 -0
- package/template/src/resources/src/composables/useGum.ts +3 -0
- package/template/src/resources/src/composables/usePulse.ts +5 -0
- package/template/src/resources/src/env.d.ts +20 -0
- package/template/src/resources/src/helpers/nformatter.ts +10 -0
- package/template/src/resources/src/helpers/utils.ts +68 -0
- package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
- package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/template/src/resources/src/layouts/Layout/index.vue +76 -0
- package/template/src/resources/src/main.ts +27 -0
- package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/template/src/resources/src/pages/auth/login.vue +93 -0
- package/template/src/resources/src/pages/auth/register.vue +130 -0
- package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
- package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
- package/template/src/resources/src/pages/dashboard/index.vue +76 -0
- package/template/src/resources/src/plugins/axios.ts +33 -0
- package/template/src/resources/src/plugins/browserDetect.ts +55 -0
- package/template/src/resources/src/plugins/dialog.ts +167 -0
- package/template/src/resources/src/plugins/gum.ts +343 -0
- package/template/src/resources/src/plugins/pulse.ts +141 -0
- package/template/src/resources/src/plugins/routeProgress.ts +87 -0
- package/template/src/resources/src/router/index.ts +85 -0
- package/template/src/resources/src/stores/admin-ui.ts +148 -0
- package/template/src/resources/src/stores/auth.ts +151 -0
- package/template/src/resources/tsconfig.json +19 -0
- package/template/src/resources/vite.config.ts +43 -0
- package/template/src/storage/logs/app.log +20179 -0
- package/template/src/storage/logs/fatal.log +727 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { env } from "@/env.js";
|
|
6
|
+
import {
|
|
7
|
+
S3Client,
|
|
8
|
+
GetObjectCommand,
|
|
9
|
+
PutObjectCommand,
|
|
10
|
+
DeleteObjectCommand,
|
|
11
|
+
HeadObjectCommand,
|
|
12
|
+
CopyObjectCommand,
|
|
13
|
+
ListObjectsV2Command,
|
|
14
|
+
DeleteObjectsCommand,
|
|
15
|
+
GetObjectAclCommand
|
|
16
|
+
} from "@aws-sdk/client-s3";
|
|
17
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
18
|
+
|
|
19
|
+
type Disk = "public" | "private" | "tmp";
|
|
20
|
+
type FileData = string | Buffer | Uint8Array | ArrayBuffer | Blob | File;
|
|
21
|
+
type Visibility = "public" | "private";
|
|
22
|
+
type StorageDriver = "local" | "s3";
|
|
23
|
+
|
|
24
|
+
const root = path.resolve(process.cwd(), "src/storage/app");
|
|
25
|
+
const disks: Record<Disk, string> = {
|
|
26
|
+
public: path.join(root, "public"),
|
|
27
|
+
private: path.join(root, "private"),
|
|
28
|
+
tmp: path.join(root, "tmp")
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const driver: StorageDriver = env.STORAGE_DRIVER;
|
|
32
|
+
const defaultDisk: Disk = env.STORAGE_DISK;
|
|
33
|
+
const s3 =
|
|
34
|
+
driver === "s3"
|
|
35
|
+
? new S3Client({
|
|
36
|
+
region: env.STORAGE_REGION,
|
|
37
|
+
endpoint: env.STORAGE_ENDPOINT,
|
|
38
|
+
forcePathStyle: env.STORAGE_FORCE_PATH_STYLE,
|
|
39
|
+
credentials:
|
|
40
|
+
env.STORAGE_ACCESS_KEY_ID && env.STORAGE_SECRET_ACCESS_KEY
|
|
41
|
+
? {
|
|
42
|
+
accessKeyId: env.STORAGE_ACCESS_KEY_ID,
|
|
43
|
+
secretAccessKey: env.STORAGE_SECRET_ACCESS_KEY
|
|
44
|
+
}
|
|
45
|
+
: undefined
|
|
46
|
+
})
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Why: Enforces normalized and safe relative storage paths.
|
|
51
|
+
* When: Any public API receives a file or directory key.
|
|
52
|
+
* Where: Internal storage key/path helpers.
|
|
53
|
+
* How: Converts separators, trims leading slashes, and blocks traversal.
|
|
54
|
+
*/
|
|
55
|
+
function clean(input: string) {
|
|
56
|
+
const value = input.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
57
|
+
if (!value || value.includes("..")) throw new Error("Invalid storage path");
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Why: Keeps per-disk namespace explicit for object-key generation.
|
|
63
|
+
* When: Building storage keys for local/S3 operations.
|
|
64
|
+
* Where: Internal key composition helpers.
|
|
65
|
+
* How: Returns the disk name as current prefix strategy.
|
|
66
|
+
*/
|
|
67
|
+
function diskPrefix(disk: Disk) {
|
|
68
|
+
return disk;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Why: Produces canonical object keys shared across storage drivers.
|
|
73
|
+
* When: S3 operations need full object key paths.
|
|
74
|
+
* Where: Internal storage addressing helpers.
|
|
75
|
+
* How: Prefixes sanitized file path with disk namespace.
|
|
76
|
+
*/
|
|
77
|
+
function objectKey(disk: Disk, file: string) {
|
|
78
|
+
return `${diskPrefix(disk)}/${clean(file)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Why: Converts heterogeneous file payloads to local-write friendly buffers.
|
|
83
|
+
* When: Local driver persists bytes to filesystem.
|
|
84
|
+
* Where: Internal write pipeline.
|
|
85
|
+
* How: Normalizes supported input types to `Buffer`.
|
|
86
|
+
*/
|
|
87
|
+
async function toBuffer(data: FileData) {
|
|
88
|
+
if (typeof data === "string") return Buffer.from(data);
|
|
89
|
+
if (Buffer.isBuffer(data)) return data;
|
|
90
|
+
if (data instanceof Uint8Array) return Buffer.from(data);
|
|
91
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
92
|
+
if (data instanceof Blob) return Buffer.from(await data.arrayBuffer());
|
|
93
|
+
return Buffer.from([]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Why: Converts payloads to body types accepted by S3 PutObject.
|
|
98
|
+
* When: S3 driver uploads new file content.
|
|
99
|
+
* Where: Internal S3 write path.
|
|
100
|
+
* How: Preserves native body types where possible, buffers otherwise.
|
|
101
|
+
*/
|
|
102
|
+
async function toBody(data: FileData) {
|
|
103
|
+
if (typeof data === "string" || Buffer.isBuffer(data) || data instanceof Uint8Array) return data;
|
|
104
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
105
|
+
if (data instanceof Blob) return Buffer.from(await data.arrayBuffer());
|
|
106
|
+
return Buffer.from([]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Why: Resolves normalized absolute filesystem paths for local storage.
|
|
111
|
+
* When: Local driver reads/writes/removes files or directories.
|
|
112
|
+
* Where: Internal local adapter helpers.
|
|
113
|
+
* How: Joins disk root with sanitized relative file path.
|
|
114
|
+
*/
|
|
115
|
+
function abs(disk: Disk, file: string) {
|
|
116
|
+
return path.join(disks[disk], clean(file));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Why: Writes content using the active storage driver.
|
|
121
|
+
* When: Any upload/write API stores a file.
|
|
122
|
+
* Where: Core storage mutation helpers.
|
|
123
|
+
* How: Uploads object to S3 or writes bytes to local disk path.
|
|
124
|
+
*/
|
|
125
|
+
async function put(disk: Disk, file: string, data: FileData) {
|
|
126
|
+
if (driver === "s3") {
|
|
127
|
+
await s3?.send(
|
|
128
|
+
new PutObjectCommand({
|
|
129
|
+
Bucket: env.STORAGE_BUCKET,
|
|
130
|
+
Key: objectKey(disk, file),
|
|
131
|
+
Body: await toBody(data),
|
|
132
|
+
ACL: disk === "public" ? "public-read" : "private"
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
return clean(file);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const filePath = abs(disk, file);
|
|
139
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
140
|
+
await fs.writeFile(filePath, await toBuffer(data));
|
|
141
|
+
return clean(file);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Why: Stores browser-style File inputs with deterministic naming.
|
|
146
|
+
* When: Multipart uploads are written into storage.
|
|
147
|
+
* Where: Upload helper path.
|
|
148
|
+
* How: Uses provided name or generates unique timestamp+uuid filename.
|
|
149
|
+
*/
|
|
150
|
+
async function putFile(disk: Disk, directory: string, file: File, name?: string) {
|
|
151
|
+
const finalName = name || `${Date.now()}-${crypto.randomUUID()}-${file.name}`;
|
|
152
|
+
return await put(disk, path.posix.join(clean(directory), finalName), file);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Why: Persists generated artifacts in temporary disk space.
|
|
157
|
+
* When: Server prepares export/download content before client fetch.
|
|
158
|
+
* Where: Generated-download lifecycle.
|
|
159
|
+
* How: Sanitizes prefix/extension and writes uniquely named tmp file.
|
|
160
|
+
*/
|
|
161
|
+
async function writeGeneratedTemp(prefix: string, extension: string, data: FileData) {
|
|
162
|
+
const safePrefix = clean(prefix).replace(/\//g, "-");
|
|
163
|
+
const safeExtension = extension.replace(/[^a-zA-Z0-9]/g, "").toLowerCase() || "tmp";
|
|
164
|
+
const fileName = `${Date.now()}-${crypto.randomUUID()}-${safePrefix}.${safeExtension}`;
|
|
165
|
+
const file = path.posix.join("generated", fileName);
|
|
166
|
+
await put("tmp", file, data);
|
|
167
|
+
return file;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Why: Supports temporary generated file handoff with auto-cleanup.
|
|
172
|
+
* When: Download flow consumes generated artifact once.
|
|
173
|
+
* Where: Temp storage lifecycle helpers.
|
|
174
|
+
* How: Reads object/file bytes then removes original temp entry.
|
|
175
|
+
*/
|
|
176
|
+
async function consumeGeneratedTemp(file: string) {
|
|
177
|
+
const target = clean(file);
|
|
178
|
+
if (driver === "s3") {
|
|
179
|
+
const output = await s3?.send(
|
|
180
|
+
new GetObjectCommand({
|
|
181
|
+
Bucket: env.STORAGE_BUCKET,
|
|
182
|
+
Key: objectKey("tmp", target)
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
const body = output?.Body
|
|
186
|
+
? Buffer.from(await output.Body.transformToByteArray())
|
|
187
|
+
: Buffer.from([]);
|
|
188
|
+
await s3?.send(
|
|
189
|
+
new DeleteObjectCommand({
|
|
190
|
+
Bucket: env.STORAGE_BUCKET,
|
|
191
|
+
Key: objectKey("tmp", target)
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
return body;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const content = await fs.readFile(abs("tmp", target));
|
|
198
|
+
await fs.rm(abs("tmp", target), { force: true });
|
|
199
|
+
return content;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Why: Checks whether a file currently exists on selected disk.
|
|
204
|
+
* When: Flows need precondition checks before read/move/delete.
|
|
205
|
+
* Where: Storage existence helpers.
|
|
206
|
+
* How: Uses S3 head-object probe or local filesystem access.
|
|
207
|
+
*/
|
|
208
|
+
async function exists(disk: Disk, file: string) {
|
|
209
|
+
if (driver === "s3") {
|
|
210
|
+
try {
|
|
211
|
+
await s3?.send(
|
|
212
|
+
new HeadObjectCommand({ Bucket: env.STORAGE_BUCKET, Key: objectKey(disk, file) })
|
|
213
|
+
);
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await fs.access(abs(disk, file));
|
|
222
|
+
return true;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Why: Deletes a single file/object from storage.
|
|
230
|
+
* When: Features remove uploads or cleanup generated files.
|
|
231
|
+
* Where: Storage mutation helpers.
|
|
232
|
+
* How: Calls S3 delete-object or local `fs.rm` with force.
|
|
233
|
+
*/
|
|
234
|
+
async function remove(disk: Disk, file: string) {
|
|
235
|
+
if (driver === "s3") {
|
|
236
|
+
await s3?.send(
|
|
237
|
+
new DeleteObjectCommand({ Bucket: env.STORAGE_BUCKET, Key: objectKey(disk, file) })
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await fs.rm(abs(disk, file), { force: true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Why: Reads full file content from storage into memory.
|
|
247
|
+
* When: Controllers/services need raw bytes for processing/response.
|
|
248
|
+
* Where: Storage read helpers.
|
|
249
|
+
* How: Downloads S3 object bytes or reads local file buffer.
|
|
250
|
+
*/
|
|
251
|
+
async function read(disk: Disk, file: string) {
|
|
252
|
+
if (driver === "s3") {
|
|
253
|
+
const output = await s3?.send(
|
|
254
|
+
new GetObjectCommand({ Bucket: env.STORAGE_BUCKET, Key: objectKey(disk, file) })
|
|
255
|
+
);
|
|
256
|
+
return output?.Body ? Buffer.from(await output.Body.transformToByteArray()) : Buffer.from([]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return fs.readFile(abs(disk, file));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Why: Duplicates a file within the same disk namespace.
|
|
264
|
+
* When: Workflows need cloned artifacts without deleting source.
|
|
265
|
+
* Where: Storage copy helpers.
|
|
266
|
+
* How: Uses S3 copy-object or local copyFile with parent mkdir.
|
|
267
|
+
*/
|
|
268
|
+
async function copyFile(disk: Disk, from: string, to: string) {
|
|
269
|
+
if (driver === "s3") {
|
|
270
|
+
await s3?.send(
|
|
271
|
+
new CopyObjectCommand({
|
|
272
|
+
Bucket: env.STORAGE_BUCKET,
|
|
273
|
+
CopySource: `${env.STORAGE_BUCKET}/${objectKey(disk, from)}`,
|
|
274
|
+
Key: objectKey(disk, to)
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
return clean(to);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await fs.mkdir(path.dirname(abs(disk, to)), { recursive: true });
|
|
281
|
+
await fs.copyFile(abs(disk, from), abs(disk, to));
|
|
282
|
+
return clean(to);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Why: Moves a file to a new location within a disk.
|
|
287
|
+
* When: Renaming or relocating stored artifacts.
|
|
288
|
+
* Where: Storage move helpers.
|
|
289
|
+
* How: Copy+delete on S3, rename on local filesystem.
|
|
290
|
+
*/
|
|
291
|
+
async function moveFile(disk: Disk, from: string, to: string) {
|
|
292
|
+
if (driver === "s3") {
|
|
293
|
+
await copyFile(disk, from, to);
|
|
294
|
+
await remove(disk, from);
|
|
295
|
+
return clean(to);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await fs.mkdir(path.dirname(abs(disk, to)), { recursive: true });
|
|
299
|
+
await fs.rename(abs(disk, from), abs(disk, to));
|
|
300
|
+
return clean(to);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Why: Returns portable metadata required by file-management features.
|
|
305
|
+
* When: API/UI requests file size, mime, or modification timestamp.
|
|
306
|
+
* Where: Storage metadata helpers.
|
|
307
|
+
* How: Reads object headers on S3 or local file stats.
|
|
308
|
+
*/
|
|
309
|
+
async function metadata(disk: Disk, file: string) {
|
|
310
|
+
if (driver === "s3") {
|
|
311
|
+
const head = await s3?.send(
|
|
312
|
+
new HeadObjectCommand({ Bucket: env.STORAGE_BUCKET, Key: objectKey(disk, file) })
|
|
313
|
+
);
|
|
314
|
+
return {
|
|
315
|
+
size: Number(head?.ContentLength || 0),
|
|
316
|
+
mimeType: head?.ContentType || "application/octet-stream",
|
|
317
|
+
lastModified: head?.LastModified ? head.LastModified.getTime() : 0
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const stat = await fs.stat(abs(disk, file));
|
|
322
|
+
return {
|
|
323
|
+
size: stat.size,
|
|
324
|
+
mimeType: "application/octet-stream",
|
|
325
|
+
lastModified: stat.mtimeMs
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Why: Provides short-lived access links for storage objects.
|
|
331
|
+
* When: Clients need direct download access without proxying bytes.
|
|
332
|
+
* Where: Storage facade URL utilities.
|
|
333
|
+
* How: Signs S3 URL with TTL or maps local file to `/storage/*` route.
|
|
334
|
+
*/
|
|
335
|
+
async function temporaryUrl(disk: Disk, file: string, ttl = env.STORAGE_SIGNED_URL_TTL_SECONDS) {
|
|
336
|
+
if (driver === "s3") {
|
|
337
|
+
return getSignedUrl(
|
|
338
|
+
s3 as S3Client,
|
|
339
|
+
new GetObjectCommand({
|
|
340
|
+
Bucket: env.STORAGE_BUCKET,
|
|
341
|
+
Key: objectKey(disk, file)
|
|
342
|
+
}),
|
|
343
|
+
{ expiresIn: ttl }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return `/storage/${clean(file)}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Why: Lists flat file entries in a disk directory.
|
|
352
|
+
* When: Features need browse/select behavior for stored files.
|
|
353
|
+
* Where: Storage listing helpers.
|
|
354
|
+
* How: Uses S3 object prefix scan or local directory read.
|
|
355
|
+
*/
|
|
356
|
+
async function listFiles(disk: Disk, directory = "") {
|
|
357
|
+
const dir = clean(directory || "root").replace(/^root$/, "");
|
|
358
|
+
if (driver === "s3") {
|
|
359
|
+
const out = await s3?.send(
|
|
360
|
+
new ListObjectsV2Command({
|
|
361
|
+
Bucket: env.STORAGE_BUCKET,
|
|
362
|
+
Prefix: `${diskPrefix(disk)}/${dir}`.replace(/\/$/, "")
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
return (out?.Contents || [])
|
|
367
|
+
.map((item) => String(item.Key || ""))
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.map((key) => key.replace(`${diskPrefix(disk)}/`, ""));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const base = abs(disk, dir || ".");
|
|
373
|
+
const entries = await fs.readdir(base, { withFileTypes: true }).catch(() => [] as any[]);
|
|
374
|
+
return entries
|
|
375
|
+
.filter((entry) => entry.isFile())
|
|
376
|
+
.map((entry) => (dir ? `${dir}/` : "") + entry.name);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Why: Exposes readable streams from either local or S3 backends.
|
|
381
|
+
* When: Responses or processors need streamed file access.
|
|
382
|
+
* Where: Storage streaming helpers.
|
|
383
|
+
* How: Adapts S3 web stream to Node Readable or wraps local bytes.
|
|
384
|
+
*/
|
|
385
|
+
async function readStream(disk: Disk, file: string) {
|
|
386
|
+
if (driver === "s3") {
|
|
387
|
+
const output = await s3?.send(
|
|
388
|
+
new GetObjectCommand({ Bucket: env.STORAGE_BUCKET, Key: objectKey(disk, file) })
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const body = output?.Body;
|
|
392
|
+
|
|
393
|
+
if (!body) return Readable.from([]);
|
|
394
|
+
|
|
395
|
+
if ("transformToWebStream" in body && typeof body.transformToWebStream === "function") {
|
|
396
|
+
const webStream = body.transformToWebStream();
|
|
397
|
+
return Readable.fromWeb(webStream as Parameters<typeof Readable.fromWeb>[0]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if ("transformToByteArray" in body && typeof body.transformToByteArray === "function") {
|
|
401
|
+
return Readable.from(Buffer.from(await body.transformToByteArray()));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (body instanceof Readable) return body;
|
|
405
|
+
|
|
406
|
+
return Readable.from([]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return Readable.from(await fs.readFile(abs(disk, file)));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Why: Updates object visibility for S3-backed buckets.
|
|
414
|
+
* When: Business rules toggle between private and public access.
|
|
415
|
+
* Where: Storage ACL helpers.
|
|
416
|
+
* How: Re-copies object to itself with updated ACL metadata.
|
|
417
|
+
*/
|
|
418
|
+
async function setVisibility(disk: Disk, file: string, visibility: Visibility) {
|
|
419
|
+
if (driver !== "s3") return clean(file);
|
|
420
|
+
|
|
421
|
+
await s3?.send(
|
|
422
|
+
new CopyObjectCommand({
|
|
423
|
+
Bucket: env.STORAGE_BUCKET,
|
|
424
|
+
CopySource: `${env.STORAGE_BUCKET}/${objectKey(disk, file)}`,
|
|
425
|
+
Key: objectKey(disk, file),
|
|
426
|
+
ACL: visibility === "public" ? "public-read" : "private",
|
|
427
|
+
MetadataDirective: "COPY"
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
return clean(file);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Why: Reports effective file visibility in a driver-agnostic way.
|
|
436
|
+
* When: Features need to inspect current public/private access level.
|
|
437
|
+
* Where: Storage ACL query helpers.
|
|
438
|
+
* How: Infers local visibility by disk, or inspects S3 grants.
|
|
439
|
+
*/
|
|
440
|
+
async function getVisibility(disk: Disk, file: string): Promise<Visibility> {
|
|
441
|
+
if (driver !== "s3") {
|
|
442
|
+
return disk === "public" ? "public" : "private";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const acl = await s3?.send(
|
|
446
|
+
new GetObjectAclCommand({
|
|
447
|
+
Bucket: env.STORAGE_BUCKET,
|
|
448
|
+
Key: objectKey(disk, file)
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const isPublic = (acl?.Grants || []).some(
|
|
453
|
+
(grant) =>
|
|
454
|
+
grant.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" &&
|
|
455
|
+
grant.Permission === "READ"
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
return isPublic ? "public" : "private";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Why: Ensures a directory path exists in the selected disk.
|
|
463
|
+
* When: Pre-creating folder structure for grouped file writes.
|
|
464
|
+
* Where: Storage directory helpers.
|
|
465
|
+
* How: Creates S3 folder marker object or local recursive directory.
|
|
466
|
+
*/
|
|
467
|
+
async function makeDirectory(disk: Disk, directory: string) {
|
|
468
|
+
const dir = clean(directory);
|
|
469
|
+
if (driver === "s3") {
|
|
470
|
+
await s3?.send(
|
|
471
|
+
new PutObjectCommand({
|
|
472
|
+
Bucket: env.STORAGE_BUCKET,
|
|
473
|
+
Key: `${diskPrefix(disk)}/${dir}/`,
|
|
474
|
+
Body: ""
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
return dir;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
await fs.mkdir(abs(disk, dir), { recursive: true });
|
|
481
|
+
return dir;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Why: Removes a directory and its stored contents.
|
|
486
|
+
* When: Cleanup flows purge a folder tree.
|
|
487
|
+
* Where: Storage directory mutation helpers.
|
|
488
|
+
* How: Lists and batch-deletes S3 objects or recursively removes local dir.
|
|
489
|
+
*/
|
|
490
|
+
async function deleteDirectory(disk: Disk, directory: string) {
|
|
491
|
+
const dir = clean(directory);
|
|
492
|
+
if (driver === "s3") {
|
|
493
|
+
const prefix = `${diskPrefix(disk)}/${dir}/`;
|
|
494
|
+
const listed = await s3?.send(
|
|
495
|
+
new ListObjectsV2Command({
|
|
496
|
+
Bucket: env.STORAGE_BUCKET,
|
|
497
|
+
Prefix: prefix
|
|
498
|
+
})
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const objects = (listed?.Contents || [])
|
|
502
|
+
.map((item) => item.Key)
|
|
503
|
+
.filter((key): key is string => Boolean(key))
|
|
504
|
+
.map((Key) => ({ Key }));
|
|
505
|
+
|
|
506
|
+
if (objects.length > 0) {
|
|
507
|
+
await s3?.send(
|
|
508
|
+
new DeleteObjectsCommand({
|
|
509
|
+
Bucket: env.STORAGE_BUCKET,
|
|
510
|
+
Delete: { Objects: objects }
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await fs.rm(abs(disk, dir), { recursive: true, force: true });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Why: Lists immediate child directories for a disk path.
|
|
523
|
+
* When: UI/services need folder navigation data.
|
|
524
|
+
* Where: Storage directory listing helpers.
|
|
525
|
+
* How: Uses S3 common-prefix listing or local dirent filtering.
|
|
526
|
+
*/
|
|
527
|
+
async function listDirectories(disk: Disk, directory = "") {
|
|
528
|
+
const dir = directory.trim() ? clean(directory) : "";
|
|
529
|
+
if (driver === "s3") {
|
|
530
|
+
const prefix = `${diskPrefix(disk)}/${dir}`.replace(/\/$/, "") + (dir ? "/" : "");
|
|
531
|
+
const out = await s3?.send(
|
|
532
|
+
new ListObjectsV2Command({
|
|
533
|
+
Bucket: env.STORAGE_BUCKET,
|
|
534
|
+
Prefix: prefix,
|
|
535
|
+
Delimiter: "/"
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
return (out?.CommonPrefixes || [])
|
|
540
|
+
.map((item) => String(item.Prefix || ""))
|
|
541
|
+
.filter(Boolean)
|
|
542
|
+
.map((value) => value.replace(`${diskPrefix(disk)}/`, "").replace(/\/$/, ""));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const base = abs(disk, dir || ".");
|
|
546
|
+
const entries = await fs.readdir(base, { withFileTypes: true }).catch(() => [] as any[]);
|
|
547
|
+
return entries
|
|
548
|
+
.filter((entry) => entry.isDirectory())
|
|
549
|
+
.map((entry) => (dir ? `${dir}/` : "") + entry.name);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Why: Stores readable stream data through unified storage API.
|
|
554
|
+
* When: Upstream producers emit stream output instead of full buffers.
|
|
555
|
+
* Where: Storage stream write helpers.
|
|
556
|
+
* How: Buffers incoming chunks and delegates final write to `put`.
|
|
557
|
+
*/
|
|
558
|
+
async function writeStream(disk: Disk, file: string, stream: Readable) {
|
|
559
|
+
const chunks: Buffer[] = [];
|
|
560
|
+
for await (const chunk of stream) {
|
|
561
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
await put(disk, file, Buffer.concat(chunks));
|
|
565
|
+
return clean(file);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Why: Unified storage facade for local and S3-backed file operations.
|
|
570
|
+
* When: Features need upload/read/move/delete/download or signed URLs.
|
|
571
|
+
* Where: Controllers/jobs and module business logic.
|
|
572
|
+
* How: Routes calls to disk-specific adapters while preserving one API.
|
|
573
|
+
*/
|
|
574
|
+
export const storage = {
|
|
575
|
+
/**
|
|
576
|
+
* Why: Prepares local disk roots required by the active storage facade.
|
|
577
|
+
* When: Kernel bootstraps framework services.
|
|
578
|
+
* Where: Runtime startup path.
|
|
579
|
+
* How: Creates configured disk directories only for the local driver.
|
|
580
|
+
*/
|
|
581
|
+
async init() {
|
|
582
|
+
if (driver === "local") {
|
|
583
|
+
await Promise.all(Object.values(disks).map((disk) => fs.mkdir(disk, { recursive: true })));
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
driver,
|
|
588
|
+
|
|
589
|
+
defaultDisk,
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Why: Exposes a disk-scoped API without duplicating storage logic.
|
|
593
|
+
* When: Features need explicit `public`, `private`, or `tmp` targeting.
|
|
594
|
+
* Where: Controllers, jobs, and other application services.
|
|
595
|
+
* How: Returns thin wrappers around internal helpers bound to one disk.
|
|
596
|
+
*/
|
|
597
|
+
disk(disk: Disk) {
|
|
598
|
+
return {
|
|
599
|
+
put: (file: string, data: FileData) => put(disk, file, data),
|
|
600
|
+
putFile: (directory: string, file: File, name?: string) => putFile(disk, directory, file, name),
|
|
601
|
+
get: (file: string) => read(disk, file),
|
|
602
|
+
delete: (file: string) => remove(disk, file),
|
|
603
|
+
copy: (from: string, to: string) => copyFile(disk, from, to),
|
|
604
|
+
move: (from: string, to: string) => moveFile(disk, from, to),
|
|
605
|
+
exists: (file: string) => exists(disk, file),
|
|
606
|
+
missing: async (file: string) => !(await exists(disk, file)),
|
|
607
|
+
size: async (file: string) => (await metadata(disk, file)).size,
|
|
608
|
+
mimeType: async (file: string) => (await metadata(disk, file)).mimeType,
|
|
609
|
+
lastModified: async (file: string) => (await metadata(disk, file)).lastModified,
|
|
610
|
+
files: (directory = "") => listFiles(disk, directory),
|
|
611
|
+
directories: (directory = "") => listDirectories(disk, directory),
|
|
612
|
+
makeDirectory: (directory: string) => makeDirectory(disk, directory),
|
|
613
|
+
deleteDirectory: (directory: string) => deleteDirectory(disk, directory),
|
|
614
|
+
readStream: (file: string) => readStream(disk, file),
|
|
615
|
+
writeStream: (file: string, stream: Readable) => writeStream(disk, file, stream),
|
|
616
|
+
temporaryUrl: (file: string, ttl?: number) => temporaryUrl(disk, file, ttl),
|
|
617
|
+
setVisibility: (file: string, visibility: Visibility) => setVisibility(disk, file, visibility),
|
|
618
|
+
getVisibility: (file: string) => getVisibility(disk, file),
|
|
619
|
+
path: (file: string) => abs(disk, file),
|
|
620
|
+
url: (file: string) =>
|
|
621
|
+
driver === "s3"
|
|
622
|
+
? `${env.STORAGE_ENDPOINT || "https://s3.amazonaws.com"}/${env.STORAGE_BUCKET}/${objectKey(disk, file)}`
|
|
623
|
+
: `/storage/${clean(file)}`
|
|
624
|
+
};
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
url(file: string) {
|
|
628
|
+
return storage.disk(defaultDisk).url(file);
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
put(file: string, data: FileData) {
|
|
632
|
+
return storage.disk(defaultDisk).put(file, data);
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
putFile(directory: string, file: File, name?: string) {
|
|
636
|
+
return storage.disk(defaultDisk).putFile(directory, file, name);
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
get(file: string) {
|
|
640
|
+
return storage.disk(defaultDisk).get(file);
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
delete(file: string) {
|
|
644
|
+
return storage.disk(defaultDisk).delete(file);
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
copy(from: string, to: string) {
|
|
648
|
+
return storage.disk(defaultDisk).copy(from, to);
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
move(from: string, to: string) {
|
|
652
|
+
return storage.disk(defaultDisk).move(from, to);
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
exists(file: string) {
|
|
656
|
+
return storage.disk(defaultDisk).exists(file);
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
missing(file: string) {
|
|
660
|
+
return storage.disk(defaultDisk).missing(file);
|
|
661
|
+
},
|
|
662
|
+
|
|
663
|
+
files(directory = "") {
|
|
664
|
+
return storage.disk(defaultDisk).files(directory);
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
directories(directory = "") {
|
|
668
|
+
return storage.disk(defaultDisk).directories(directory);
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
makeDirectory(directory: string) {
|
|
672
|
+
return storage.disk(defaultDisk).makeDirectory(directory);
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
deleteDirectory(directory: string) {
|
|
676
|
+
return storage.disk(defaultDisk).deleteDirectory(directory);
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
size(file: string) {
|
|
680
|
+
return storage.disk(defaultDisk).size(file);
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
mime(file: string) {
|
|
684
|
+
return storage.disk(defaultDisk).mimeType(file);
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
mimeType(file: string) {
|
|
688
|
+
return storage.disk(defaultDisk).mimeType(file);
|
|
689
|
+
},
|
|
690
|
+
|
|
691
|
+
lastModified(file: string) {
|
|
692
|
+
return storage.disk(defaultDisk).lastModified(file);
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
readStream(file: string) {
|
|
696
|
+
return storage.disk(defaultDisk).readStream(file);
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
writeStream(file: string, stream: Readable) {
|
|
700
|
+
return storage.disk(defaultDisk).writeStream(file, stream);
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
temporaryUrl(file: string, ttl?: number) {
|
|
704
|
+
return storage.disk(defaultDisk).temporaryUrl(file, ttl);
|
|
705
|
+
},
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Why: Builds a download response from the default disk in one call.
|
|
709
|
+
* When: Endpoints need attachment delivery semantics.
|
|
710
|
+
* Where: Controller-level file download flows.
|
|
711
|
+
* How: Reads file bytes and sets content-type/content-disposition headers.
|
|
712
|
+
*/
|
|
713
|
+
async download(_c: any, file: string, filename?: string) {
|
|
714
|
+
const body = await storage.get(file);
|
|
715
|
+
return new Response(body, {
|
|
716
|
+
status: 200,
|
|
717
|
+
headers: {
|
|
718
|
+
"content-type": await storage.mime(file),
|
|
719
|
+
"content-disposition": `attachment; filename=${filename || path.posix.basename(clean(file))}`
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Why: Creates ephemeral generated files for deferred download flows.
|
|
726
|
+
* When: API produces files (CSV/PDF/etc.) before the client fetches them.
|
|
727
|
+
* Where: Example and business export pipelines.
|
|
728
|
+
* How: Writes into tmp storage using a sanitized prefix and extension.
|
|
729
|
+
*/
|
|
730
|
+
async generateForDownload(options: { prefix: string; extension: string; data: FileData; }) {
|
|
731
|
+
return await writeGeneratedTemp(options.prefix, options.extension, options.data);
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Why: Provides read-once semantics for generated temporary artifacts.
|
|
736
|
+
* When: Client consumes a previously generated download token/path.
|
|
737
|
+
* Where: Download handoff endpoints.
|
|
738
|
+
* How: Reads tmp file content and deletes the underlying object afterwards.
|
|
739
|
+
*/
|
|
740
|
+
async consumeGenerated(file: string) {
|
|
741
|
+
return await consumeGeneratedTemp(file);
|
|
742
|
+
}
|
|
743
|
+
};
|