@memberjunction/storage 5.24.0 → 5.25.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/dist/FileStorageEngine.d.ts +187 -0
- package/dist/FileStorageEngine.d.ts.map +1 -0
- package/dist/FileStorageEngine.js +301 -0
- package/dist/FileStorageEngine.js.map +1 -0
- package/dist/config.d.ts +50 -50
- package/dist/drivers/BoxFileStorage.d.ts.map +1 -1
- package/dist/drivers/BoxFileStorage.js +17 -7
- package/dist/drivers/BoxFileStorage.js.map +1 -1
- package/dist/drivers/DropboxFileStorage.d.ts.map +1 -1
- package/dist/drivers/DropboxFileStorage.js +32 -2
- package/dist/drivers/DropboxFileStorage.js.map +1 -1
- package/dist/drivers/GoogleDriveFileStorage.d.ts.map +1 -1
- package/dist/drivers/GoogleDriveFileStorage.js +15 -4
- package/dist/drivers/GoogleDriveFileStorage.js.map +1 -1
- package/dist/drivers/SharePointFileStorage.d.ts.map +1 -1
- package/dist/drivers/SharePointFileStorage.js +8 -2
- package/dist/drivers/SharePointFileStorage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
3
|
+
import { FileStorageEngineBase, StorageAccountWithProvider, MJFileStorageAccountEntity, MJFileStorageProviderEntity } from '@memberjunction/core-entities';
|
|
4
|
+
import { FileStorageBase } from './generic/FileStorageBase.js';
|
|
5
|
+
/**
|
|
6
|
+
* Options for uploading a file to MJ Storage.
|
|
7
|
+
*/
|
|
8
|
+
export interface UploadFileOptions {
|
|
9
|
+
/** Raw file content as a Buffer */
|
|
10
|
+
content: Buffer;
|
|
11
|
+
/** File name (used for the MJ: Files record and the storage path) */
|
|
12
|
+
fileName: string;
|
|
13
|
+
/** MIME type of the file (e.g., 'application/pdf') */
|
|
14
|
+
mimeType: string;
|
|
15
|
+
/** User context for DB operations and credential access */
|
|
16
|
+
contextUser: UserInfo;
|
|
17
|
+
/**
|
|
18
|
+
* Optional pre-resolved FileStorageAccount ID.
|
|
19
|
+
* When provided, the file is uploaded to this specific account.
|
|
20
|
+
* Otherwise, the first active account is used.
|
|
21
|
+
*/
|
|
22
|
+
storageAccountId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional metadata provider. Defaults to `Metadata.Provider`.
|
|
25
|
+
*/
|
|
26
|
+
provider?: IMetadataProvider;
|
|
27
|
+
/**
|
|
28
|
+
* Optional path prefix within the storage bucket.
|
|
29
|
+
* Defaults to `'artifacts/<date>/<uuid>'`.
|
|
30
|
+
*/
|
|
31
|
+
pathPrefix?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Result returned by {@link FileStorageEngine.UploadFile}.
|
|
35
|
+
*/
|
|
36
|
+
export interface UploadFileResult {
|
|
37
|
+
/** The newly created MJ: Files record ID */
|
|
38
|
+
FileID: string;
|
|
39
|
+
/** The storage path (ProviderKey) where the file was stored */
|
|
40
|
+
StoragePath: string;
|
|
41
|
+
/** The storage account that was used */
|
|
42
|
+
Account: MJFileStorageAccountEntity;
|
|
43
|
+
/** The storage provider that was used */
|
|
44
|
+
Provider: MJFileStorageProviderEntity;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Server-side file storage engine providing high-level operations for uploading,
|
|
48
|
+
* downloading, and managing files in MJ Storage.
|
|
49
|
+
*
|
|
50
|
+
* Follows the containment pattern (like AIEngine wraps AIEngineBase):
|
|
51
|
+
* - Delegates all metadata access to {@link FileStorageEngineBase}
|
|
52
|
+
* - Adds server-side methods: {@link UploadFile}, {@link GetDriver}, {@link ResolveStorageAccount}
|
|
53
|
+
*
|
|
54
|
+
* **Client-side code** should use `FileStorageEngineBase` from `@memberjunction/core-entities`
|
|
55
|
+
* for metadata-only access (accounts, providers, lookups).
|
|
56
|
+
*
|
|
57
|
+
* Usage:
|
|
58
|
+
* ```typescript
|
|
59
|
+
* import { FileStorageEngine } from '@memberjunction/storage';
|
|
60
|
+
*
|
|
61
|
+
* const engine = FileStorageEngine.Instance;
|
|
62
|
+
* await engine.Config(false, contextUser);
|
|
63
|
+
*
|
|
64
|
+
* // Upload a file
|
|
65
|
+
* const result = await engine.UploadFile({
|
|
66
|
+
* content: Buffer.from(base64Data, 'base64'),
|
|
67
|
+
* fileName: 'report.pdf',
|
|
68
|
+
* mimeType: 'application/pdf',
|
|
69
|
+
* contextUser
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* // Get a driver for direct operations
|
|
73
|
+
* const driver = await engine.GetDriver(accountId, contextUser);
|
|
74
|
+
* const objects = await driver.ListObjects('/');
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare class FileStorageEngine extends BaseSingleton<FileStorageEngine> {
|
|
78
|
+
private _loaded;
|
|
79
|
+
private _loading;
|
|
80
|
+
private _loadingPromise;
|
|
81
|
+
private _contextUser;
|
|
82
|
+
private _driverCache;
|
|
83
|
+
/**
|
|
84
|
+
* Returns the global singleton instance.
|
|
85
|
+
*/
|
|
86
|
+
static get Instance(): FileStorageEngine;
|
|
87
|
+
/** Access to the underlying metadata-only engine. */
|
|
88
|
+
protected get Base(): FileStorageEngineBase;
|
|
89
|
+
/** Returns true if the engine has been configured. */
|
|
90
|
+
get Loaded(): boolean;
|
|
91
|
+
/** Gets all file storage accounts (cached). */
|
|
92
|
+
get Accounts(): MJFileStorageAccountEntity[];
|
|
93
|
+
/** Gets all file storage providers (cached). */
|
|
94
|
+
get Providers(): MJFileStorageProviderEntity[];
|
|
95
|
+
/** Gets all storage accounts combined with their provider details (cached). */
|
|
96
|
+
get AccountsWithProviders(): StorageAccountWithProvider[];
|
|
97
|
+
/** Whether any storage accounts are configured. */
|
|
98
|
+
get HasStorageAccounts(): boolean;
|
|
99
|
+
/** Gets a file storage account by its ID. */
|
|
100
|
+
GetAccountById(accountId: string): MJFileStorageAccountEntity | undefined;
|
|
101
|
+
/** Gets a file storage provider by its ID. */
|
|
102
|
+
GetProviderById(providerId: string): MJFileStorageProviderEntity | undefined;
|
|
103
|
+
/** Gets a file storage account by its name (case-insensitive). */
|
|
104
|
+
GetAccountByName(name: string): MJFileStorageAccountEntity | undefined;
|
|
105
|
+
/** Gets file storage accounts linked to a given provider ID. */
|
|
106
|
+
GetAccountsByProviderID(providerId: string): MJFileStorageAccountEntity[];
|
|
107
|
+
/** Gets a storage account with its provider details by account ID. */
|
|
108
|
+
GetAccountWithProvider(accountId: string): StorageAccountWithProvider | null;
|
|
109
|
+
/**
|
|
110
|
+
* Configures the engine by loading the underlying metadata cache and any
|
|
111
|
+
* server-specific state. Safe to call multiple times — uses cached data
|
|
112
|
+
* unless `forceRefresh` is true. Concurrent callers share a single loading
|
|
113
|
+
* promise to avoid redundant work.
|
|
114
|
+
*/
|
|
115
|
+
Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Internal loading logic — separated for clean promise management.
|
|
118
|
+
* First ensures the base metadata cache is loaded, then loads any
|
|
119
|
+
* server-specific state (extensible for future needs).
|
|
120
|
+
*/
|
|
121
|
+
private innerLoad;
|
|
122
|
+
/**
|
|
123
|
+
* Initializes storage drivers for all active accounts and caches them.
|
|
124
|
+
* Called automatically during Config(). Can also be called independently to
|
|
125
|
+
* re-initialize drivers without reloading metadata (e.g., after credential rotation).
|
|
126
|
+
* Accounts that fail to initialize are logged and skipped — they will fall back to
|
|
127
|
+
* on-demand initialization when GetDriver() is called.
|
|
128
|
+
*/
|
|
129
|
+
RefreshDriverCache(): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Resolves a storage account to use for file operations.
|
|
132
|
+
*
|
|
133
|
+
* Resolution logic:
|
|
134
|
+
* 1. If `accountId` is provided, returns that specific account
|
|
135
|
+
* 2. Otherwise, returns the first active account
|
|
136
|
+
* 3. If no active accounts exist, returns the first account regardless of active status
|
|
137
|
+
*
|
|
138
|
+
* @param accountId - Optional explicit account ID
|
|
139
|
+
* @returns The resolved account with provider, or null if no accounts are configured
|
|
140
|
+
*/
|
|
141
|
+
ResolveStorageAccount(accountId?: string): StorageAccountWithProvider | null;
|
|
142
|
+
/**
|
|
143
|
+
* Returns an authenticated storage driver for a given account.
|
|
144
|
+
*
|
|
145
|
+
* Checks the pre-initialized driver cache first (populated during Config()).
|
|
146
|
+
* If the account wasn't cached (e.g., it failed during Config or was added after),
|
|
147
|
+
* falls back to on-demand initialization.
|
|
148
|
+
*
|
|
149
|
+
* This handles:
|
|
150
|
+
* - Looking up the account and provider from cached metadata
|
|
151
|
+
* - Decrypting credentials via the Credential Engine
|
|
152
|
+
* - Setting up OAuth token refresh callbacks for providers like Box
|
|
153
|
+
*
|
|
154
|
+
* @param accountId - The FileStorageAccount ID to get a driver for
|
|
155
|
+
* @param contextUser - User context for credential decryption (used for on-demand init)
|
|
156
|
+
* @returns An initialized, ready-to-use FileStorageBase driver
|
|
157
|
+
* @throws Error if the account is not found or driver initialization fails
|
|
158
|
+
*/
|
|
159
|
+
GetDriver(accountId: string, contextUser: UserInfo): Promise<FileStorageBase>;
|
|
160
|
+
/**
|
|
161
|
+
* Uploads a file to MJ Storage and creates an `MJ: Files` entity record.
|
|
162
|
+
*
|
|
163
|
+
* This is the primary high-level method for storing files. It handles:
|
|
164
|
+
* 1. Resolving which storage account to use
|
|
165
|
+
* 2. Initializing an authenticated driver
|
|
166
|
+
* 3. Uploading the file content
|
|
167
|
+
* 4. Creating the `MJ: Files` database record
|
|
168
|
+
*
|
|
169
|
+
* @param options - Upload options (content, fileName, mimeType, contextUser, etc.)
|
|
170
|
+
* @returns Upload result containing the file ID, storage path, and account/provider used
|
|
171
|
+
* @throws Error if no storage accounts are configured or the upload/save fails
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* const result = await FileStorageEngine.Instance.UploadFile({
|
|
176
|
+
* content: Buffer.from(base64Data, 'base64'),
|
|
177
|
+
* fileName: 'report.pdf',
|
|
178
|
+
* mimeType: 'application/pdf',
|
|
179
|
+
* contextUser,
|
|
180
|
+
* storageAccountId: resolvedAccountId // optional
|
|
181
|
+
* });
|
|
182
|
+
* console.log('Created file:', result.FileID);
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
UploadFile(options: UploadFileOptions): Promise<UploadFileResult>;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=FileStorageEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileStorageEngine.d.ts","sourceRoot":"","sources":["../src/FileStorageEngine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { LogError, Metadata } from '@memberjunction/core';
|
|
2
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
3
|
+
import { FileStorageEngineBase } from '@memberjunction/core-entities';
|
|
4
|
+
import { initializeDriverWithAccountCredentials } from './util.js';
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Engine
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Server-side file storage engine providing high-level operations for uploading,
|
|
10
|
+
* downloading, and managing files in MJ Storage.
|
|
11
|
+
*
|
|
12
|
+
* Follows the containment pattern (like AIEngine wraps AIEngineBase):
|
|
13
|
+
* - Delegates all metadata access to {@link FileStorageEngineBase}
|
|
14
|
+
* - Adds server-side methods: {@link UploadFile}, {@link GetDriver}, {@link ResolveStorageAccount}
|
|
15
|
+
*
|
|
16
|
+
* **Client-side code** should use `FileStorageEngineBase` from `@memberjunction/core-entities`
|
|
17
|
+
* for metadata-only access (accounts, providers, lookups).
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { FileStorageEngine } from '@memberjunction/storage';
|
|
22
|
+
*
|
|
23
|
+
* const engine = FileStorageEngine.Instance;
|
|
24
|
+
* await engine.Config(false, contextUser);
|
|
25
|
+
*
|
|
26
|
+
* // Upload a file
|
|
27
|
+
* const result = await engine.UploadFile({
|
|
28
|
+
* content: Buffer.from(base64Data, 'base64'),
|
|
29
|
+
* fileName: 'report.pdf',
|
|
30
|
+
* mimeType: 'application/pdf',
|
|
31
|
+
* contextUser
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Get a driver for direct operations
|
|
35
|
+
* const driver = await engine.GetDriver(accountId, contextUser);
|
|
36
|
+
* const objects = await driver.ListObjects('/');
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class FileStorageEngine extends BaseSingleton {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(...arguments);
|
|
42
|
+
// Loading state management (mirrors AIEngine pattern)
|
|
43
|
+
this._loaded = false;
|
|
44
|
+
this._loading = false;
|
|
45
|
+
this._loadingPromise = null;
|
|
46
|
+
// Server-specific state: cached, initialized drivers keyed by account ID
|
|
47
|
+
this._driverCache = new Map();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the global singleton instance.
|
|
51
|
+
*/
|
|
52
|
+
static get Instance() {
|
|
53
|
+
return super.getInstance();
|
|
54
|
+
}
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
// Containment — delegate to FileStorageEngineBase
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
58
|
+
/** Access to the underlying metadata-only engine. */
|
|
59
|
+
get Base() {
|
|
60
|
+
return FileStorageEngineBase.Instance;
|
|
61
|
+
}
|
|
62
|
+
/** Returns true if the engine has been configured. */
|
|
63
|
+
get Loaded() {
|
|
64
|
+
return this._loaded && this.Base.Loaded;
|
|
65
|
+
}
|
|
66
|
+
// --- Delegated metadata getters ---
|
|
67
|
+
/** Gets all file storage accounts (cached). */
|
|
68
|
+
get Accounts() {
|
|
69
|
+
return this.Base.Accounts;
|
|
70
|
+
}
|
|
71
|
+
/** Gets all file storage providers (cached). */
|
|
72
|
+
get Providers() {
|
|
73
|
+
return this.Base.Providers;
|
|
74
|
+
}
|
|
75
|
+
/** Gets all storage accounts combined with their provider details (cached). */
|
|
76
|
+
get AccountsWithProviders() {
|
|
77
|
+
return this.Base.AccountsWithProviders;
|
|
78
|
+
}
|
|
79
|
+
/** Whether any storage accounts are configured. */
|
|
80
|
+
get HasStorageAccounts() {
|
|
81
|
+
return this.Base.AccountsWithProviders.length > 0;
|
|
82
|
+
}
|
|
83
|
+
// --- Delegated lookup methods ---
|
|
84
|
+
/** Gets a file storage account by its ID. */
|
|
85
|
+
GetAccountById(accountId) {
|
|
86
|
+
return this.Base.GetAccountById(accountId);
|
|
87
|
+
}
|
|
88
|
+
/** Gets a file storage provider by its ID. */
|
|
89
|
+
GetProviderById(providerId) {
|
|
90
|
+
return this.Base.GetProviderById(providerId);
|
|
91
|
+
}
|
|
92
|
+
/** Gets a file storage account by its name (case-insensitive). */
|
|
93
|
+
GetAccountByName(name) {
|
|
94
|
+
return this.Base.GetAccountByName(name);
|
|
95
|
+
}
|
|
96
|
+
/** Gets file storage accounts linked to a given provider ID. */
|
|
97
|
+
GetAccountsByProviderID(providerId) {
|
|
98
|
+
return this.Base.GetAccountsByProviderID(providerId);
|
|
99
|
+
}
|
|
100
|
+
/** Gets a storage account with its provider details by account ID. */
|
|
101
|
+
GetAccountWithProvider(accountId) {
|
|
102
|
+
return this.Base.GetAccountWithProvider(accountId);
|
|
103
|
+
}
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
105
|
+
// Configuration
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
107
|
+
/**
|
|
108
|
+
* Configures the engine by loading the underlying metadata cache and any
|
|
109
|
+
* server-specific state. Safe to call multiple times — uses cached data
|
|
110
|
+
* unless `forceRefresh` is true. Concurrent callers share a single loading
|
|
111
|
+
* promise to avoid redundant work.
|
|
112
|
+
*/
|
|
113
|
+
async Config(forceRefresh, contextUser, provider) {
|
|
114
|
+
if (this._loaded && !forceRefresh) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// If currently loading, return the existing promise so all callers wait together
|
|
118
|
+
if (this._loading && this._loadingPromise) {
|
|
119
|
+
return this._loadingPromise;
|
|
120
|
+
}
|
|
121
|
+
this._loading = true;
|
|
122
|
+
this._loadingPromise = this.innerLoad(forceRefresh, contextUser, provider);
|
|
123
|
+
try {
|
|
124
|
+
await this._loadingPromise;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this._loading = false;
|
|
128
|
+
this._loadingPromise = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Internal loading logic — separated for clean promise management.
|
|
133
|
+
* First ensures the base metadata cache is loaded, then loads any
|
|
134
|
+
* server-specific state (extensible for future needs).
|
|
135
|
+
*/
|
|
136
|
+
async innerLoad(forceRefresh, contextUser, provider) {
|
|
137
|
+
try {
|
|
138
|
+
this._contextUser = contextUser;
|
|
139
|
+
// Load base metadata (accounts, providers)
|
|
140
|
+
await this.Base.Config(forceRefresh ?? false, contextUser, provider);
|
|
141
|
+
// Initialize drivers for all active accounts and cache them
|
|
142
|
+
await this.RefreshDriverCache();
|
|
143
|
+
this._loaded = true;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
LogError(error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Initializes storage drivers for all active accounts and caches them.
|
|
152
|
+
* Called automatically during Config(). Can also be called independently to
|
|
153
|
+
* re-initialize drivers without reloading metadata (e.g., after credential rotation).
|
|
154
|
+
* Accounts that fail to initialize are logged and skipped — they will fall back to
|
|
155
|
+
* on-demand initialization when GetDriver() is called.
|
|
156
|
+
*/
|
|
157
|
+
async RefreshDriverCache() {
|
|
158
|
+
this._driverCache.clear();
|
|
159
|
+
const activeAccounts = this.Base.AccountsWithProviders.filter(a => a.provider.IsActive !== false);
|
|
160
|
+
if (activeAccounts.length === 0 || !this._contextUser) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const contextUser = this._contextUser;
|
|
164
|
+
const results = await Promise.allSettled(activeAccounts.map(async ({ account, provider: storageProvider }) => {
|
|
165
|
+
const driver = await initializeDriverWithAccountCredentials({
|
|
166
|
+
accountEntity: account,
|
|
167
|
+
providerEntity: storageProvider,
|
|
168
|
+
contextUser
|
|
169
|
+
});
|
|
170
|
+
this._driverCache.set(account.ID, driver);
|
|
171
|
+
}));
|
|
172
|
+
// Log failures but don't throw — failed accounts fall back to on-demand init
|
|
173
|
+
for (let i = 0; i < results.length; i++) {
|
|
174
|
+
if (results[i].status === 'rejected') {
|
|
175
|
+
const accountName = activeAccounts[i].account.Name;
|
|
176
|
+
const reason = results[i].reason;
|
|
177
|
+
LogError(`FileStorageEngine: failed to pre-initialize driver for account "${accountName}": ${reason}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
182
|
+
// Server-side operations
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* Resolves a storage account to use for file operations.
|
|
186
|
+
*
|
|
187
|
+
* Resolution logic:
|
|
188
|
+
* 1. If `accountId` is provided, returns that specific account
|
|
189
|
+
* 2. Otherwise, returns the first active account
|
|
190
|
+
* 3. If no active accounts exist, returns the first account regardless of active status
|
|
191
|
+
*
|
|
192
|
+
* @param accountId - Optional explicit account ID
|
|
193
|
+
* @returns The resolved account with provider, or null if no accounts are configured
|
|
194
|
+
*/
|
|
195
|
+
ResolveStorageAccount(accountId) {
|
|
196
|
+
if (accountId) {
|
|
197
|
+
return this.Base.GetAccountWithProvider(accountId);
|
|
198
|
+
}
|
|
199
|
+
const accounts = this.Base.AccountsWithProviders;
|
|
200
|
+
if (accounts.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
return accounts.find(a => a.provider.IsActive !== false) ?? accounts[0];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Returns an authenticated storage driver for a given account.
|
|
206
|
+
*
|
|
207
|
+
* Checks the pre-initialized driver cache first (populated during Config()).
|
|
208
|
+
* If the account wasn't cached (e.g., it failed during Config or was added after),
|
|
209
|
+
* falls back to on-demand initialization.
|
|
210
|
+
*
|
|
211
|
+
* This handles:
|
|
212
|
+
* - Looking up the account and provider from cached metadata
|
|
213
|
+
* - Decrypting credentials via the Credential Engine
|
|
214
|
+
* - Setting up OAuth token refresh callbacks for providers like Box
|
|
215
|
+
*
|
|
216
|
+
* @param accountId - The FileStorageAccount ID to get a driver for
|
|
217
|
+
* @param contextUser - User context for credential decryption (used for on-demand init)
|
|
218
|
+
* @returns An initialized, ready-to-use FileStorageBase driver
|
|
219
|
+
* @throws Error if the account is not found or driver initialization fails
|
|
220
|
+
*/
|
|
221
|
+
async GetDriver(accountId, contextUser) {
|
|
222
|
+
// Check pre-initialized cache first
|
|
223
|
+
const cached = this._driverCache.get(accountId);
|
|
224
|
+
if (cached) {
|
|
225
|
+
return cached;
|
|
226
|
+
}
|
|
227
|
+
// On-demand fallback: account wasn't in cache (failed init, added late, etc.)
|
|
228
|
+
const resolved = this.Base.GetAccountWithProvider(accountId);
|
|
229
|
+
if (!resolved) {
|
|
230
|
+
throw new Error(`FileStorageEngine.GetDriver: account '${accountId}' not found in cached metadata. Did you call Config() first?`);
|
|
231
|
+
}
|
|
232
|
+
const driver = await initializeDriverWithAccountCredentials({
|
|
233
|
+
accountEntity: resolved.account,
|
|
234
|
+
providerEntity: resolved.provider,
|
|
235
|
+
contextUser
|
|
236
|
+
});
|
|
237
|
+
// Cache it for future calls
|
|
238
|
+
this._driverCache.set(accountId, driver);
|
|
239
|
+
return driver;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Uploads a file to MJ Storage and creates an `MJ: Files` entity record.
|
|
243
|
+
*
|
|
244
|
+
* This is the primary high-level method for storing files. It handles:
|
|
245
|
+
* 1. Resolving which storage account to use
|
|
246
|
+
* 2. Initializing an authenticated driver
|
|
247
|
+
* 3. Uploading the file content
|
|
248
|
+
* 4. Creating the `MJ: Files` database record
|
|
249
|
+
*
|
|
250
|
+
* @param options - Upload options (content, fileName, mimeType, contextUser, etc.)
|
|
251
|
+
* @returns Upload result containing the file ID, storage path, and account/provider used
|
|
252
|
+
* @throws Error if no storage accounts are configured or the upload/save fails
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const result = await FileStorageEngine.Instance.UploadFile({
|
|
257
|
+
* content: Buffer.from(base64Data, 'base64'),
|
|
258
|
+
* fileName: 'report.pdf',
|
|
259
|
+
* mimeType: 'application/pdf',
|
|
260
|
+
* contextUser,
|
|
261
|
+
* storageAccountId: resolvedAccountId // optional
|
|
262
|
+
* });
|
|
263
|
+
* console.log('Created file:', result.FileID);
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
async UploadFile(options) {
|
|
267
|
+
const { content, fileName, mimeType, contextUser, storageAccountId, provider } = options;
|
|
268
|
+
const md = provider ?? Metadata.Provider;
|
|
269
|
+
// 1. Resolve storage account
|
|
270
|
+
const resolved = this.ResolveStorageAccount(storageAccountId);
|
|
271
|
+
if (!resolved) {
|
|
272
|
+
throw new Error('FileStorageEngine.UploadFile: no file storage accounts configured. Cannot upload.');
|
|
273
|
+
}
|
|
274
|
+
// 2. Initialize driver
|
|
275
|
+
const driver = await this.GetDriver(resolved.account.ID, contextUser);
|
|
276
|
+
// 3. Upload
|
|
277
|
+
const pathPrefix = options.pathPrefix ?? `artifacts/${new Date().toISOString().slice(0, 10)}/${crypto.randomUUID()}`;
|
|
278
|
+
const storagePath = `${pathPrefix}/${fileName}`;
|
|
279
|
+
const uploaded = await driver.PutObject(storagePath, content, mimeType);
|
|
280
|
+
if (!uploaded) {
|
|
281
|
+
throw new Error(`FileStorageEngine.UploadFile: PutObject returned false for path '${storagePath}'`);
|
|
282
|
+
}
|
|
283
|
+
// 4. Create MJ: Files record
|
|
284
|
+
const fileEntity = await md.GetEntityObject('MJ: Files', contextUser);
|
|
285
|
+
fileEntity.Name = fileName;
|
|
286
|
+
fileEntity.ContentType = mimeType;
|
|
287
|
+
fileEntity.ProviderID = resolved.provider.ID;
|
|
288
|
+
fileEntity.ProviderKey = storagePath;
|
|
289
|
+
fileEntity.Status = 'Uploaded';
|
|
290
|
+
if (!(await fileEntity.Save())) {
|
|
291
|
+
throw new Error(`FileStorageEngine.UploadFile: failed to save MJ: Files record for '${fileName}'`);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
FileID: fileEntity.ID,
|
|
295
|
+
StoragePath: storagePath,
|
|
296
|
+
Account: resolved.account,
|
|
297
|
+
Provider: resolved.provider
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=FileStorageEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileStorageEngine.js","sourceRoot":"","sources":["../src/FileStorageEngine.ts"],"names":[],"mappings":"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"}
|