@rebasepro/server-core 0.0.1-canary.09e5ec5
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/LICENSE +6 -0
- package/README.md +40 -0
- package/build-errors.txt +52 -0
- package/coverage/clover.xml +3739 -0
- package/coverage/coverage-final.json +31 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
- package/coverage/lcov-report/src/api/errors.ts.html +472 -0
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
- package/coverage/lcov-report/src/api/graphql/index.html +116 -0
- package/coverage/lcov-report/src/api/index.html +176 -0
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
- package/coverage/lcov-report/src/api/rest/index.html +131 -0
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
- package/coverage/lcov-report/src/api/server.ts.html +823 -0
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
- package/coverage/lcov-report/src/auth/index.html +176 -0
- package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
- package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
- package/coverage/lcov-report/src/auth/password.ts.html +310 -0
- package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
- package/coverage/lcov-report/src/collections/index.html +116 -0
- package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
- package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
- package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
- package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
- package/coverage/lcov-report/src/db/index.html +146 -0
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
- package/coverage/lcov-report/src/db/services/index.html +176 -0
- package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
- package/coverage/lcov-report/src/services/index.html +131 -0
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
- package/coverage/lcov-report/src/storage/index.html +161 -0
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
- package/coverage/lcov-report/src/storage/types.ts.html +451 -0
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov.info +7179 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49934 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +49968 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
- package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/server-core/src/api/errors.d.ts +35 -0
- package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
- package/dist/server-core/src/api/graphql/index.d.ts +1 -0
- package/dist/server-core/src/api/index.d.ts +9 -0
- package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
- package/dist/server-core/src/api/rest/index.d.ts +1 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
- package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
- package/dist/server-core/src/api/server.d.ts +40 -0
- package/dist/server-core/src/api/types.d.ts +90 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +16 -0
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/index.d.ts +23 -0
- package/dist/server-core/src/auth/interfaces.d.ts +309 -0
- package/dist/server-core/src/auth/jwt.d.ts +43 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +81 -0
- package/dist/server-core/src/auth/password.d.ts +22 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +27 -0
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/server-core/src/collections/loader.d.ts +5 -0
- package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -0
- package/dist/server-core/src/db/interfaces.d.ts +18 -0
- package/dist/server-core/src/email/index.d.ts +6 -0
- package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
- package/dist/server-core/src/email/templates.d.ts +42 -0
- package/dist/server-core/src/email/types.d.ts +107 -0
- package/dist/server-core/src/functions/function-loader.d.ts +17 -0
- package/dist/server-core/src/functions/function-routes.d.ts +10 -0
- package/dist/server-core/src/functions/index.d.ts +3 -0
- package/dist/server-core/src/history/history-routes.d.ts +23 -0
- package/dist/server-core/src/history/index.d.ts +1 -0
- package/dist/server-core/src/index.d.ts +29 -0
- package/dist/server-core/src/init.d.ts +159 -0
- package/dist/server-core/src/serve-spa.d.ts +30 -0
- package/dist/server-core/src/services/driver-registry.d.ts +78 -0
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
- package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
- package/dist/server-core/src/storage/index.d.ts +25 -0
- package/dist/server-core/src/storage/routes.d.ts +38 -0
- package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
- package/dist/server-core/src/storage/types.d.ts +103 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -0
- package/dist/server-core/src/utils/sql.d.ts +27 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +23 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/history_diff.log +385 -0
- package/jest.config.cjs +16 -0
- package/package.json +86 -0
- package/scratch.ts +9 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
- package/src/api/errors.ts +179 -0
- package/src/api/graphql/graphql-schema-generator.ts +336 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +715 -0
- package/src/api/rest/api-generator.ts +472 -0
- package/src/api/rest/index.ts +2 -0
- package/src/api/rest/query-parser.ts +155 -0
- package/src/api/schema-editor-routes.ts +41 -0
- package/src/api/server.ts +248 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +529 -0
- package/src/auth/apple-oauth.ts +130 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +48 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/interfaces.ts +363 -0
- package/src/auth/jwt.ts +181 -0
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +384 -0
- package/src/auth/password.ts +77 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +788 -0
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +421 -0
- package/src/cron/cron-scheduler.ts +413 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +18 -0
- package/src/email/smtp-email-service.ts +91 -0
- package/src/email/templates.ts +388 -0
- package/src/email/types.ts +105 -0
- package/src/functions/function-loader.ts +119 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +129 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +66 -0
- package/src/init.ts +727 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +365 -0
- package/src/storage/S3StorageController.ts +298 -0
- package/src/storage/index.ts +43 -0
- package/src/storage/routes.ts +264 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +134 -0
- package/src/types/index.ts +27 -0
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +38 -0
- package/src/utils/request-logger.ts +66 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +640 -0
- package/test/api-generator.test.ts +501 -0
- package/test/ast-schema-editor.test.ts +63 -0
- package/test/auth-middleware-hono.test.ts +556 -0
- package/test/auth-routes.test.ts +1047 -0
- package/test/driver-registry.test.ts +282 -0
- package/test/error-propagation.test.ts +226 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +155 -0
- package/test/jwt-security.test.ts +182 -0
- package/test/jwt.test.ts +324 -0
- package/test/middleware.test.ts +300 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +263 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +271 -0
- package/test/storage-registry.test.ts +282 -0
- package/test/storage-routes.test.ts +222 -0
- package/test/storage-s3.test.ts +304 -0
- package/test-ast.ts +28 -0
- package/test.ts +6 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +80 -0
|
@@ -0,0 +1,365 @@
|
|
|
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 parts = bucket ? [this.basePath, bucket, storagePath] : [this.basePath, storagePath];
|
|
81
|
+
const resolved = path.resolve(path.join(...parts));
|
|
82
|
+
if (!resolved.startsWith(this.basePath + path.sep) && resolved !== this.basePath) {
|
|
83
|
+
throw new Error("Path traversal detected: resolved storage path is outside the base 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
|
+
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await unlink(fullPath);
|
|
271
|
+
// Also delete metadata file if exists
|
|
272
|
+
try {
|
|
273
|
+
await unlink(`${fullPath}.metadata.json`);
|
|
274
|
+
} catch {
|
|
275
|
+
// Metadata file might not exist
|
|
276
|
+
}
|
|
277
|
+
} catch (error: unknown) {
|
|
278
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
// File doesn't exist, nothing to delete
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async listObjects(prefix: string, options?: {
|
|
286
|
+
bucket?: string;
|
|
287
|
+
maxResults?: number;
|
|
288
|
+
pageToken?: string;
|
|
289
|
+
}): Promise<StorageListResult> {
|
|
290
|
+
// Normalize path to handle leading/trailing slashes
|
|
291
|
+
const normalizedPath = normalizeStoragePath(prefix);
|
|
292
|
+
const fullPath = this.getFullPath(normalizedPath, options?.bucket);
|
|
293
|
+
const items: StorageReference[] = [];
|
|
294
|
+
const prefixes: StorageReference[] = [];
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
await access(fullPath, fs.constants.R_OK);
|
|
298
|
+
const entries = await readdir(fullPath, { withFileTypes: true });
|
|
299
|
+
|
|
300
|
+
let count = 0;
|
|
301
|
+
const maxResults = options?.maxResults ?? 1000;
|
|
302
|
+
const startIndex = options?.pageToken ? parseInt(options.pageToken, 10) : 0;
|
|
303
|
+
|
|
304
|
+
for (let i = startIndex; i < entries.length && count < maxResults; i++) {
|
|
305
|
+
const entry = entries[i];
|
|
306
|
+
|
|
307
|
+
// Skip metadata files
|
|
308
|
+
if (entry.name.endsWith(".metadata.json")) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
313
|
+
const bucket = options?.bucket ?? "default";
|
|
314
|
+
|
|
315
|
+
const ref: StorageReference = {
|
|
316
|
+
bucket,
|
|
317
|
+
fullPath: entryPath,
|
|
318
|
+
name: entry.name,
|
|
319
|
+
parent: null as never, // Simplified - not fully implementing parent chain
|
|
320
|
+
root: null as never,
|
|
321
|
+
toString: () => `local://${bucket}/${entryPath}`
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (entry.isDirectory()) {
|
|
325
|
+
prefixes.push(ref);
|
|
326
|
+
} else {
|
|
327
|
+
items.push(ref);
|
|
328
|
+
}
|
|
329
|
+
count++;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const nextPageToken = startIndex + count < entries.length
|
|
333
|
+
? String(startIndex + count)
|
|
334
|
+
: undefined;
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
items,
|
|
338
|
+
prefixes,
|
|
339
|
+
nextPageToken
|
|
340
|
+
};
|
|
341
|
+
} catch (error: unknown) {
|
|
342
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
343
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
344
|
+
return { items: [],
|
|
345
|
+
prefixes: [] };
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get the absolute filesystem path for serving files
|
|
353
|
+
* Used by the storage routes to serve files directly
|
|
354
|
+
*/
|
|
355
|
+
getAbsolutePath(key: string, bucket?: string): string {
|
|
356
|
+
return this.getFullPath(key, bucket);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get the base path for the storage
|
|
361
|
+
*/
|
|
362
|
+
getBasePath(): string {
|
|
363
|
+
return this.basePath;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
return bucket ?? this.config.bucket;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async putObject({
|
|
79
|
+
file,
|
|
80
|
+
key,
|
|
81
|
+
metadata,
|
|
82
|
+
bucket
|
|
83
|
+
}: UploadFileProps): Promise<UploadFileResult> {
|
|
84
|
+
this.validateFile(file);
|
|
85
|
+
|
|
86
|
+
const usedBucket = this.getBucket(bucket);
|
|
87
|
+
|
|
88
|
+
// Convert File to Buffer
|
|
89
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
90
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
91
|
+
|
|
92
|
+
const command = new PutObjectCommand({
|
|
93
|
+
Bucket: usedBucket,
|
|
94
|
+
Key: key,
|
|
95
|
+
Body: buffer,
|
|
96
|
+
ContentType: file.type,
|
|
97
|
+
Metadata: metadata ? this.flattenMetadata(metadata) : undefined
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await this.client.send(command);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
key,
|
|
104
|
+
bucket: usedBucket,
|
|
105
|
+
storageUrl: `s3://${usedBucket}/${key}`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Flatten nested metadata to string values (S3 requirement)
|
|
111
|
+
*/
|
|
112
|
+
private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {
|
|
113
|
+
const flattened: Record<string, string> = {};
|
|
114
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
flattened[key] = value;
|
|
117
|
+
} else if (value !== undefined && value !== null) {
|
|
118
|
+
flattened[key] = JSON.stringify(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return flattened;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
|
|
125
|
+
// Handle s3:// and gs:// URLs for backward compatibility
|
|
126
|
+
let resolvedPath = key;
|
|
127
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
128
|
+
|
|
129
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
130
|
+
if (match) {
|
|
131
|
+
const protocolLength = match[0].length;
|
|
132
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
133
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
134
|
+
if (firstSlash > 0) {
|
|
135
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
136
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// First check if the object exists and get metadata
|
|
142
|
+
const headCommand = new HeadObjectCommand({
|
|
143
|
+
Bucket: resolvedBucket,
|
|
144
|
+
Key: resolvedPath
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const headResult = await this.client.send(headCommand);
|
|
148
|
+
|
|
149
|
+
// Generate a signed URL
|
|
150
|
+
const getCommand = new GetObjectCommand({
|
|
151
|
+
Bucket: resolvedBucket,
|
|
152
|
+
Key: resolvedPath
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const expiresIn = this.config.signedUrlExpiration ?? 3600;
|
|
156
|
+
const url = await getSignedUrl(this.client, getCommand, { expiresIn });
|
|
157
|
+
|
|
158
|
+
const metadata: DownloadMetadata = {
|
|
159
|
+
bucket: resolvedBucket,
|
|
160
|
+
fullPath: resolvedPath,
|
|
161
|
+
name: resolvedPath.split("/").pop() || resolvedPath,
|
|
162
|
+
size: headResult.ContentLength || 0,
|
|
163
|
+
contentType: headResult.ContentType || "application/octet-stream",
|
|
164
|
+
customMetadata: headResult.Metadata || {}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
url,
|
|
169
|
+
metadata
|
|
170
|
+
};
|
|
171
|
+
} catch (error: unknown) {
|
|
172
|
+
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
173
|
+
if (s3Error.name === "NotFound" || s3Error.$metadata?.httpStatusCode === 404) {
|
|
174
|
+
return {
|
|
175
|
+
url: null,
|
|
176
|
+
fileNotFound: true
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getObject(key: string, bucket?: string): Promise<File | null> {
|
|
184
|
+
// Handle s3:// and gs:// URLs
|
|
185
|
+
let resolvedPath = key;
|
|
186
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
187
|
+
|
|
188
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
189
|
+
if (match) {
|
|
190
|
+
const protocolLength = match[0].length;
|
|
191
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
192
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
193
|
+
if (firstSlash > 0) {
|
|
194
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
195
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const command = new GetObjectCommand({
|
|
201
|
+
Bucket: resolvedBucket,
|
|
202
|
+
Key: resolvedPath
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const response = await this.client.send(command);
|
|
206
|
+
|
|
207
|
+
if (!response.Body) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert stream to buffer
|
|
212
|
+
const chunks: Uint8Array[] = [];
|
|
213
|
+
// @ts-ignore - Body is a ReadableStream in Node.js
|
|
214
|
+
for await (const chunk of response.Body) {
|
|
215
|
+
chunks.push(chunk);
|
|
216
|
+
}
|
|
217
|
+
const buffer = Buffer.concat(chunks);
|
|
218
|
+
|
|
219
|
+
const contentType = response.ContentType || "application/octet-stream";
|
|
220
|
+
const fileName = resolvedPath.split("/").pop() || resolvedPath;
|
|
221
|
+
|
|
222
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
223
|
+
return new File([blob], fileName, { type: contentType });
|
|
224
|
+
} catch (error: unknown) {
|
|
225
|
+
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
226
|
+
if (s3Error.name === "NoSuchKey" || s3Error.$metadata?.httpStatusCode === 404) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async deleteObject(key: string, bucket?: string): Promise<void> {
|
|
234
|
+
// Handle s3:// and gs:// URLs
|
|
235
|
+
let resolvedPath = key;
|
|
236
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
237
|
+
|
|
238
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
239
|
+
if (match) {
|
|
240
|
+
const protocolLength = match[0].length;
|
|
241
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
242
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
243
|
+
if (firstSlash > 0) {
|
|
244
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
245
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const command = new DeleteObjectCommand({
|
|
250
|
+
Bucket: resolvedBucket,
|
|
251
|
+
Key: resolvedPath
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await this.client.send(command);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async listObjects(prefix: string, options?: {
|
|
258
|
+
bucket?: string;
|
|
259
|
+
maxResults?: number;
|
|
260
|
+
pageToken?: string;
|
|
261
|
+
}): Promise<StorageListResult> {
|
|
262
|
+
const resolvedBucket = this.getBucket(options?.bucket);
|
|
263
|
+
|
|
264
|
+
const command = new ListObjectsV2Command({
|
|
265
|
+
Bucket: resolvedBucket,
|
|
266
|
+
Prefix: prefix || undefined,
|
|
267
|
+
MaxKeys: options?.maxResults ?? 1000,
|
|
268
|
+
ContinuationToken: options?.pageToken,
|
|
269
|
+
Delimiter: "/" // This gives us folder-like behavior
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const response = await this.client.send(command);
|
|
273
|
+
|
|
274
|
+
const items: StorageReference[] = (response.Contents || []).map(obj => ({
|
|
275
|
+
bucket: resolvedBucket,
|
|
276
|
+
fullPath: obj.Key || "",
|
|
277
|
+
name: (obj.Key || "").split("/").pop() || "",
|
|
278
|
+
parent: null as never,
|
|
279
|
+
root: null as never,
|
|
280
|
+
toString: () => `s3://${resolvedBucket}/${obj.Key}`
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({
|
|
284
|
+
bucket: resolvedBucket,
|
|
285
|
+
fullPath: prefix.Prefix || "",
|
|
286
|
+
name: (prefix.Prefix || "").replace(/\/$/, "").split("/").pop() || "",
|
|
287
|
+
parent: null as never,
|
|
288
|
+
root: null as never,
|
|
289
|
+
toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
items,
|
|
294
|
+
prefixes,
|
|
295
|
+
nextPageToken: response.NextContinuationToken
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage module for Rebase backend
|
|
3
|
+
*
|
|
4
|
+
* Provides pluggable file storage with two built-in providers:
|
|
5
|
+
* - **Local filesystem** — zero config, great for dev and single-server deployments.
|
|
6
|
+
* - **S3-compatible** — works with AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
|
|
7
|
+
* Backblaze B2, DigitalOcean Spaces, and GCS (via S3 interop).
|
|
8
|
+
*
|
|
9
|
+
* For other providers (native GCS SDK, Azure Blob, etc.), implement the
|
|
10
|
+
* `StorageController` interface and pass the instance directly to the `storage` config.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from "./types";
|
|
14
|
+
export { LocalStorageController } from "./LocalStorageController";
|
|
15
|
+
export { S3StorageController } from "./S3StorageController";
|
|
16
|
+
export { createStorageRoutes } from "./routes";
|
|
17
|
+
export type { StorageRoutesConfig } from "./routes";
|
|
18
|
+
export * from "./storage-registry";
|
|
19
|
+
|
|
20
|
+
import { BackendStorageConfig, StorageController } from "./types";
|
|
21
|
+
import { LocalStorageController } from "./LocalStorageController";
|
|
22
|
+
import { S3StorageController } from "./S3StorageController";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a storage controller from a config object.
|
|
26
|
+
*
|
|
27
|
+
* For custom providers, implement `StorageController` directly instead
|
|
28
|
+
* of going through this factory.
|
|
29
|
+
*/
|
|
30
|
+
export function createStorageController(config: BackendStorageConfig): StorageController {
|
|
31
|
+
switch (config.type) {
|
|
32
|
+
case "local":
|
|
33
|
+
return new LocalStorageController(config);
|
|
34
|
+
case "s3":
|
|
35
|
+
return new S3StorageController(config);
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unknown storage type: ${(config as Record<string, unknown>).type}. ` +
|
|
39
|
+
"Built-in types: local, s3. " +
|
|
40
|
+
"For other providers, implement the StorageController interface directly."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|