@onlineapps/conn-base-storage 1.0.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/API.md +618 -0
- package/README.md +341 -0
- package/SHARED_URL_ADDRESSING.md +258 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +213 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/index.js.html +1579 -0
- package/coverage/internal-url-adapter.js.html +604 -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 +131 -0
- package/coverage/lcov-report/index.js.html +1579 -0
- package/coverage/lcov-report/internal-url-adapter.js.html +604 -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.info +434 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/jest.config.js +13 -0
- package/jest.integration.config.js +9 -0
- package/package.json +33 -0
- package/src/index.js +853 -0
- package/src/internal-url-adapter.js +174 -0
- package/src/sharedUrlAdapter.js +258 -0
- package/test/component/storage.component.test.js +363 -0
- package/test/integration/setup.js +3 -0
- package/test/integration/storage.integration.test.js +224 -0
- package/test/unit/internal-url-adapter.test.js +211 -0
- package/test/unit/legacy.storage.test.js.bak +614 -0
- package/test/unit/storage.extended.unit.test.js +435 -0
- package/test/unit/storage.unit.test.js +373 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal URL Adapter
|
|
3
|
+
* Převádí abstraktní internal:// URL na skutečné endpointy
|
|
4
|
+
* Skrývá implementační detaily před službami
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class InternalUrlAdapter {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.environment = config.environment || process.env.NODE_ENV || 'development';
|
|
10
|
+
this.dockerNetwork = config.dockerNetwork || process.env.DOCKER_NETWORK || false;
|
|
11
|
+
|
|
12
|
+
// Mapování služeb na jejich interní adresy
|
|
13
|
+
this.serviceMap = {
|
|
14
|
+
storage: {
|
|
15
|
+
docker: 'api_services_storage:9000',
|
|
16
|
+
local: 'localhost:9000'
|
|
17
|
+
},
|
|
18
|
+
registry: {
|
|
19
|
+
docker: 'api_registry:8080',
|
|
20
|
+
local: 'localhost:8080'
|
|
21
|
+
},
|
|
22
|
+
monitoring: {
|
|
23
|
+
docker: 'api_monitoring:3000',
|
|
24
|
+
local: 'localhost:3000'
|
|
25
|
+
},
|
|
26
|
+
mq: {
|
|
27
|
+
docker: 'api_services_rabbit:5672',
|
|
28
|
+
local: 'localhost:5672'
|
|
29
|
+
},
|
|
30
|
+
cache: {
|
|
31
|
+
docker: 'api_node_cache:6379',
|
|
32
|
+
local: 'localhost:6379'
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Rozšiřitelné přes config
|
|
37
|
+
if (config.services) {
|
|
38
|
+
Object.assign(this.serviceMap, config.services);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Převede internal:// URL na skutečnou adresu
|
|
44
|
+
* @param {string} internalUrl - URL ve formátu internal://service/path
|
|
45
|
+
* @returns {string} Skutečná HTTP/HTTPS URL
|
|
46
|
+
*/
|
|
47
|
+
resolve(internalUrl) {
|
|
48
|
+
if (!internalUrl || !internalUrl.startsWith('internal://')) {
|
|
49
|
+
return internalUrl; // Není internal URL, vrátíme jak je
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parsování internal://service/path
|
|
53
|
+
const urlParts = internalUrl.replace('internal://', '').split('/');
|
|
54
|
+
const serviceName = urlParts[0];
|
|
55
|
+
const path = urlParts.slice(1).join('/');
|
|
56
|
+
|
|
57
|
+
// Najdeme mapování pro službu
|
|
58
|
+
const serviceConfig = this.serviceMap[serviceName];
|
|
59
|
+
if (!serviceConfig) {
|
|
60
|
+
throw new Error(`Unknown service: ${serviceName}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Určíme endpoint podle prostředí
|
|
64
|
+
const isDocker = this.dockerNetwork || this.isRunningInDocker();
|
|
65
|
+
const endpoint = isDocker ? serviceConfig.docker : serviceConfig.local;
|
|
66
|
+
|
|
67
|
+
// Sestavíme skutečnou URL
|
|
68
|
+
return `http://${endpoint}/${path}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Převede skutečnou URL zpět na internal://
|
|
73
|
+
* @param {string} actualUrl - Skutečná HTTP URL
|
|
74
|
+
* @param {string} serviceName - Název služby
|
|
75
|
+
* @returns {string} Internal URL
|
|
76
|
+
*/
|
|
77
|
+
toInternal(actualUrl, serviceName) {
|
|
78
|
+
if (!actualUrl || !serviceName) {
|
|
79
|
+
return actualUrl;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Odstranit protokol a host
|
|
83
|
+
const url = new URL(actualUrl);
|
|
84
|
+
const path = url.pathname + url.search + url.hash;
|
|
85
|
+
|
|
86
|
+
return `internal://${serviceName}${path}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detekce Docker prostředí
|
|
91
|
+
*/
|
|
92
|
+
isRunningInDocker() {
|
|
93
|
+
// Několik způsobů detekce Docker kontejneru
|
|
94
|
+
return !!(
|
|
95
|
+
process.env.DOCKER_ENV ||
|
|
96
|
+
process.env.DOCKER_CONTAINER ||
|
|
97
|
+
(require('fs').existsSync('/.dockerenv')) ||
|
|
98
|
+
(process.env.HOSTNAME && process.env.HOSTNAME.length === 12) // Docker container ID
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Middleware pro Express - automaticky resolvuje internal:// URL v odpovědích
|
|
104
|
+
*/
|
|
105
|
+
expressMiddleware() {
|
|
106
|
+
return (req, res, next) => {
|
|
107
|
+
const originalJson = res.json;
|
|
108
|
+
|
|
109
|
+
res.json = (data) => {
|
|
110
|
+
// Rekurzivně projít response a nahradit internal:// URL
|
|
111
|
+
const transformed = this.transformResponse(data);
|
|
112
|
+
return originalJson.call(res, transformed);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Rekurzivní transformace odpovědi
|
|
121
|
+
*/
|
|
122
|
+
transformResponse(obj) {
|
|
123
|
+
if (typeof obj === 'string' && obj.startsWith('internal://')) {
|
|
124
|
+
return this.resolve(obj);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(obj)) {
|
|
128
|
+
return obj.map(item => this.transformResponse(item));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (obj && typeof obj === 'object') {
|
|
132
|
+
const transformed = {};
|
|
133
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
134
|
+
transformed[key] = this.transformResponse(value);
|
|
135
|
+
}
|
|
136
|
+
return transformed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return obj;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* HTTP client interceptor - automaticky resolvuje internal:// URL v požadavcích
|
|
144
|
+
*/
|
|
145
|
+
axiosInterceptor(axios) {
|
|
146
|
+
axios.interceptors.request.use((config) => {
|
|
147
|
+
if (config.url && config.url.startsWith('internal://')) {
|
|
148
|
+
config.url = this.resolve(config.url);
|
|
149
|
+
}
|
|
150
|
+
return config;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return axios;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Získat všechny dostupné služby
|
|
158
|
+
*/
|
|
159
|
+
getAvailableServices() {
|
|
160
|
+
return Object.keys(this.serviceMap);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Přidat nebo aktualizovat mapování služby
|
|
165
|
+
*/
|
|
166
|
+
registerService(name, dockerEndpoint, localEndpoint) {
|
|
167
|
+
this.serviceMap[name] = {
|
|
168
|
+
docker: dockerEndpoint,
|
|
169
|
+
local: localEndpoint
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = InternalUrlAdapter;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared URL Adapter
|
|
3
|
+
*
|
|
4
|
+
* Provides unified addressing for backend and frontend using shared:// protocol
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* - shared://registry/service-name/v1.0.0/spec.json
|
|
8
|
+
* - shared://workflow/cookbook-123/step-1.json
|
|
9
|
+
* - shared://storage/files/document.pdf
|
|
10
|
+
*
|
|
11
|
+
* Maps to MinIO buckets:
|
|
12
|
+
* - registry -> registry bucket
|
|
13
|
+
* - workflow -> workflow bucket
|
|
14
|
+
* - storage -> default storage bucket
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class SharedUrlAdapter {
|
|
18
|
+
constructor(storageConnector) {
|
|
19
|
+
this.storage = storageConnector;
|
|
20
|
+
|
|
21
|
+
// Bucket mapping
|
|
22
|
+
this.bucketMap = {
|
|
23
|
+
'registry': 'registry',
|
|
24
|
+
'workflow': 'workflow',
|
|
25
|
+
'storage': 'api-storage',
|
|
26
|
+
'cache': 'cache',
|
|
27
|
+
'logs': 'logs'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse shared:// URL into components
|
|
33
|
+
* @param {string} sharedUrl - URL in format shared://bucket/path
|
|
34
|
+
* @returns {Object} { bucket, path, protocol }
|
|
35
|
+
*/
|
|
36
|
+
parseSharedUrl(sharedUrl) {
|
|
37
|
+
if (!sharedUrl.startsWith('shared://')) {
|
|
38
|
+
throw new Error(`Invalid shared URL format. Must start with 'shared://'. Got: ${sharedUrl}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const urlPart = sharedUrl.substring(9); // Remove 'shared://'
|
|
42
|
+
const firstSlash = urlPart.indexOf('/');
|
|
43
|
+
|
|
44
|
+
if (firstSlash === -1) {
|
|
45
|
+
throw new Error(`Invalid shared URL format. Missing path. Got: ${sharedUrl}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const namespace = urlPart.substring(0, firstSlash);
|
|
49
|
+
const path = urlPart.substring(firstSlash + 1);
|
|
50
|
+
|
|
51
|
+
const bucket = this.bucketMap[namespace];
|
|
52
|
+
if (!bucket) {
|
|
53
|
+
throw new Error(`Unknown namespace '${namespace}'. Available: ${Object.keys(this.bucketMap).join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
protocol: 'shared',
|
|
58
|
+
namespace,
|
|
59
|
+
bucket,
|
|
60
|
+
path,
|
|
61
|
+
originalUrl: sharedUrl
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert shared:// URL to MinIO internal URL
|
|
67
|
+
* @param {string} sharedUrl
|
|
68
|
+
* @returns {string} MinIO URL
|
|
69
|
+
*/
|
|
70
|
+
toMinioUrl(sharedUrl) {
|
|
71
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
72
|
+
return `s3://${bucket}/${path}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert shared:// URL to HTTP URL for external access
|
|
77
|
+
* @param {string} sharedUrl
|
|
78
|
+
* @returns {Promise<string>} Presigned HTTP URL
|
|
79
|
+
*/
|
|
80
|
+
async toHttpUrl(sharedUrl, expiry = 3600) {
|
|
81
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
82
|
+
|
|
83
|
+
if (!this.storage) {
|
|
84
|
+
throw new Error('Storage connector not initialized');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return await this.storage.getPresignedUrl(bucket, path, expiry);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Upload content to shared:// URL
|
|
92
|
+
* @param {string} sharedUrl - Target URL
|
|
93
|
+
* @param {string|Buffer} content - Content to upload
|
|
94
|
+
* @returns {Promise<Object>} Upload result with fingerprint
|
|
95
|
+
*/
|
|
96
|
+
async upload(sharedUrl, content) {
|
|
97
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
98
|
+
|
|
99
|
+
if (!this.storage) {
|
|
100
|
+
throw new Error('Storage connector not initialized');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Ensure bucket exists
|
|
104
|
+
await this.storage.ensureBucket(bucket);
|
|
105
|
+
|
|
106
|
+
// For shared URLs, we want to use the exact path specified
|
|
107
|
+
// So we upload to the specific location instead of using fingerprinted names
|
|
108
|
+
const fingerprint = this.storage.generateFingerprint(content);
|
|
109
|
+
|
|
110
|
+
// Prepare content for upload
|
|
111
|
+
const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
112
|
+
|
|
113
|
+
await this.storage.client.putObject(bucket, path, data, data.length, {
|
|
114
|
+
'Content-Type': path.endsWith('.json') ? 'application/json' : 'application/octet-stream',
|
|
115
|
+
'x-amz-meta-fingerprint': fingerprint
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.storage.logger.info(`Uploaded to shared://${bucket}/${path}`, {
|
|
119
|
+
bucket,
|
|
120
|
+
path,
|
|
121
|
+
fingerprint,
|
|
122
|
+
size: data.length
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
bucket,
|
|
127
|
+
path,
|
|
128
|
+
fingerprint,
|
|
129
|
+
size: data.length,
|
|
130
|
+
sharedUrl
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Download content from shared:// URL
|
|
136
|
+
* @param {string} sharedUrl - Source URL
|
|
137
|
+
* @param {string} [expectedFingerprint] - Optional fingerprint for verification
|
|
138
|
+
* @returns {Promise<string>} Downloaded content
|
|
139
|
+
*/
|
|
140
|
+
async download(sharedUrl, expectedFingerprint = null) {
|
|
141
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
142
|
+
|
|
143
|
+
if (!this.storage) {
|
|
144
|
+
throw new Error('Storage connector not initialized');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (expectedFingerprint) {
|
|
148
|
+
return await this.storage.downloadWithVerification(bucket, path, expectedFingerprint);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Direct download without verification
|
|
152
|
+
const stream = await this.storage.client.getObject(bucket, path);
|
|
153
|
+
const chunks = [];
|
|
154
|
+
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
stream.on('data', chunk => chunks.push(chunk));
|
|
157
|
+
stream.on('error', reject);
|
|
158
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* List objects under shared:// path
|
|
164
|
+
* @param {string} sharedUrl - Base URL to list
|
|
165
|
+
* @returns {Promise<Array>} List of objects
|
|
166
|
+
*/
|
|
167
|
+
async list(sharedUrl) {
|
|
168
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
169
|
+
|
|
170
|
+
if (!this.storage) {
|
|
171
|
+
throw new Error('Storage connector not initialized');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return await this.storage.listWithFingerprints(bucket, path);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Delete object at shared:// URL
|
|
179
|
+
* @param {string} sharedUrl - URL to delete
|
|
180
|
+
* @returns {Promise<boolean>} Success status
|
|
181
|
+
*/
|
|
182
|
+
async delete(sharedUrl) {
|
|
183
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
184
|
+
|
|
185
|
+
if (!this.storage) {
|
|
186
|
+
throw new Error('Storage connector not initialized');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return await this.storage.deleteObject(bucket, path);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if shared:// URL exists
|
|
194
|
+
* @param {string} sharedUrl
|
|
195
|
+
* @returns {Promise<boolean>}
|
|
196
|
+
*/
|
|
197
|
+
async exists(sharedUrl) {
|
|
198
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
199
|
+
|
|
200
|
+
if (!this.storage) {
|
|
201
|
+
throw new Error('Storage connector not initialized');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await this.storage.client.statObject(bucket, path);
|
|
206
|
+
return true;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error.code === 'NotFound') {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create a shared:// URL from components
|
|
217
|
+
* @param {string} namespace - Namespace (registry, workflow, storage)
|
|
218
|
+
* @param {string} path - Path within namespace
|
|
219
|
+
* @returns {string} Shared URL
|
|
220
|
+
*/
|
|
221
|
+
createSharedUrl(namespace, path) {
|
|
222
|
+
if (!this.bucketMap[namespace]) {
|
|
223
|
+
throw new Error(`Unknown namespace '${namespace}'. Available: ${Object.keys(this.bucketMap).join(', ')}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Ensure path doesn't start with /
|
|
227
|
+
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
228
|
+
|
|
229
|
+
return `shared://${namespace}/${cleanPath}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get metadata for shared:// URL
|
|
234
|
+
* @param {string} sharedUrl
|
|
235
|
+
* @returns {Promise<Object>} Object metadata
|
|
236
|
+
*/
|
|
237
|
+
async getMetadata(sharedUrl) {
|
|
238
|
+
const { bucket, path } = this.parseSharedUrl(sharedUrl);
|
|
239
|
+
|
|
240
|
+
if (!this.storage) {
|
|
241
|
+
throw new Error('Storage connector not initialized');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const stat = await this.storage.client.statObject(bucket, path);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
size: stat.size,
|
|
248
|
+
etag: stat.etag,
|
|
249
|
+
lastModified: stat.lastModified,
|
|
250
|
+
metadata: stat.metaData,
|
|
251
|
+
sharedUrl,
|
|
252
|
+
bucket,
|
|
253
|
+
path
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = SharedUrlAdapter;
|