@rebasepro/server-core 0.7.0 → 0.9.0
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/README.md +2 -2
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +68 -0
- package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
- package/dist/api/rest/query-parser.d.ts +6 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
- package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +55 -0
- package/dist/auth/cookie-utils.d.ts +32 -0
- package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
- package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +32 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +158 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +66 -0
- package/dist/auth/session-routes.d.ts +29 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +18 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/define-cron.d.ts +28 -0
- package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dynamic-import-Dvh-K5fl.js +22 -0
- package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +30 -0
- package/dist/email/templates.d.ts +50 -0
- package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
- package/dist/env.d.ts +102 -0
- package/dist/functions/define-function.d.ts +55 -0
- package/dist/functions/function-loader.d.ts +18 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.es.js +18184 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +48629 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +65 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +344 -0
- package/dist/jwt-BwIn8xmk.js +4200 -0
- package/dist/jwt-BwIn8xmk.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-DnYXB-Wd.js +162 -0
- package/dist/ms-DnYXB-Wd.js.map +1 -0
- package/dist/openapi-generator-Z9oYWLf_.js +586 -0
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
- package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
- package/dist/serve-spa.d.ts +36 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +51 -0
- package/dist/src-B4OLmNVa.js +437 -0
- package/dist/src-B4OLmNVa.js.map +1 -0
- package/dist/src-B5WkP1Rv.js +24778 -0
- package/dist/src-B5WkP1Rv.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +40 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +45 -0
- package/dist/utils/dynamic-import.d.ts +25 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
- package/package.json +49 -22
- package/coverage/clover.xml +0 -3739
- package/coverage/coverage-final.json +0 -31
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
- package/coverage/lcov-report/src/api/errors.ts.html +0 -472
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
- package/coverage/lcov-report/src/api/graphql/index.html +0 -116
- package/coverage/lcov-report/src/api/index.html +0 -176
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
- package/coverage/lcov-report/src/api/rest/index.html +0 -131
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
- package/coverage/lcov-report/src/api/server.ts.html +0 -823
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
- package/coverage/lcov-report/src/auth/index.html +0 -176
- package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
- package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
- package/coverage/lcov-report/src/auth/password.ts.html +0 -310
- package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
- package/coverage/lcov-report/src/collections/index.html +0 -116
- package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
- package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
- package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
- package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
- package/coverage/lcov-report/src/db/index.html +0 -146
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
- package/coverage/lcov-report/src/db/services/index.html +0 -176
- package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
- package/coverage/lcov-report/src/services/index.html +0 -131
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
- package/coverage/lcov-report/src/storage/index.html +0 -161
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
- package/coverage/lcov-report/src/storage/types.ts.html +0 -451
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -7179
- package/jest.config.cjs +0 -19
- package/src/api/ast-schema-editor.ts +0 -289
- package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
- package/src/api/errors.ts +0 -278
- package/src/api/graphql/graphql-schema-generator.ts +0 -383
- package/src/api/graphql/index.ts +0 -2
- package/src/api/index.ts +0 -11
- package/src/api/logs-routes.ts +0 -132
- package/src/api/openapi-generator.ts +0 -727
- package/src/api/rest/api-generator-count.test.ts +0 -126
- package/src/api/rest/api-generator.ts +0 -747
- package/src/api/rest/query-parser.ts +0 -300
- package/src/api/schema-editor-routes.ts +0 -41
- package/src/api/server.ts +0 -271
- package/src/api/types.ts +0 -104
- package/src/auth/adapter-middleware.ts +0 -114
- package/src/auth/admin-roles-route.ts +0 -36
- package/src/auth/admin-user-ops.ts +0 -236
- package/src/auth/admin-users-route.ts +0 -302
- package/src/auth/api-keys/api-key-middleware.ts +0 -133
- package/src/auth/api-keys/api-key-routes.ts +0 -193
- package/src/auth/api-keys/api-key-store.ts +0 -340
- package/src/auth/apple-oauth.ts +0 -122
- package/src/auth/bitbucket-oauth.ts +0 -84
- package/src/auth/builtin-auth-adapter.ts +0 -380
- package/src/auth/custom-auth-adapter.ts +0 -86
- package/src/auth/discord-oauth.ts +0 -85
- package/src/auth/facebook-oauth.ts +0 -74
- package/src/auth/github-oauth.ts +0 -112
- package/src/auth/gitlab-oauth.ts +0 -72
- package/src/auth/google-oauth.ts +0 -223
- package/src/auth/jwt.ts +0 -208
- package/src/auth/linkedin-oauth.ts +0 -83
- package/src/auth/magic-link-routes.ts +0 -167
- package/src/auth/mfa-crypto.ts +0 -91
- package/src/auth/mfa-routes.ts +0 -323
- package/src/auth/mfa.ts +0 -161
- package/src/auth/microsoft-oauth.ts +0 -90
- package/src/auth/middleware.ts +0 -369
- package/src/auth/password.ts +0 -77
- package/src/auth/rate-limiter.ts +0 -225
- package/src/auth/reset-password-admin.ts +0 -160
- package/src/auth/rls-scope.ts +0 -58
- package/src/auth/routes.ts +0 -802
- package/src/auth/session-routes.ts +0 -353
- package/src/auth/slack-oauth.ts +0 -73
- package/src/auth/spotify-oauth.ts +0 -69
- package/src/auth/twitter-oauth.ts +0 -128
- package/src/collections/BackendCollectionRegistry.ts +0 -20
- package/src/collections/loader.ts +0 -50
- package/src/cron/cron-loader.ts +0 -84
- package/src/cron/cron-routes.test.ts +0 -265
- package/src/cron/cron-routes.ts +0 -85
- package/src/cron/cron-scheduler.test.ts +0 -559
- package/src/cron/cron-scheduler.ts +0 -581
- package/src/cron/cron-store.ts +0 -166
- package/src/db/interfaces.ts +0 -60
- package/src/email/index.ts +0 -19
- package/src/email/smtp-email-service.ts +0 -123
- package/src/email/templates.ts +0 -470
- package/src/env.ts +0 -227
- package/src/functions/function-loader.ts +0 -116
- package/src/functions/function-routes.ts +0 -31
- package/src/history/history-routes.ts +0 -129
- package/src/index.ts +0 -70
- package/src/init/docs.ts +0 -47
- package/src/init/health.ts +0 -37
- package/src/init/middlewares.ts +0 -61
- package/src/init/shutdown.ts +0 -56
- package/src/init/storage.ts +0 -57
- package/src/init.ts +0 -1041
- package/src/serve-spa.ts +0 -89
- package/src/services/driver-registry.ts +0 -183
- package/src/services/routed-realtime-service.ts +0 -113
- package/src/services/webhook-service.ts +0 -155
- package/src/singleton.ts +0 -70
- package/src/storage/LocalStorageController.ts +0 -389
- package/src/storage/S3StorageController.ts +0 -300
- package/src/storage/image-transform.ts +0 -218
- package/src/storage/routes.ts +0 -368
- package/src/storage/storage-registry.ts +0 -188
- package/src/storage/tus-handler.ts +0 -315
- package/src/types/index.ts +0 -27
- package/src/utils/dev-port.ts +0 -196
- package/src/utils/logger.ts +0 -143
- package/src/utils/logging.ts +0 -40
- package/src/utils/request-id.ts +0 -40
- package/src/utils/request-logger.ts +0 -72
- package/test/api-generator.test.ts +0 -588
- package/test/api-key-permission-guard.test.ts +0 -144
- package/test/ast-schema-editor.test.ts +0 -89
- package/test/auth-middleware-hono.test.ts +0 -556
- package/test/auth-routes.test.ts +0 -1098
- package/test/backend-hooks-data.test.ts +0 -477
- package/test/custom-auth-adapter.test.ts +0 -189
- package/test/driver-registry.test.ts +0 -282
- package/test/email-templates.test.ts +0 -174
- package/test/env.test.ts +0 -128
- package/test/error-propagation.test.ts +0 -226
- package/test/errors-hono.test.ts +0 -133
- package/test/errors.test.ts +0 -155
- package/test/function-loader.test.ts +0 -127
- package/test/graphql-schema-generator.test.ts +0 -554
- package/test/jwt-security.test.ts +0 -182
- package/test/jwt.test.ts +0 -326
- package/test/mfa.test.ts +0 -197
- package/test/middleware.test.ts +0 -305
- package/test/multi-datasource-routing.test.ts +0 -113
- package/test/password.test.ts +0 -165
- package/test/query-parser.test.ts +0 -233
- package/test/rate-limiter.test.ts +0 -102
- package/test/reset-password-admin.test.ts +0 -113
- package/test/routed-realtime-service.test.ts +0 -86
- package/test/safe-compare.test.ts +0 -66
- package/test/singleton.test.ts +0 -59
- package/test/smtp-email-service.test.ts +0 -169
- package/test/storage-local.test.ts +0 -271
- package/test/storage-registry.test.ts +0 -282
- package/test/storage-routes.test.ts +0 -222
- package/test/storage-s3.test.ts +0 -304
- package/test/transform-auth-response.test.ts +0 -305
- package/test/webhook-service.test.ts +0 -260
- package/tsconfig.json +0 -54
- package/tsconfig.prod.json +0 -23
- package/vite.config.ts +0 -86
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local filesystem storage controller
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from "fs";
|
|
6
|
-
import * as path from "path";
|
|
7
|
-
import { promisify } from "util";
|
|
8
|
-
import {
|
|
9
|
-
StorageController,
|
|
10
|
-
LocalStorageConfig,
|
|
11
|
-
DEFAULT_MAX_FILE_SIZE
|
|
12
|
-
} from "./types";
|
|
13
|
-
import {
|
|
14
|
-
UploadFileProps,
|
|
15
|
-
UploadFileResult,
|
|
16
|
-
DownloadConfig,
|
|
17
|
-
DownloadMetadata,
|
|
18
|
-
StorageListResult,
|
|
19
|
-
StorageReference
|
|
20
|
-
} from "@rebasepro/types";
|
|
21
|
-
|
|
22
|
-
const mkdir = promisify(fs.mkdir);
|
|
23
|
-
const writeFile = promisify(fs.writeFile);
|
|
24
|
-
const readFile = promisify(fs.readFile);
|
|
25
|
-
const unlink = promisify(fs.unlink);
|
|
26
|
-
const readdir = promisify(fs.readdir);
|
|
27
|
-
const stat = promisify(fs.stat);
|
|
28
|
-
const access = promisify(fs.access);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Remove initial and trailing slashes from a path.
|
|
32
|
-
* Handles paths like "/images/", "images/", "/images" → "images"
|
|
33
|
-
*/
|
|
34
|
-
function normalizeStoragePath(s: string): string {
|
|
35
|
-
let result = s;
|
|
36
|
-
while (result.startsWith("/")) {
|
|
37
|
-
result = result.slice(1);
|
|
38
|
-
}
|
|
39
|
-
while (result.endsWith("/")) {
|
|
40
|
-
result = result.slice(0, -1);
|
|
41
|
-
}
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Local filesystem storage implementation
|
|
47
|
-
* Stores files in a directory structure: {basePath}/{bucket}/{path}
|
|
48
|
-
*/
|
|
49
|
-
export class LocalStorageController implements StorageController {
|
|
50
|
-
private config: LocalStorageConfig;
|
|
51
|
-
private basePath: string;
|
|
52
|
-
|
|
53
|
-
constructor(config: LocalStorageConfig) {
|
|
54
|
-
this.config = config;
|
|
55
|
-
this.basePath = path.resolve(config.basePath);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
getType(): "local" {
|
|
59
|
-
return "local";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Ensure directory exists, creating it if necessary
|
|
64
|
-
*/
|
|
65
|
-
private async ensureDir(dirPath: string): Promise<void> {
|
|
66
|
-
try {
|
|
67
|
-
await mkdir(dirPath, { recursive: true });
|
|
68
|
-
} catch (error: unknown) {
|
|
69
|
-
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get the full filesystem path for a storage path.
|
|
77
|
-
* Includes a path traversal guard to prevent escaping the base directory.
|
|
78
|
-
*/
|
|
79
|
-
private getFullPath(storagePath: string, bucket?: string): string {
|
|
80
|
-
const bucketPath = bucket ? path.join(this.basePath, bucket) : this.basePath;
|
|
81
|
-
const resolved = path.resolve(path.join(bucketPath, storagePath));
|
|
82
|
-
if (!resolved.startsWith(bucketPath + path.sep) && resolved !== bucketPath) {
|
|
83
|
-
throw new Error("Path traversal detected: resolved storage path is outside the bucket directory.");
|
|
84
|
-
}
|
|
85
|
-
return resolved;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Validate file before upload
|
|
90
|
-
*/
|
|
91
|
-
private validateFile(file: File): void {
|
|
92
|
-
const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
93
|
-
if (file.size > maxSize) {
|
|
94
|
-
throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
98
|
-
if (!this.config.allowedMimeTypes.includes(file.type)) {
|
|
99
|
-
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(", ")}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async putObject({
|
|
105
|
-
file,
|
|
106
|
-
key,
|
|
107
|
-
metadata,
|
|
108
|
-
bucket
|
|
109
|
-
}: UploadFileProps): Promise<UploadFileResult> {
|
|
110
|
-
this.validateFile(file);
|
|
111
|
-
|
|
112
|
-
// Always use a bucket (default to 'default')
|
|
113
|
-
const usedBucket = bucket ?? "default";
|
|
114
|
-
const fullStoragePath = key;
|
|
115
|
-
const fullPath = this.getFullPath(fullStoragePath, usedBucket);
|
|
116
|
-
|
|
117
|
-
// Ensure parent directory exists
|
|
118
|
-
await this.ensureDir(path.dirname(fullPath));
|
|
119
|
-
|
|
120
|
-
// Convert File to Buffer and write
|
|
121
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
122
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
123
|
-
await writeFile(fullPath, buffer);
|
|
124
|
-
|
|
125
|
-
// Always save metadata file with at least contentType (required for preview)
|
|
126
|
-
const metadataPath = `${fullPath}.metadata.json`;
|
|
127
|
-
await writeFile(metadataPath, JSON.stringify({
|
|
128
|
-
...(metadata || {}),
|
|
129
|
-
contentType: file.type,
|
|
130
|
-
size: file.size,
|
|
131
|
-
uploadedAt: new Date().toISOString()
|
|
132
|
-
}, null, 2));
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
key: fullStoragePath,
|
|
136
|
-
bucket: usedBucket,
|
|
137
|
-
storageUrl: `local://${usedBucket}/${fullStoragePath}`
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
|
|
142
|
-
// Handle local:// URLs
|
|
143
|
-
let resolvedPath = key;
|
|
144
|
-
let resolvedBucket = bucket;
|
|
145
|
-
|
|
146
|
-
if (key.startsWith("local://")) {
|
|
147
|
-
const withoutProtocol = key.substring("local://".length);
|
|
148
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
149
|
-
if (firstSlash > 0) {
|
|
150
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
151
|
-
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Normalize path to handle leading/trailing slashes
|
|
156
|
-
resolvedPath = normalizeStoragePath(resolvedPath);
|
|
157
|
-
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
await access(fullPath, fs.constants.R_OK);
|
|
161
|
-
} catch {
|
|
162
|
-
return {
|
|
163
|
-
url: null,
|
|
164
|
-
fileNotFound: true
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Read metadata if available
|
|
169
|
-
let metadata: DownloadMetadata | undefined;
|
|
170
|
-
const metadataPath = `${fullPath}.metadata.json`;
|
|
171
|
-
try {
|
|
172
|
-
const metadataContent = await readFile(metadataPath, "utf-8");
|
|
173
|
-
const savedMetadata = JSON.parse(metadataContent);
|
|
174
|
-
const fileStat = await stat(fullPath);
|
|
175
|
-
|
|
176
|
-
metadata = {
|
|
177
|
-
bucket: resolvedBucket ?? "default",
|
|
178
|
-
fullPath: resolvedPath,
|
|
179
|
-
name: path.basename(resolvedPath),
|
|
180
|
-
size: fileStat.size,
|
|
181
|
-
contentType: savedMetadata.contentType || "application/octet-stream",
|
|
182
|
-
customMetadata: savedMetadata
|
|
183
|
-
};
|
|
184
|
-
} catch {
|
|
185
|
-
// No metadata file, create basic metadata from stat
|
|
186
|
-
try {
|
|
187
|
-
const fileStat = await stat(fullPath);
|
|
188
|
-
metadata = {
|
|
189
|
-
bucket: resolvedBucket ?? "default",
|
|
190
|
-
fullPath: resolvedPath,
|
|
191
|
-
name: path.basename(resolvedPath),
|
|
192
|
-
size: fileStat.size,
|
|
193
|
-
contentType: "application/octet-stream",
|
|
194
|
-
customMetadata: {}
|
|
195
|
-
};
|
|
196
|
-
} catch {
|
|
197
|
-
// Stat failed
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Return a relative URL that will be served by the storage routes
|
|
202
|
-
const bucketPath = resolvedBucket ? `${resolvedBucket}/` : "";
|
|
203
|
-
const url = `/api/storage/file/${bucketPath}${resolvedPath}`;
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
url,
|
|
207
|
-
metadata
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async getObject(key: string, bucket?: string): Promise<File | null> {
|
|
212
|
-
// Handle local:// URLs
|
|
213
|
-
let resolvedPath = key;
|
|
214
|
-
let resolvedBucket = bucket;
|
|
215
|
-
|
|
216
|
-
if (key.startsWith("local://")) {
|
|
217
|
-
const withoutProtocol = key.substring("local://".length);
|
|
218
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
219
|
-
if (firstSlash > 0) {
|
|
220
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
221
|
-
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Normalize path to handle leading/trailing slashes
|
|
226
|
-
resolvedPath = normalizeStoragePath(resolvedPath);
|
|
227
|
-
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
await access(fullPath, fs.constants.R_OK);
|
|
231
|
-
const buffer = await readFile(fullPath);
|
|
232
|
-
|
|
233
|
-
// Try to get content type from metadata
|
|
234
|
-
let contentType = "application/octet-stream";
|
|
235
|
-
try {
|
|
236
|
-
const metadataPath = `${fullPath}.metadata.json`;
|
|
237
|
-
const metadataContent = await readFile(metadataPath, "utf-8");
|
|
238
|
-
const metadata = JSON.parse(metadataContent);
|
|
239
|
-
contentType = metadata.contentType || contentType;
|
|
240
|
-
} catch {
|
|
241
|
-
// No metadata, use default content type
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const blob = new Blob([buffer], { type: contentType });
|
|
245
|
-
return new File([blob], path.basename(resolvedPath), { type: contentType });
|
|
246
|
-
} catch {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async deleteObject(key: string, bucket?: string): Promise<void> {
|
|
252
|
-
// Handle local:// URLs
|
|
253
|
-
let resolvedPath = key;
|
|
254
|
-
let resolvedBucket = bucket;
|
|
255
|
-
|
|
256
|
-
if (key.startsWith("local://")) {
|
|
257
|
-
const withoutProtocol = key.substring("local://".length);
|
|
258
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
259
|
-
if (firstSlash > 0) {
|
|
260
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
261
|
-
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Normalize path to handle leading/trailing slashes
|
|
266
|
-
resolvedPath = normalizeStoragePath(resolvedPath);
|
|
267
|
-
|
|
268
|
-
if (!resolvedPath) {
|
|
269
|
-
// Safety: never delete the bucket root
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
274
|
-
|
|
275
|
-
// Check if path exists before attempting to delete
|
|
276
|
-
try {
|
|
277
|
-
await access(fullPath, fs.constants.F_OK);
|
|
278
|
-
} catch {
|
|
279
|
-
// File doesn't exist — nothing to delete
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const stats = await stat(fullPath);
|
|
285
|
-
if (stats.isDirectory()) {
|
|
286
|
-
// Only remove if empty — client must delete contents first
|
|
287
|
-
await fs.promises.rmdir(fullPath);
|
|
288
|
-
} else {
|
|
289
|
-
await unlink(fullPath);
|
|
290
|
-
// Also delete metadata file if exists
|
|
291
|
-
try {
|
|
292
|
-
await unlink(`${fullPath}.metadata.json`);
|
|
293
|
-
} catch {
|
|
294
|
-
// Metadata file might not exist
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
} catch (error: unknown) {
|
|
298
|
-
if (error instanceof Error) {
|
|
299
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
300
|
-
if (code === "ENOENT" || code === "ENOTEMPTY") {
|
|
301
|
-
// File doesn't exist or directory not empty — ignore
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async listObjects(prefix: string, options?: {
|
|
310
|
-
bucket?: string;
|
|
311
|
-
maxResults?: number;
|
|
312
|
-
pageToken?: string;
|
|
313
|
-
}): Promise<StorageListResult> {
|
|
314
|
-
// Normalize path to handle leading/trailing slashes
|
|
315
|
-
const normalizedPath = normalizeStoragePath(prefix);
|
|
316
|
-
const fullPath = this.getFullPath(normalizedPath, options?.bucket);
|
|
317
|
-
const items: StorageReference[] = [];
|
|
318
|
-
const prefixes: StorageReference[] = [];
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
await access(fullPath, fs.constants.R_OK);
|
|
322
|
-
const entries = await readdir(fullPath, { withFileTypes: true });
|
|
323
|
-
|
|
324
|
-
let count = 0;
|
|
325
|
-
const maxResults = options?.maxResults ?? 1000;
|
|
326
|
-
const startIndex = options?.pageToken ? parseInt(options.pageToken, 10) : 0;
|
|
327
|
-
|
|
328
|
-
for (let i = startIndex; i < entries.length && count < maxResults; i++) {
|
|
329
|
-
const entry = entries[i];
|
|
330
|
-
|
|
331
|
-
// Skip metadata files
|
|
332
|
-
if (entry.name.endsWith(".metadata.json")) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
337
|
-
const bucket = options?.bucket ?? "default";
|
|
338
|
-
|
|
339
|
-
const ref: StorageReference = {
|
|
340
|
-
bucket,
|
|
341
|
-
fullPath: entryPath,
|
|
342
|
-
name: entry.name,
|
|
343
|
-
parent: null as never, // Simplified - not fully implementing parent chain
|
|
344
|
-
root: null as never,
|
|
345
|
-
toString: () => `local://${bucket}/${entryPath}`
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
if (entry.isDirectory()) {
|
|
349
|
-
prefixes.push(ref);
|
|
350
|
-
} else {
|
|
351
|
-
items.push(ref);
|
|
352
|
-
}
|
|
353
|
-
count++;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const nextPageToken = startIndex + count < entries.length
|
|
357
|
-
? String(startIndex + count)
|
|
358
|
-
: undefined;
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
items,
|
|
362
|
-
prefixes,
|
|
363
|
-
nextPageToken
|
|
364
|
-
};
|
|
365
|
-
} catch (error: unknown) {
|
|
366
|
-
const code = (error as NodeJS.ErrnoException)?.code;
|
|
367
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
368
|
-
return { items: [],
|
|
369
|
-
prefixes: [] };
|
|
370
|
-
}
|
|
371
|
-
throw error;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Get the absolute filesystem path for serving files
|
|
377
|
-
* Used by the storage routes to serve files directly
|
|
378
|
-
*/
|
|
379
|
-
getAbsolutePath(key: string, bucket?: string): string {
|
|
380
|
-
return this.getFullPath(key, bucket);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Get the base path for the storage
|
|
385
|
-
*/
|
|
386
|
-
getBasePath(): string {
|
|
387
|
-
return this.basePath;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3-compatible storage controller (works with AWS S3 and MinIO)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
S3Client,
|
|
7
|
-
PutObjectCommand,
|
|
8
|
-
GetObjectCommand,
|
|
9
|
-
DeleteObjectCommand,
|
|
10
|
-
ListObjectsV2Command,
|
|
11
|
-
HeadObjectCommand,
|
|
12
|
-
_Object,
|
|
13
|
-
CommonPrefix
|
|
14
|
-
} from "@aws-sdk/client-s3";
|
|
15
|
-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
16
|
-
import {
|
|
17
|
-
StorageController,
|
|
18
|
-
S3StorageConfig,
|
|
19
|
-
DEFAULT_MAX_FILE_SIZE
|
|
20
|
-
} from "./types";
|
|
21
|
-
import {
|
|
22
|
-
UploadFileProps,
|
|
23
|
-
UploadFileResult,
|
|
24
|
-
DownloadConfig,
|
|
25
|
-
DownloadMetadata,
|
|
26
|
-
StorageListResult,
|
|
27
|
-
StorageReference
|
|
28
|
-
} from "@rebasepro/types";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* S3-compatible storage implementation
|
|
32
|
-
* Works with AWS S3 and MinIO (with forcePathStyle option)
|
|
33
|
-
*/
|
|
34
|
-
export class S3StorageController implements StorageController {
|
|
35
|
-
private config: S3StorageConfig;
|
|
36
|
-
private client: S3Client;
|
|
37
|
-
|
|
38
|
-
constructor(config: S3StorageConfig) {
|
|
39
|
-
this.config = config;
|
|
40
|
-
this.client = new S3Client({
|
|
41
|
-
region: config.region || "us-east-1",
|
|
42
|
-
endpoint: config.endpoint,
|
|
43
|
-
forcePathStyle: config.forcePathStyle ?? !!config.endpoint, // Auto-enable for custom endpoints (MinIO)
|
|
44
|
-
credentials: {
|
|
45
|
-
accessKeyId: config.accessKeyId,
|
|
46
|
-
secretAccessKey: config.secretAccessKey
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getType(): "s3" {
|
|
52
|
-
return "s3";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Validate file before upload
|
|
57
|
-
*/
|
|
58
|
-
private validateFile(file: File): void {
|
|
59
|
-
const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
60
|
-
if (file.size > maxSize) {
|
|
61
|
-
throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
65
|
-
if (!this.config.allowedMimeTypes.includes(file.type)) {
|
|
66
|
-
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(", ")}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get the bucket name - either from parameter or config
|
|
73
|
-
*/
|
|
74
|
-
private getBucket(bucket?: string): string {
|
|
75
|
-
// "default" is a logical bucket name used by local storage;
|
|
76
|
-
// for S3 it should resolve to the configured bucket.
|
|
77
|
-
if (!bucket || bucket === "default") return this.config.bucket;
|
|
78
|
-
return bucket;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async putObject({
|
|
82
|
-
file,
|
|
83
|
-
key,
|
|
84
|
-
metadata,
|
|
85
|
-
bucket
|
|
86
|
-
}: UploadFileProps): Promise<UploadFileResult> {
|
|
87
|
-
this.validateFile(file);
|
|
88
|
-
|
|
89
|
-
const usedBucket = this.getBucket(bucket);
|
|
90
|
-
|
|
91
|
-
// Convert File to Buffer
|
|
92
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
93
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
94
|
-
|
|
95
|
-
const command = new PutObjectCommand({
|
|
96
|
-
Bucket: usedBucket,
|
|
97
|
-
Key: key,
|
|
98
|
-
Body: buffer,
|
|
99
|
-
ContentType: file.type,
|
|
100
|
-
Metadata: metadata ? this.flattenMetadata(metadata) : undefined
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
await this.client.send(command);
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
key,
|
|
107
|
-
bucket: usedBucket,
|
|
108
|
-
storageUrl: `s3://${usedBucket}/${key}`
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Flatten nested metadata to string values (S3 requirement)
|
|
114
|
-
*/
|
|
115
|
-
private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {
|
|
116
|
-
const flattened: Record<string, string> = {};
|
|
117
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
118
|
-
if (typeof value === "string") {
|
|
119
|
-
flattened[key] = value;
|
|
120
|
-
} else if (value !== undefined && value !== null) {
|
|
121
|
-
flattened[key] = JSON.stringify(value);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return flattened;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
|
|
128
|
-
// Handle s3:// and gs:// URLs for backward compatibility
|
|
129
|
-
let resolvedPath = key;
|
|
130
|
-
let resolvedBucket = this.getBucket(bucket);
|
|
131
|
-
|
|
132
|
-
const match = key.match(/^(s3|gs):\/\//);
|
|
133
|
-
if (match) {
|
|
134
|
-
const protocolLength = match[0].length;
|
|
135
|
-
const withoutProtocol = key.substring(protocolLength);
|
|
136
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
137
|
-
if (firstSlash > 0) {
|
|
138
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
139
|
-
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
// First check if the object exists and get metadata
|
|
145
|
-
const headCommand = new HeadObjectCommand({
|
|
146
|
-
Bucket: resolvedBucket,
|
|
147
|
-
Key: resolvedPath
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const headResult = await this.client.send(headCommand);
|
|
151
|
-
|
|
152
|
-
// Generate a signed URL
|
|
153
|
-
const getCommand = new GetObjectCommand({
|
|
154
|
-
Bucket: resolvedBucket,
|
|
155
|
-
Key: resolvedPath
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const expiresIn = this.config.signedUrlExpiration ?? 3600;
|
|
159
|
-
const url = await getSignedUrl(this.client, getCommand, { expiresIn });
|
|
160
|
-
|
|
161
|
-
const metadata: DownloadMetadata = {
|
|
162
|
-
bucket: resolvedBucket,
|
|
163
|
-
fullPath: resolvedPath,
|
|
164
|
-
name: resolvedPath.split("/").pop() || resolvedPath,
|
|
165
|
-
size: headResult.ContentLength || 0,
|
|
166
|
-
contentType: headResult.ContentType || "application/octet-stream",
|
|
167
|
-
customMetadata: headResult.Metadata || {}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
url,
|
|
172
|
-
metadata
|
|
173
|
-
};
|
|
174
|
-
} catch (error: unknown) {
|
|
175
|
-
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
176
|
-
if (s3Error.name === "NotFound" || s3Error.$metadata?.httpStatusCode === 404) {
|
|
177
|
-
return {
|
|
178
|
-
url: null,
|
|
179
|
-
fileNotFound: true
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async getObject(key: string, bucket?: string): Promise<File | null> {
|
|
187
|
-
// Handle s3:// and gs:// URLs
|
|
188
|
-
let resolvedPath = key;
|
|
189
|
-
let resolvedBucket = this.getBucket(bucket);
|
|
190
|
-
|
|
191
|
-
const match = key.match(/^(s3|gs):\/\//);
|
|
192
|
-
if (match) {
|
|
193
|
-
const protocolLength = match[0].length;
|
|
194
|
-
const withoutProtocol = key.substring(protocolLength);
|
|
195
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
196
|
-
if (firstSlash > 0) {
|
|
197
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
198
|
-
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
const command = new GetObjectCommand({
|
|
204
|
-
Bucket: resolvedBucket,
|
|
205
|
-
Key: resolvedPath
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const response = await this.client.send(command);
|
|
209
|
-
|
|
210
|
-
if (!response.Body) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Convert stream to buffer
|
|
215
|
-
const chunks: Uint8Array[] = [];
|
|
216
|
-
for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {
|
|
217
|
-
chunks.push(chunk);
|
|
218
|
-
}
|
|
219
|
-
const buffer = Buffer.concat(chunks);
|
|
220
|
-
|
|
221
|
-
const contentType = response.ContentType || "application/octet-stream";
|
|
222
|
-
const fileName = resolvedPath.split("/").pop() || resolvedPath;
|
|
223
|
-
|
|
224
|
-
const blob = new Blob([buffer], { type: contentType });
|
|
225
|
-
return new File([blob], fileName, { type: contentType });
|
|
226
|
-
} catch (error: unknown) {
|
|
227
|
-
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
228
|
-
if (s3Error.name === "NoSuchKey" || s3Error.$metadata?.httpStatusCode === 404) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
throw error;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async deleteObject(key: string, bucket?: string): Promise<void> {
|
|
236
|
-
// Handle s3:// and gs:// URLs
|
|
237
|
-
let resolvedPath = key;
|
|
238
|
-
let resolvedBucket = this.getBucket(bucket);
|
|
239
|
-
|
|
240
|
-
const match = key.match(/^(s3|gs):\/\//);
|
|
241
|
-
if (match) {
|
|
242
|
-
const protocolLength = match[0].length;
|
|
243
|
-
const withoutProtocol = key.substring(protocolLength);
|
|
244
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
245
|
-
if (firstSlash > 0) {
|
|
246
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
247
|
-
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const command = new DeleteObjectCommand({
|
|
252
|
-
Bucket: resolvedBucket,
|
|
253
|
-
Key: resolvedPath
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
await this.client.send(command);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async listObjects(prefix: string, options?: {
|
|
260
|
-
bucket?: string;
|
|
261
|
-
maxResults?: number;
|
|
262
|
-
pageToken?: string;
|
|
263
|
-
}): Promise<StorageListResult> {
|
|
264
|
-
const resolvedBucket = this.getBucket(options?.bucket);
|
|
265
|
-
|
|
266
|
-
const command = new ListObjectsV2Command({
|
|
267
|
-
Bucket: resolvedBucket,
|
|
268
|
-
Prefix: prefix || undefined,
|
|
269
|
-
MaxKeys: options?.maxResults ?? 1000,
|
|
270
|
-
ContinuationToken: options?.pageToken,
|
|
271
|
-
Delimiter: "/" // This gives us folder-like behavior
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const response = await this.client.send(command);
|
|
275
|
-
|
|
276
|
-
const items: StorageReference[] = (response.Contents || []).map(obj => ({
|
|
277
|
-
bucket: resolvedBucket,
|
|
278
|
-
fullPath: obj.Key || "",
|
|
279
|
-
name: (obj.Key || "").split("/").pop() || "",
|
|
280
|
-
parent: null as never,
|
|
281
|
-
root: null as never,
|
|
282
|
-
toString: () => `s3://${resolvedBucket}/${obj.Key}`
|
|
283
|
-
}));
|
|
284
|
-
|
|
285
|
-
const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({
|
|
286
|
-
bucket: resolvedBucket,
|
|
287
|
-
fullPath: prefix.Prefix || "",
|
|
288
|
-
name: (prefix.Prefix || "").replace(/\/$/, "").split("/").pop() || "",
|
|
289
|
-
parent: null as never,
|
|
290
|
-
root: null as never,
|
|
291
|
-
toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
|
|
292
|
-
}));
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
items,
|
|
296
|
-
prefixes,
|
|
297
|
-
nextPageToken: response.NextContinuationToken
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
}
|