@lilaquadrat/studio 10.0.0-beta.9 → 10.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/eslint.config.js +146 -0
- package/lib/fastify-plugins.d.ts +6 -0
- package/lib/fastify-plugins.js +7 -0
- package/lib/fastify-plugins.js.map +1 -0
- package/lib/helpers.d.ts +4 -2
- package/lib/helpers.js +13 -2
- package/lib/helpers.js.map +1 -1
- package/lib/main.d.ts +1 -3
- package/lib/main.js +7 -3
- package/lib/main.js.map +1 -1
- package/lib/models.d.ts +4 -4
- package/lib/models.js +4 -4
- package/lib/models.js.map +1 -1
- package/lib/services.d.ts +5 -5
- package/lib/services.js +5 -13
- package/lib/services.js.map +1 -1
- package/lib/src/Immutable.class.d.ts +8 -1
- package/lib/src/Immutable.class.js +52 -8
- package/lib/src/Immutable.class.js.map +1 -1
- package/lib/src/ShareClientFactory.class.d.ts +1 -3
- package/lib/src/ShareClientFactory.class.js +1 -9
- package/lib/src/ShareClientFactory.class.js.map +1 -1
- package/lib/src/classes/models.class.js.map +1 -1
- package/lib/src/classes/modelv2.class.d.ts +2 -0
- package/lib/src/classes/modelv2.class.js +1 -1
- package/lib/src/classes/modelv2.class.js.map +1 -1
- package/lib/src/classes/mongo.class.js +4 -14
- package/lib/src/classes/mongo.class.js.map +1 -1
- package/lib/src/functions/handleError.d.ts +2 -3
- package/lib/src/functions/handleError.js +3 -16
- package/lib/src/functions/handleError.js.map +1 -1
- package/lib/src/functions/optionsHelper.d.ts +4 -4
- package/lib/src/functions/optionsHelper.js +5 -4
- package/lib/src/functions/optionsHelper.js.map +1 -1
- package/lib/src/functions/respondCode.d.ts +2 -1
- package/lib/src/functions/respondCode.js +1 -1
- package/lib/src/functions/respondCode.js.map +1 -1
- package/lib/src/helpers/ControllerHelper.d.ts +73 -0
- package/lib/src/helpers/ControllerHelper.js +242 -0
- package/lib/src/helpers/ControllerHelper.js.map +1 -0
- package/lib/src/helpers/EnvMapper.js +1 -0
- package/lib/src/helpers/EnvMapper.js.map +1 -0
- package/lib/src/helpers/auth0config.d.ts +6 -0
- package/lib/src/helpers/auth0config.js +23 -0
- package/lib/src/helpers/auth0config.js.map +1 -0
- package/lib/src/helpers/authPlugin.d.ts +29 -0
- package/lib/src/helpers/authPlugin.js +77 -0
- package/lib/src/helpers/authPlugin.js.map +1 -0
- package/lib/src/helpers/cacheHelper.d.ts +69 -0
- package/lib/src/helpers/cacheHelper.js +235 -0
- package/lib/src/helpers/cacheHelper.js.map +1 -0
- package/lib/src/helpers/createSasToken.d.ts +0 -2
- package/lib/src/helpers/createSasToken.js +35 -32
- package/lib/src/helpers/createSasToken.js.map +1 -1
- package/lib/src/helpers/getSecrets.d.ts +1 -1
- package/lib/src/helpers/getSecrets.js +10 -12
- package/lib/src/helpers/getSecrets.js.map +1 -1
- package/lib/src/helpers/limiterPlugin.d.ts +9 -0
- package/lib/src/helpers/limiterPlugin.js +72 -0
- package/lib/src/helpers/limiterPlugin.js.map +1 -0
- package/lib/src/helpers/loggingPlugin.d.ts +30 -0
- package/lib/src/helpers/loggingPlugin.js +87 -0
- package/lib/src/helpers/loggingPlugin.js.map +1 -0
- package/lib/src/helpers/queryAssertionPlugin.d.ts +3 -0
- package/lib/src/helpers/queryAssertionPlugin.js +20 -0
- package/lib/src/helpers/queryAssertionPlugin.js.map +1 -0
- package/lib/src/helpers/safeObjectId.d.ts +1 -1
- package/lib/src/helpers/safeObjectId.js +5 -1
- package/lib/src/helpers/safeObjectId.js.map +1 -1
- package/lib/src/helpers/storageSdkFactory.d.ts +2 -0
- package/lib/src/helpers/storageSdkFactory.js +11 -0
- package/lib/src/helpers/storageSdkFactory.js.map +1 -0
- package/lib/src/helpers/studioAppPlugin.d.ts +3 -0
- package/lib/src/helpers/studioAppPlugin.js +16 -0
- package/lib/src/helpers/studioAppPlugin.js.map +1 -0
- package/lib/src/logger.js +40 -2
- package/lib/src/logger.js.map +1 -1
- package/lib/src/models/access.model.d.ts +14 -3
- package/lib/src/models/access.model.js +7 -9
- package/lib/src/models/access.model.js.map +1 -1
- package/lib/src/models/customers.model.js +14 -4
- package/lib/src/models/customers.model.js.map +1 -1
- package/lib/src/models/design.model.d.ts +4 -0
- package/lib/src/models/design.model.js +58 -0
- package/lib/src/models/design.model.js.map +1 -0
- package/lib/src/models/domain.model.js +1 -1
- package/lib/src/models/domain.model.js.map +1 -1
- package/lib/src/models/editor.model.js +7 -0
- package/lib/src/models/editor.model.js.map +1 -1
- package/lib/src/models/emailLimit.model.d.ts +4 -0
- package/lib/src/models/emailLimit.model.js +31 -0
- package/lib/src/models/emailLimit.model.js.map +1 -0
- package/lib/src/models/hosting.model.js +1 -3
- package/lib/src/models/hosting.model.js.map +1 -1
- package/lib/src/models/hostingSettings.model.js +6 -4
- package/lib/src/models/hostingSettings.model.js.map +1 -1
- package/lib/src/models/invoice.model.d.ts +4 -0
- package/lib/src/models/invoice.model.js +235 -0
- package/lib/src/models/invoice.model.js.map +1 -0
- package/lib/src/models/mailFrom.model.js +51 -10
- package/lib/src/models/mailFrom.model.js.map +1 -1
- package/lib/src/models/project.model.js +2 -4
- package/lib/src/models/project.model.js.map +1 -1
- package/lib/src/models/publish-method.model.js +79 -430
- package/lib/src/models/publish-method.model.js.map +1 -1
- package/lib/src/models/publish.model.js +6 -0
- package/lib/src/models/publish.model.js.map +1 -1
- package/lib/src/models/storage.model.js +23 -5
- package/lib/src/models/storage.model.js.map +1 -1
- package/lib/src/models/structure.model.js +40 -0
- package/lib/src/models/structure.model.js.map +1 -1
- package/lib/src/models/upload.model.js +38 -2
- package/lib/src/models/upload.model.js.map +1 -1
- package/lib/src/prompts/textGeneration.js +88 -0
- package/lib/src/prompts/textGeneration.js.map +1 -1
- package/lib/src/prompts/textGenerationMulti.js +78 -44
- package/lib/src/prompts/textGenerationMulti.js.map +1 -1
- package/lib/src/services/access.service.d.ts +132 -33
- package/lib/src/services/access.service.js +270 -92
- package/lib/src/services/access.service.js.map +1 -1
- package/lib/src/services/ai.service.d.ts +4 -3
- package/lib/src/services/ai.service.js +22 -29
- package/lib/src/services/ai.service.js.map +1 -1
- package/lib/src/services/auth.service.d.ts +11 -0
- package/lib/src/services/auth.service.js +70 -0
- package/lib/src/services/auth.service.js.map +1 -0
- package/lib/src/services/conf.service.d.ts +3 -31
- package/lib/src/services/conf.service.js +58 -167
- package/lib/src/services/conf.service.js.map +1 -1
- package/lib/src/services/customers.service.d.ts +8 -4
- package/lib/src/services/customers.service.js +34 -7
- package/lib/src/services/customers.service.js.map +1 -1
- package/lib/src/services/designs.service.d.ts +7 -0
- package/lib/src/services/designs.service.js +10 -0
- package/lib/src/services/designs.service.js.map +1 -0
- package/lib/src/services/domains.service.d.ts +18 -84
- package/lib/src/services/domains.service.js +91 -583
- package/lib/src/services/domains.service.js.map +1 -1
- package/lib/src/services/editor.service.d.ts +4 -0
- package/lib/src/services/editor.service.js +28 -0
- package/lib/src/services/editor.service.js.map +1 -1
- package/lib/src/services/emailLimit.service.d.ts +21 -0
- package/lib/src/services/emailLimit.service.js +51 -0
- package/lib/src/services/emailLimit.service.js.map +1 -0
- package/lib/src/services/hosting.service.d.ts +12 -24
- package/lib/src/services/hosting.service.js +32 -122
- package/lib/src/services/hosting.service.js.map +1 -1
- package/lib/src/services/hostingAdmin.service.d.ts +1 -1
- package/lib/src/services/hostingAdmin.service.js +2 -2
- package/lib/src/services/hostingAdmin.service.js.map +1 -1
- package/lib/src/services/import.service.d.ts +6 -22
- package/lib/src/services/import.service.js +63 -65
- package/lib/src/services/import.service.js.map +1 -1
- package/lib/src/services/invoices.service.d.ts +30 -0
- package/lib/src/services/invoices.service.js +265 -0
- package/lib/src/services/invoices.service.js.map +1 -0
- package/lib/src/services/jetstream.service.d.ts +5 -3
- package/lib/src/services/jetstream.service.js +63 -7
- package/lib/src/services/jetstream.service.js.map +1 -1
- package/lib/src/services/listParticipants.service.d.ts +3 -5
- package/lib/src/services/listParticipants.service.js +76 -16
- package/lib/src/services/listParticipants.service.js.map +1 -1
- package/lib/src/services/mailFrom.service.d.ts +14 -1
- package/lib/src/services/mailFrom.service.js +59 -0
- package/lib/src/services/mailFrom.service.js.map +1 -1
- package/lib/src/services/me.service.d.ts +23 -12
- package/lib/src/services/me.service.js +65 -88
- package/lib/src/services/me.service.js.map +1 -1
- package/lib/src/services/publish.service.d.ts +6 -8
- package/lib/src/services/publish.service.js +34 -32
- package/lib/src/services/publish.service.js.map +1 -1
- package/lib/src/services/publishData.service.d.ts +10 -7
- package/lib/src/services/publishData.service.js +32 -75
- package/lib/src/services/publishData.service.js.map +1 -1
- package/lib/src/services/spamAnalasys.service.d.ts +4 -4
- package/lib/src/services/spamAnalasys.service.js +36 -44
- package/lib/src/services/spamAnalasys.service.js.map +1 -1
- package/lib/src/services/storage.service.d.ts +68 -39
- package/lib/src/services/storage.service.js +378 -209
- package/lib/src/services/storage.service.js.map +1 -1
- package/lib/src/services/structures.service.d.ts +8 -1
- package/lib/src/services/structures.service.js +26 -1
- package/lib/src/services/structures.service.js.map +1 -1
- package/lib/src/services/upload.service.d.ts +8 -1
- package/lib/src/services/upload.service.js +76 -3
- package/lib/src/services/upload.service.js.map +1 -1
- package/lib/tests/groupStructuresByModel.spec.d.ts +1 -0
- package/lib/tests/groupStructuresByModel.spec.js +33 -0
- package/lib/tests/groupStructuresByModel.spec.js.map +1 -0
- package/lib/tests/listParticipantsServiceJoin.spec.d.ts +1 -0
- package/lib/tests/listParticipantsServiceJoin.spec.js +151 -0
- package/lib/tests/listParticipantsServiceJoin.spec.js.map +1 -0
- package/lib/tests/storageServiceHandleFile.spec.d.ts +1 -0
- package/lib/tests/storageServiceHandleFile.spec.js +94 -0
- package/lib/tests/storageServiceHandleFile.spec.js.map +1 -0
- package/lib/tests/storageServiceToken.spec.d.ts +1 -0
- package/lib/tests/storageServiceToken.spec.js +104 -0
- package/lib/tests/storageServiceToken.spec.js.map +1 -0
- package/lib/tests/uploadServiceCreate.spec.d.ts +1 -0
- package/lib/tests/uploadServiceCreate.spec.js +81 -0
- package/lib/tests/uploadServiceCreate.spec.js.map +1 -0
- package/package.json +30 -26
- package/lib/src/AzureBlobStorage.share.d.ts +0 -19
- package/lib/src/AzureBlobStorage.share.js +0 -162
- package/lib/src/AzureBlobStorage.share.js.map +0 -1
- package/lib/src/AzureFileStorage.share.d.ts +0 -22
- package/lib/src/AzureFileStorage.share.js +0 -139
- package/lib/src/AzureFileStorage.share.js.map +0 -1
- package/lib/src/AzureVault.d.ts +0 -14
- package/lib/src/AzureVault.js +0 -28
- package/lib/src/AzureVault.js.map +0 -1
- package/lib/src/dns.challenge.class.d.ts +0 -17
- package/lib/src/dns.challenge.class.js +0 -41
- package/lib/src/dns.challenge.class.js.map +0 -1
- package/lib/src/http.challenge.class.d.ts +0 -33
- package/lib/src/http.challenge.class.js +0 -58
- package/lib/src/http.challenge.class.js.map +0 -1
- package/lib/src/models/certificate-action.model.d.ts +0 -5
- package/lib/src/models/certificate-action.model.js +0 -230
- package/lib/src/models/certificate-action.model.js.map +0 -1
- package/lib/src/models/certificate.model.d.ts +0 -4
- package/lib/src/models/certificate.model.js +0 -96
- package/lib/src/models/certificate.model.js.map +0 -1
- package/lib/src/models/editorBase.model.d.ts +0 -4
- package/lib/src/models/editorBase.model.js +0 -39
- package/lib/src/models/editorBase.model.js.map +0 -1
- package/lib/src/services/certificates.service.js +0 -199
- package/lib/src/services/certificates.service.js.map +0 -1
- package/lib/src/services/certificatesAction.service.d.ts +0 -0
- package/lib/src/services/certificatesAction.service.js +0 -237
- package/lib/src/services/certificatesAction.service.js.map +0 -1
- package/lib/src/services/editorBase.service.d.ts +0 -46
- package/lib/src/services/editorBase.service.js +0 -161
- package/lib/src/services/editorBase.service.js.map +0 -1
- package/lib/src/services/handleFile.service.d.ts +0 -9
- package/lib/src/services/handleFile.service.js +0 -45
- package/lib/src/services/handleFile.service.js.map +0 -1
- package/lib/src/services/media.service.d.ts +0 -35
- package/lib/src/services/media.service.js +0 -418
- package/lib/src/services/media.service.js.map +0 -1
- package/lib/src/services/share.service.d.ts +0 -6
- package/lib/src/services/share.service.js +0 -4
- package/lib/src/services/share.service.js.map +0 -1
- /package/lib/src/{services/certificates.service.d.ts → helpers/EnvMapper.d.ts} +0 -0
|
@@ -8,31 +8,10 @@ import StorageModel from '../models/storage.model.js';
|
|
|
8
8
|
import JetStreamService from './jetstream.service.js';
|
|
9
9
|
import UploadService from './upload.service.js';
|
|
10
10
|
import filenameHelper from '../helpers/filenameHelper.js';
|
|
11
|
+
import jetstreamService from './jetstream.service.js';
|
|
11
12
|
export class StorageService extends Immutable {
|
|
12
13
|
constructor(options) {
|
|
13
14
|
super();
|
|
14
|
-
this.buckets = {
|
|
15
|
-
cdn: {
|
|
16
|
-
public: true,
|
|
17
|
-
overwrite: false,
|
|
18
|
-
cache: 31536000,
|
|
19
|
-
},
|
|
20
|
-
hosting: {
|
|
21
|
-
public: false,
|
|
22
|
-
overwrite: true,
|
|
23
|
-
cache: 0,
|
|
24
|
-
},
|
|
25
|
-
secure: {
|
|
26
|
-
public: false,
|
|
27
|
-
overwrite: false,
|
|
28
|
-
cache: 31536000,
|
|
29
|
-
},
|
|
30
|
-
internal: {
|
|
31
|
-
public: false,
|
|
32
|
-
overwrite: false,
|
|
33
|
-
cache: 31536000,
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
15
|
this.model = StorageModel;
|
|
37
16
|
this.options = options;
|
|
38
17
|
}
|
|
@@ -42,40 +21,48 @@ export class StorageService extends Immutable {
|
|
|
42
21
|
* The token is cryptographically signed using HMAC-SHA256 and base64url-encoded to be
|
|
43
22
|
* non-human readable and URL-safe.
|
|
44
23
|
*
|
|
45
|
-
* @param fileId - The file identifier (ObjectId or string) to grant access to
|
|
46
24
|
* @param expiresIn - Time in seconds until the token expires (default: 3600 = 1 hour)
|
|
47
25
|
* @param limits - Optional restrictions to apply to the token
|
|
26
|
+
* @param limits.fileId - Restrict token to specific file (storage _id)
|
|
48
27
|
* @param limits.company - Restrict token to specific company
|
|
49
28
|
* @param limits.project - Restrict token to specific project
|
|
50
29
|
* @param limits.app - Restrict token to specific app
|
|
51
30
|
* @param limits.url - Restrict token to specific URL path prefix
|
|
52
31
|
* @param limits.prefix - Restrict token to specific file prefix
|
|
32
|
+
* @param limits.customer - Restrict token to specific customer
|
|
33
|
+
* @param limits.list - Restrict token to specific list
|
|
53
34
|
*
|
|
54
|
-
* @returns Base64url-encoded token containing:
|
|
35
|
+
* @returns Base64url-encoded token containing: expires[:limits]:signature
|
|
55
36
|
*
|
|
56
37
|
* @example
|
|
57
38
|
* // Generate token valid for 1 hour with no restrictions
|
|
58
|
-
* const { token } = getSecureToken(
|
|
39
|
+
* const { token } = getSecureToken(3600);
|
|
59
40
|
*
|
|
60
41
|
* @example
|
|
61
42
|
* // Generate token limited to specific company and project
|
|
62
|
-
* const { token } = getSecureToken(
|
|
43
|
+
* const { token } = getSecureToken(3600, {
|
|
63
44
|
* company: 'acme',
|
|
64
45
|
* project: 'website'
|
|
65
46
|
* });
|
|
66
47
|
*
|
|
67
48
|
* @example
|
|
49
|
+
* // Generate token limited to specific file
|
|
50
|
+
* const { token } = getSecureToken(3600, {
|
|
51
|
+
* fileId: '507f1f77bcf86cd799439011'
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
68
55
|
* // Generate token limited to specific URL path
|
|
69
|
-
* const { token } = getSecureToken(
|
|
56
|
+
* const { token } = getSecureToken(3600, {
|
|
70
57
|
* url: '/company/project/files/'
|
|
71
58
|
* });
|
|
72
59
|
*/
|
|
73
|
-
getSecureToken(
|
|
60
|
+
getSecureToken(expiresIn = 3600, limits) {
|
|
74
61
|
const secret = process.env.SERVER_SECRET || 'secret';
|
|
75
62
|
const now = Math.floor(Date.now() / 1000);
|
|
76
63
|
const expires = now + expiresIn;
|
|
77
|
-
// Build payload:
|
|
78
|
-
let payload = `${
|
|
64
|
+
// Build payload: expires[:base64(limits)]
|
|
65
|
+
let payload = `${expires}`;
|
|
79
66
|
if (limits && Object.keys(limits).length > 0) {
|
|
80
67
|
// Base64 encode the JSON to avoid : separator conflicts
|
|
81
68
|
const limitsJson = JSON.stringify(limits);
|
|
@@ -131,57 +118,101 @@ export class StorageService extends Immutable {
|
|
|
131
118
|
decoded = Buffer.from(token, 'base64url').toString('utf-8');
|
|
132
119
|
}
|
|
133
120
|
catch {
|
|
121
|
+
console.debug('[validateSecureToken] failed to decode token from base64url');
|
|
134
122
|
return false;
|
|
135
123
|
}
|
|
136
124
|
// Extract payload and signature (signature is after last colon)
|
|
137
125
|
const lastColonIndex = decoded.lastIndexOf(':');
|
|
138
|
-
if (lastColonIndex === -1)
|
|
126
|
+
if (lastColonIndex === -1) {
|
|
127
|
+
console.debug('[validateSecureToken] decoded payload has no colon separator');
|
|
139
128
|
return false;
|
|
129
|
+
}
|
|
140
130
|
const payload = decoded.substring(0, lastColonIndex);
|
|
141
131
|
const signature = decoded.substring(lastColonIndex + 1);
|
|
142
132
|
// Verify signature matches expected HMAC
|
|
143
133
|
const expectedSignature = createHmac('sha256', secret).update(payload).digest('hex');
|
|
144
|
-
if (signature !== expectedSignature)
|
|
134
|
+
if (signature !== expectedSignature) {
|
|
135
|
+
console.debug('[validateSecureToken] signature mismatch', {
|
|
136
|
+
expected: expectedSignature.substring(0, 8) + '...',
|
|
137
|
+
received: signature.substring(0, 8) + '...',
|
|
138
|
+
});
|
|
145
139
|
return false;
|
|
146
|
-
|
|
140
|
+
}
|
|
141
|
+
// Parse payload structure: expires[:base64(limits)]
|
|
147
142
|
const parts = payload.split(':');
|
|
148
|
-
// parts[0] is
|
|
149
|
-
// parts[1] is
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
// parts[0] is expires timestamp
|
|
144
|
+
// parts[1] is optional base64-encoded JSON limits object
|
|
145
|
+
if (parts.length < 1) {
|
|
146
|
+
console.debug('[validateSecureToken] payload is empty', { parts });
|
|
152
147
|
return false;
|
|
148
|
+
}
|
|
153
149
|
// Check if token has expired
|
|
154
|
-
const expires = parseInt(parts[
|
|
155
|
-
if (
|
|
150
|
+
const expires = parseInt(parts[0], 10);
|
|
151
|
+
if (Number.isNaN(expires)) {
|
|
152
|
+
console.debug('[validateSecureToken] expires is not a number', { parts });
|
|
156
153
|
return false;
|
|
154
|
+
}
|
|
155
|
+
if (Date.now() / 1000 > expires) {
|
|
156
|
+
console.debug('[validateSecureToken] token expired', {
|
|
157
|
+
expires: new Date(expires * 1000).toISOString(),
|
|
158
|
+
now: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
157
162
|
// If token has limits, validate against checkLimits
|
|
158
|
-
if (parts.length >
|
|
163
|
+
if (parts.length > 1) {
|
|
159
164
|
let tokenLimits;
|
|
160
165
|
try {
|
|
161
166
|
// Decode from base64 first, then parse JSON
|
|
162
|
-
const limitsJson = Buffer.from(parts[
|
|
167
|
+
const limitsJson = Buffer.from(parts[1], 'base64').toString('utf-8');
|
|
163
168
|
tokenLimits = JSON.parse(limitsJson);
|
|
164
169
|
}
|
|
165
170
|
catch {
|
|
171
|
+
console.debug('[validateSecureToken] failed to decode or parse token limits');
|
|
166
172
|
return false;
|
|
167
173
|
}
|
|
168
174
|
// Token has limits but no values provided to check
|
|
169
|
-
if (!checkLimits)
|
|
175
|
+
if (!checkLimits) {
|
|
176
|
+
console.debug('[validateSecureToken] token has limits but no checkLimits provided', { tokenLimits });
|
|
170
177
|
return false;
|
|
178
|
+
}
|
|
171
179
|
// Validate each limit field if present in token
|
|
172
|
-
if (tokenLimits.
|
|
180
|
+
if (tokenLimits.fileId && tokenLimits.fileId !== checkLimits.fileId) {
|
|
181
|
+
console.debug('[validateSecureToken] fileId mismatch', { token: tokenLimits.fileId, provided: checkLimits.fileId });
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (tokenLimits.company && tokenLimits.company !== checkLimits.company) {
|
|
185
|
+
console.debug('[validateSecureToken] company mismatch', { token: tokenLimits.company, provided: checkLimits.company });
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (tokenLimits.project && tokenLimits.project !== checkLimits.project) {
|
|
189
|
+
console.debug('[validateSecureToken] project mismatch', { token: tokenLimits.project, provided: checkLimits.project });
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (tokenLimits.app && tokenLimits.app !== checkLimits.app) {
|
|
193
|
+
console.debug('[validateSecureToken] app mismatch', { token: tokenLimits.app, provided: checkLimits.app });
|
|
173
194
|
return false;
|
|
174
|
-
|
|
195
|
+
}
|
|
196
|
+
if (tokenLimits.prefix && tokenLimits.prefix !== checkLimits.prefix) {
|
|
197
|
+
console.debug('[validateSecureToken] prefix mismatch', { token: tokenLimits.prefix, provided: checkLimits.prefix });
|
|
175
198
|
return false;
|
|
176
|
-
|
|
199
|
+
}
|
|
200
|
+
if (tokenLimits.customer && tokenLimits.customer !== checkLimits.customer) {
|
|
201
|
+
console.debug('[validateSecureToken] customer mismatch', { token: tokenLimits.customer, provided: checkLimits.customer });
|
|
177
202
|
return false;
|
|
178
|
-
|
|
203
|
+
}
|
|
204
|
+
if (tokenLimits.list && tokenLimits.list !== checkLimits.list) {
|
|
205
|
+
console.debug('[validateSecureToken] list mismatch', { token: tokenLimits.list, provided: checkLimits.list });
|
|
179
206
|
return false;
|
|
207
|
+
}
|
|
180
208
|
// For URL limit, check if requested URL starts with the token's allowed prefix
|
|
181
|
-
if (tokenLimits.url && checkLimits.url && !checkLimits.url.startsWith(tokenLimits.url))
|
|
209
|
+
if (tokenLimits.url && checkLimits.url && !checkLimits.url.startsWith(tokenLimits.url)) {
|
|
210
|
+
console.debug('[validateSecureToken] url mismatch', { tokenUrl: tokenLimits.url, providedUrl: checkLimits.url });
|
|
182
211
|
return false;
|
|
212
|
+
}
|
|
183
213
|
}
|
|
184
214
|
else if (checkLimits) {
|
|
215
|
+
console.debug('[validateSecureToken] checkLimits provided but token has no limits');
|
|
185
216
|
return false;
|
|
186
217
|
}
|
|
187
218
|
return true;
|
|
@@ -211,6 +242,7 @@ export class StorageService extends Immutable {
|
|
|
211
242
|
throw new Error('STORAGE_FILE_NOT_FOUND');
|
|
212
243
|
// Merge provided limits with storage document context
|
|
213
244
|
const tokenLimits = {
|
|
245
|
+
fileId: fileId.toString(),
|
|
214
246
|
company: limits?.company || storage.company,
|
|
215
247
|
project: limits?.project || storage.project,
|
|
216
248
|
app: limits?.app || storage.app,
|
|
@@ -218,7 +250,7 @@ export class StorageService extends Immutable {
|
|
|
218
250
|
url: limits?.url,
|
|
219
251
|
};
|
|
220
252
|
// Generate secure token
|
|
221
|
-
const tokenData = this.getSecureToken(
|
|
253
|
+
const tokenData = this.getSecureToken(expiresIn, tokenLimits);
|
|
222
254
|
// Construct download URL
|
|
223
255
|
const baseUrl = process.env.SECURE_URL;
|
|
224
256
|
const path = storage.prefix ? `${storage.prefix}/${storage.filename}` : storage.filename;
|
|
@@ -246,6 +278,17 @@ export class StorageService extends Immutable {
|
|
|
246
278
|
await fs.promises.unlink(path.file);
|
|
247
279
|
throw new Error('FILE_SIZE');
|
|
248
280
|
}
|
|
281
|
+
if (uploadFile.size) {
|
|
282
|
+
const chunkStat = await fs.promises.stat(path.file);
|
|
283
|
+
const existingSize = uploadFile.paths?.length
|
|
284
|
+
? (await Promise.all(uploadFile.paths.map((p) => fs.promises.stat(p.path).then((s) => s.size).catch(() => 0))))
|
|
285
|
+
.reduce((sum, s) => sum + s, 0)
|
|
286
|
+
: 0;
|
|
287
|
+
if (existingSize + chunkStat.size > uploadFile.size) {
|
|
288
|
+
await fs.promises.unlink(path.file);
|
|
289
|
+
throw new Error('FILE_SIZE');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
249
292
|
const updloadState = await UploadService.addPathToUpload(uploadInternalId, index, path.file);
|
|
250
293
|
/**
|
|
251
294
|
* if all chunks are there, complete the file and the upload
|
|
@@ -272,10 +315,13 @@ export class StorageService extends Immutable {
|
|
|
272
315
|
await this.concatenateFilesWithCat(data, pathAndFile.file);
|
|
273
316
|
}
|
|
274
317
|
}
|
|
275
|
-
else {
|
|
318
|
+
else if (type !== 'copy') {
|
|
276
319
|
const writeStream = fs.createWriteStream(pathAndFile.file);
|
|
277
320
|
await pipeline(data, writeStream);
|
|
278
321
|
}
|
|
322
|
+
else {
|
|
323
|
+
await fs.promises.copyFile(data, pathAndFile.file);
|
|
324
|
+
}
|
|
279
325
|
return pathAndFile;
|
|
280
326
|
}
|
|
281
327
|
async writeFileVersion(data, internalId, version) {
|
|
@@ -369,9 +415,7 @@ export class StorageService extends Immutable {
|
|
|
369
415
|
catch (error) {
|
|
370
416
|
if (error.code === 'ENOENT') {
|
|
371
417
|
console.warn(`File not found: ${fullPath}`);
|
|
372
|
-
return;
|
|
373
418
|
}
|
|
374
|
-
throw error;
|
|
375
419
|
}
|
|
376
420
|
}
|
|
377
421
|
/**
|
|
@@ -399,12 +443,6 @@ export class StorageService extends Immutable {
|
|
|
399
443
|
const file = [baseFolder, filename].join('/');
|
|
400
444
|
return { dir: baseFolder, file, relativePath: file };
|
|
401
445
|
}
|
|
402
|
-
// async createUpload(filename: string, size: number, chunks: number, mimetype: string, options: UserAppOptionsRequired) {
|
|
403
|
-
// const useFilename = this.sanitizeName(filename);
|
|
404
|
-
// if (await this.exists(options.company, options.project, useFilename)) throw new Error('FILE_EXISTS');
|
|
405
|
-
// if (await UploadService.uploadExists(options.company, options.project, useFilename)) throw new Error('UPLOAD_EXISTS');
|
|
406
|
-
// return UploadService.createUpload(useFilename, size, chunks, mimetype, options);
|
|
407
|
-
// }
|
|
408
446
|
static sanitizeName(name) {
|
|
409
447
|
return name.replace(/[^a-z0-9-_.]/gi, '').toLowerCase();
|
|
410
448
|
}
|
|
@@ -426,10 +464,17 @@ export class StorageService extends Immutable {
|
|
|
426
464
|
}
|
|
427
465
|
return useMetadata;
|
|
428
466
|
}
|
|
429
|
-
get(
|
|
467
|
+
async get(select, limit) {
|
|
468
|
+
const { company, project, app, list, customer, assetId, query } = select;
|
|
430
469
|
const realQuery = { company, project, app, parent: { $exists: false } };
|
|
470
|
+
if (list)
|
|
471
|
+
realQuery.list = list;
|
|
472
|
+
if (customer)
|
|
473
|
+
realQuery.customer = customer;
|
|
474
|
+
if (assetId?.length)
|
|
475
|
+
realQuery.assetId = { $in: assetId };
|
|
431
476
|
let tags;
|
|
432
|
-
if (query
|
|
477
|
+
if (query?.tags) {
|
|
433
478
|
if (!Array.isArray(query.tags)) {
|
|
434
479
|
tags = [query.tags];
|
|
435
480
|
}
|
|
@@ -438,15 +483,34 @@ export class StorageService extends Immutable {
|
|
|
438
483
|
}
|
|
439
484
|
realQuery['metadata.tags'] = { $in: tags };
|
|
440
485
|
}
|
|
441
|
-
if (query
|
|
486
|
+
if (query?.search) {
|
|
442
487
|
realQuery.filename = { $regex: query.search, $options: 'i' };
|
|
443
488
|
}
|
|
489
|
+
const structureLookupStages = (assetId?.length || query?.populateAssetId)
|
|
490
|
+
? [
|
|
491
|
+
{ $lookup: { from: 'structures', localField: 'assetId', foreignField: 'assetId', as: '_structure' } },
|
|
492
|
+
{ $unwind: { path: '$_structure', preserveNullAndEmptyArrays: true } },
|
|
493
|
+
{ $addFields: { assetId: { id: '$assetId', title: { $ifNull: ['$_structure.title', '$_structure.question', ''] } } } },
|
|
494
|
+
{ $project: { _structure: 0 } },
|
|
495
|
+
]
|
|
496
|
+
: [];
|
|
497
|
+
let result;
|
|
444
498
|
// If ignorePrefix is given, return all matching files without prefix filtering
|
|
445
|
-
if (query
|
|
446
|
-
|
|
499
|
+
if (query?.ignorePrefix) {
|
|
500
|
+
// If prefix is provided with ignorePrefix, match all files where prefix starts with the given value
|
|
501
|
+
if (query?.prefix) {
|
|
502
|
+
const prefixWithSlash = query.prefix.endsWith('/') ? query.prefix : `${query.prefix}/`;
|
|
503
|
+
const escapedPrefix = prefixWithSlash.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
504
|
+
realQuery.$or = [
|
|
505
|
+
{ prefix: query.prefix }, // Exact match
|
|
506
|
+
{ prefix: { $regex: `^${escapedPrefix}` } }, // Starts with prefix/
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
result = await StorageModel.db.aggregate([
|
|
447
510
|
{
|
|
448
511
|
$match: realQuery,
|
|
449
512
|
},
|
|
513
|
+
...structureLookupStages,
|
|
450
514
|
{
|
|
451
515
|
$facet: {
|
|
452
516
|
metadata: [{ $count: 'all' }],
|
|
@@ -454,7 +518,7 @@ export class StorageService extends Immutable {
|
|
|
454
518
|
{ $sort: { _id: -1 } },
|
|
455
519
|
{ $skip: limit?.skip || 0 },
|
|
456
520
|
{ $limit: limit?.limit || 50 },
|
|
457
|
-
{ $project: { path: 0
|
|
521
|
+
{ $project: { path: 0 } },
|
|
458
522
|
{ $addFields: { type: 'file' } },
|
|
459
523
|
],
|
|
460
524
|
},
|
|
@@ -468,11 +532,11 @@ export class StorageService extends Immutable {
|
|
|
468
532
|
},
|
|
469
533
|
])
|
|
470
534
|
.toArray()
|
|
471
|
-
.then((
|
|
535
|
+
.then((r) => r[0] || { all: 0, data: [], count: 0 });
|
|
472
536
|
}
|
|
473
537
|
// If search or tags are given, show only files at the current level (no folders)
|
|
474
|
-
if (query
|
|
475
|
-
const isRootLevel = query
|
|
538
|
+
if (!result && (query?.search || query?.tags)) {
|
|
539
|
+
const isRootLevel = query?.prefix === undefined || query?.prefix === '';
|
|
476
540
|
if (isRootLevel) {
|
|
477
541
|
// Root level: only files without prefix
|
|
478
542
|
realQuery.$or = [
|
|
@@ -485,10 +549,11 @@ export class StorageService extends Immutable {
|
|
|
485
549
|
// Non-root: only files with exact prefix match
|
|
486
550
|
realQuery.prefix = query.prefix;
|
|
487
551
|
}
|
|
488
|
-
|
|
552
|
+
result = await StorageModel.db.aggregate([
|
|
489
553
|
{
|
|
490
554
|
$match: realQuery,
|
|
491
555
|
},
|
|
556
|
+
...structureLookupStages,
|
|
492
557
|
{
|
|
493
558
|
$facet: {
|
|
494
559
|
metadata: [{ $count: 'all' }],
|
|
@@ -496,7 +561,7 @@ export class StorageService extends Immutable {
|
|
|
496
561
|
{ $sort: { _id: -1 } },
|
|
497
562
|
{ $skip: limit?.skip || 0 },
|
|
498
563
|
{ $limit: limit?.limit || 50 },
|
|
499
|
-
{ $project: { path: 0
|
|
564
|
+
{ $project: { path: 0 } },
|
|
500
565
|
{ $addFields: { type: 'file' } },
|
|
501
566
|
],
|
|
502
567
|
},
|
|
@@ -510,151 +575,213 @@ export class StorageService extends Immutable {
|
|
|
510
575
|
},
|
|
511
576
|
])
|
|
512
577
|
.toArray()
|
|
513
|
-
.then((
|
|
578
|
+
.then((r) => r[0] || { all: 0, data: [], count: 0 });
|
|
514
579
|
}
|
|
515
580
|
// Treat undefined prefix same as empty string (root level)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
|
|
581
|
+
if (!result) {
|
|
582
|
+
const isRootLevel = query?.prefix === undefined || query?.prefix === '';
|
|
583
|
+
if (isRootLevel) {
|
|
584
|
+
// Root level: show files without prefix and extract virtual folders
|
|
585
|
+
result = await StorageModel.db.aggregate([
|
|
586
|
+
{
|
|
587
|
+
$match: realQuery,
|
|
588
|
+
},
|
|
589
|
+
...structureLookupStages,
|
|
590
|
+
{
|
|
591
|
+
$addFields: {
|
|
592
|
+
// Extract first segment of prefix for root-level folders
|
|
593
|
+
immediateSubfolder: {
|
|
594
|
+
$cond: {
|
|
595
|
+
if: {
|
|
596
|
+
$or: [
|
|
597
|
+
{ $eq: ['$prefix', null] },
|
|
598
|
+
{ $eq: ['$prefix', ''] },
|
|
599
|
+
{ $eq: [{ $type: '$prefix' }, 'missing'] },
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
then: null, // File at root level (no prefix)
|
|
603
|
+
else: {
|
|
604
|
+
// Extract first segment of prefix
|
|
605
|
+
$arrayElemAt: [{ $split: ['$prefix', '/'] }, 0],
|
|
606
|
+
},
|
|
539
607
|
},
|
|
540
608
|
},
|
|
541
609
|
},
|
|
542
610
|
},
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
611
|
+
{
|
|
612
|
+
$facet: {
|
|
613
|
+
files: [
|
|
614
|
+
{ $match: { immediateSubfolder: null } }, // Files at root level
|
|
615
|
+
{ $sort: { _id: -1 } },
|
|
616
|
+
{ $skip: limit?.skip || 0 },
|
|
617
|
+
{ $limit: limit?.limit || 50 },
|
|
618
|
+
{ $project: { path: 0, immediateSubfolder: 0 } },
|
|
619
|
+
{ $addFields: { type: 'file' } },
|
|
620
|
+
],
|
|
621
|
+
folders: [
|
|
622
|
+
{ $match: { immediateSubfolder: { $ne: null } } },
|
|
623
|
+
{ $group: { _id: '$immediateSubfolder' } }, // Unique folder names
|
|
624
|
+
{ $sort: { _id: 1 } },
|
|
625
|
+
{
|
|
626
|
+
$project: {
|
|
627
|
+
_id: 0,
|
|
628
|
+
prefix: '$_id',
|
|
629
|
+
name: '$_id',
|
|
630
|
+
type: { $literal: 'folder' },
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
metadata: [
|
|
635
|
+
{ $match: { immediateSubfolder: null } },
|
|
636
|
+
{ $count: 'all' },
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
$project: {
|
|
642
|
+
all: { $arrayElemAt: ['$metadata.all', 0] },
|
|
643
|
+
data: { $concatArrays: ['$folders', '$files'] },
|
|
644
|
+
count: { $add: [{ $size: '$folders' }, { $size: '$files' }] },
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
])
|
|
648
|
+
.toArray()
|
|
649
|
+
.then((r) => r[0] || { all: 0, data: [], count: 0 });
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
// Non-root: show exact prefix match and extract subfolders
|
|
653
|
+
const prefix = query.prefix;
|
|
654
|
+
const prefixWithSlash = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
655
|
+
const escapedPrefix = prefixWithSlash.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
656
|
+
// Build non-root query by adding prefix conditions to realQuery
|
|
657
|
+
const nonRootQuery = {
|
|
658
|
+
...realQuery,
|
|
659
|
+
$or: [
|
|
660
|
+
{ prefix }, // Exact match files
|
|
661
|
+
{ prefix: { $regex: `^${escapedPrefix}` } }, // Deeper files for folder extraction
|
|
662
|
+
],
|
|
663
|
+
};
|
|
664
|
+
result = await StorageModel.db.aggregate([
|
|
665
|
+
{
|
|
666
|
+
$match: nonRootQuery,
|
|
667
|
+
},
|
|
668
|
+
...structureLookupStages,
|
|
669
|
+
{
|
|
670
|
+
$addFields: {
|
|
671
|
+
// Extract immediate subfolder name after current prefix
|
|
672
|
+
immediateSubfolder: {
|
|
673
|
+
$cond: {
|
|
674
|
+
if: { $eq: ['$prefix', prefix] },
|
|
675
|
+
then: null, // File at exact level
|
|
676
|
+
else: {
|
|
677
|
+
// Extract first segment after prefix
|
|
678
|
+
$arrayElemAt: [
|
|
679
|
+
{ $split: [{ $substr: ['$prefix', prefixWithSlash.length, -1] }, '/'] },
|
|
680
|
+
0,
|
|
681
|
+
],
|
|
682
|
+
},
|
|
564
683
|
},
|
|
565
684
|
},
|
|
566
|
-
|
|
567
|
-
metadata: [
|
|
568
|
-
{ $match: { immediateSubfolder: null } },
|
|
569
|
-
{ $count: 'all' },
|
|
570
|
-
],
|
|
685
|
+
},
|
|
571
686
|
},
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
687
|
+
{
|
|
688
|
+
$facet: {
|
|
689
|
+
files: [
|
|
690
|
+
{ $match: { immediateSubfolder: null } }, // Files at exact level
|
|
691
|
+
{ $sort: { _id: -1 } },
|
|
692
|
+
{ $skip: limit?.skip || 0 },
|
|
693
|
+
{ $limit: limit?.limit || 50 },
|
|
694
|
+
{ $project: { path: 0, immediateSubfolder: 0 } },
|
|
695
|
+
{ $addFields: { type: 'file' } },
|
|
696
|
+
],
|
|
697
|
+
folders: [
|
|
698
|
+
{ $match: { immediateSubfolder: { $ne: null } } },
|
|
699
|
+
{ $group: { _id: '$immediateSubfolder' } }, // Unique folder names
|
|
700
|
+
{ $sort: { _id: 1 } },
|
|
701
|
+
{
|
|
702
|
+
$project: {
|
|
703
|
+
_id: 0,
|
|
704
|
+
prefix: { $concat: [prefixWithSlash, '$_id'] },
|
|
705
|
+
name: '$_id',
|
|
706
|
+
type: { $literal: 'folder' },
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
metadata: [
|
|
711
|
+
{ $match: { immediateSubfolder: null } },
|
|
712
|
+
{ $count: 'all' },
|
|
713
|
+
],
|
|
714
|
+
},
|
|
578
715
|
},
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
716
|
+
{
|
|
717
|
+
$project: {
|
|
718
|
+
all: { $arrayElemAt: ['$metadata.all', 0] },
|
|
719
|
+
data: { $concatArrays: ['$folders', '$files'] },
|
|
720
|
+
count: { $add: [{ $size: '$folders' }, { $size: '$files' }] },
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
])
|
|
724
|
+
.toArray()
|
|
725
|
+
.then((r) => r[0] || { all: 0, data: [], count: 0 });
|
|
726
|
+
}
|
|
583
727
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
728
|
+
return result || { all: 0, data: [], count: 0 };
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Get all files for a company/project with optional prefix filtering.
|
|
732
|
+
* Returns a flat list of all matching files without creating virtual folders.
|
|
733
|
+
*
|
|
734
|
+
* @param company - Company identifier
|
|
735
|
+
* @param project - Optional project identifier
|
|
736
|
+
* @param app - Optional app identifier
|
|
737
|
+
* @param prefix - Optional prefix to filter files (e.g., "folder/subfolder")
|
|
738
|
+
* @param limit - Optional pagination with skip and limit
|
|
739
|
+
* @returns Promise with all matching files and count
|
|
740
|
+
*/
|
|
741
|
+
getAllFiles(company, project, app, prefix, limit) {
|
|
742
|
+
const query = { company, project, app, parent: { $exists: false } };
|
|
743
|
+
// If prefix is provided, filter files with that prefix
|
|
744
|
+
if (prefix) {
|
|
745
|
+
const prefixWithSlash = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
746
|
+
const escapedPrefix = prefixWithSlash.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
747
|
+
// Match files that start with the prefix (exact match or deeper)
|
|
748
|
+
query.$or = [
|
|
749
|
+
{ prefix }, // Exact match
|
|
750
|
+
{ prefix: { $regex: `^${escapedPrefix}` } }, // Starts with prefix/
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
// Build data pipeline stages
|
|
754
|
+
const dataPipeline = [{ $sort: { _id: -1 } }];
|
|
755
|
+
// Only add pagination if limit is provided
|
|
756
|
+
if (limit) {
|
|
757
|
+
if (limit.skip)
|
|
758
|
+
dataPipeline.push({ $skip: limit.skip });
|
|
759
|
+
if (limit.limit)
|
|
760
|
+
dataPipeline.push({ $limit: limit.limit });
|
|
761
|
+
}
|
|
762
|
+
dataPipeline.push({ $project: { history: 0 } });
|
|
595
763
|
return StorageModel.db.aggregate([
|
|
596
764
|
{
|
|
597
|
-
$match:
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
$addFields: {
|
|
601
|
-
// Extract immediate subfolder name after current prefix
|
|
602
|
-
immediateSubfolder: {
|
|
603
|
-
$cond: {
|
|
604
|
-
if: { $eq: ['$prefix', query.prefix] },
|
|
605
|
-
then: null, // File at exact level
|
|
606
|
-
else: {
|
|
607
|
-
// Extract first segment after prefix
|
|
608
|
-
$arrayElemAt: [
|
|
609
|
-
{ $split: [{ $substr: ['$prefix', prefixWithSlash.length, -1] }, '/'] },
|
|
610
|
-
0,
|
|
611
|
-
],
|
|
612
|
-
},
|
|
613
|
-
},
|
|
614
|
-
},
|
|
615
|
-
},
|
|
765
|
+
$match: query,
|
|
616
766
|
},
|
|
617
767
|
{
|
|
618
768
|
$facet: {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
{ $sort: { _id: -1 } },
|
|
622
|
-
{ $skip: limit?.skip || 0 },
|
|
623
|
-
{ $limit: limit?.limit || 50 },
|
|
624
|
-
{ $project: { path: 0, bucket: 0, immediateSubfolder: 0 } },
|
|
625
|
-
{ $addFields: { type: 'file' } },
|
|
626
|
-
],
|
|
627
|
-
folders: [
|
|
628
|
-
{ $match: { immediateSubfolder: { $ne: null } } },
|
|
629
|
-
{ $group: { _id: '$immediateSubfolder' } }, // Unique folder names
|
|
630
|
-
{ $sort: { _id: 1 } },
|
|
631
|
-
{
|
|
632
|
-
$project: {
|
|
633
|
-
_id: 0,
|
|
634
|
-
prefix: { $concat: [prefixWithSlash, '$_id'] },
|
|
635
|
-
name: '$_id',
|
|
636
|
-
type: { $literal: 'folder' },
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
],
|
|
640
|
-
metadata: [
|
|
641
|
-
{ $match: { immediateSubfolder: null } },
|
|
642
|
-
{ $count: 'all' },
|
|
643
|
-
],
|
|
769
|
+
metadata: [{ $count: 'all' }],
|
|
770
|
+
data: dataPipeline,
|
|
644
771
|
},
|
|
645
772
|
},
|
|
646
773
|
{
|
|
647
774
|
$project: {
|
|
648
775
|
all: { $arrayElemAt: ['$metadata.all', 0] },
|
|
649
|
-
data:
|
|
650
|
-
count: { $
|
|
776
|
+
data: 1,
|
|
777
|
+
count: { $size: '$data' },
|
|
651
778
|
},
|
|
652
779
|
},
|
|
653
780
|
])
|
|
654
781
|
.toArray()
|
|
655
782
|
.then((result) => result[0] || { all: 0, data: [], count: 0 });
|
|
656
783
|
}
|
|
657
|
-
getTags(company, project, search) {
|
|
784
|
+
async getTags(company, project, search) {
|
|
658
785
|
let cleanSearch = search.toLowerCase().trim();
|
|
659
786
|
let sortString = cleanSearch;
|
|
660
787
|
let splitSearch = [];
|
|
@@ -666,9 +793,9 @@ export class StorageService extends Immutable {
|
|
|
666
793
|
else {
|
|
667
794
|
cleanSearch = new RegExp(`^(?!([a-z]+):).*(${cleanSearch}).*`);
|
|
668
795
|
}
|
|
669
|
-
|
|
670
|
-
.toArray()
|
|
671
|
-
|
|
796
|
+
const matchedDocuments = await StorageModel.db.find({ company, project, 'metadata.tags': { $regex: cleanSearch, $options: 'i' } }, { projection: { 'metadata.tags': 1 } })
|
|
797
|
+
.toArray();
|
|
798
|
+
return this.matchSortTags(matchedDocuments, cleanSearch, sortString);
|
|
672
799
|
}
|
|
673
800
|
single(company, project, filename) {
|
|
674
801
|
return StorageModel.db.aggregate([
|
|
@@ -770,21 +897,39 @@ export class StorageService extends Immutable {
|
|
|
770
897
|
/**
|
|
771
898
|
* checks if the file was already successfully uploaded
|
|
772
899
|
*/
|
|
773
|
-
exists(filename, prefix, app, options) {
|
|
774
|
-
|
|
900
|
+
exists(filename, prefix, app, customer, list, options) {
|
|
901
|
+
const query = {
|
|
775
902
|
company: options.company,
|
|
776
903
|
project: options.project,
|
|
777
904
|
filename,
|
|
778
905
|
prefix,
|
|
779
906
|
app,
|
|
780
|
-
}
|
|
907
|
+
};
|
|
908
|
+
if (customer)
|
|
909
|
+
query.customer = customer;
|
|
910
|
+
if (list)
|
|
911
|
+
query.list = list;
|
|
912
|
+
return StorageModel.db.countDocuments(query)
|
|
781
913
|
.then((exists) => exists >= 1);
|
|
782
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* counts non-versioned storage documents bound to a specific assetId
|
|
917
|
+
*/
|
|
918
|
+
async countByAssetId(assetId, app, customer, list, options) {
|
|
919
|
+
return StorageModel.db.countDocuments({
|
|
920
|
+
company: options.company,
|
|
921
|
+
project: options.project,
|
|
922
|
+
app,
|
|
923
|
+
assetId,
|
|
924
|
+
customer,
|
|
925
|
+
list,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
783
928
|
matchSortTags(documents, search, sort) {
|
|
784
929
|
const mergedArray = [];
|
|
785
930
|
const returnArray = [];
|
|
786
931
|
documents.forEach((document) => {
|
|
787
|
-
document.tags
|
|
932
|
+
document.metadata?.tags?.forEach((tag) => {
|
|
788
933
|
if (mergedArray.find((tagArray) => tagArray.tag === tag))
|
|
789
934
|
return;
|
|
790
935
|
if (!tag.match(search))
|
|
@@ -801,14 +946,17 @@ export class StorageService extends Immutable {
|
|
|
801
946
|
checkAllowedSize(filesize, allowedSize) {
|
|
802
947
|
return filesize < allowedSize;
|
|
803
948
|
}
|
|
804
|
-
stats(company, project) {
|
|
949
|
+
stats(company, project, customer) {
|
|
950
|
+
const match = {
|
|
951
|
+
company,
|
|
952
|
+
project,
|
|
953
|
+
parent: { $exists: false },
|
|
954
|
+
};
|
|
955
|
+
if (customer)
|
|
956
|
+
match.customer = customer;
|
|
805
957
|
const aggregation = [
|
|
806
958
|
{
|
|
807
|
-
$match:
|
|
808
|
-
company,
|
|
809
|
-
project,
|
|
810
|
-
parent: { $exists: false },
|
|
811
|
-
},
|
|
959
|
+
$match: match,
|
|
812
960
|
},
|
|
813
961
|
{
|
|
814
962
|
$group: {
|
|
@@ -833,28 +981,47 @@ export class StorageService extends Immutable {
|
|
|
833
981
|
.then((value) => value[0] || { files: 0, size: 0 });
|
|
834
982
|
}
|
|
835
983
|
getByInternalId(internalId, options) {
|
|
836
|
-
return StorageModel.db.findOne({ _id: internalId, company: options.company, project: options.project });
|
|
984
|
+
return StorageModel.db.findOne({ _id: internalId, company: options.company, project: options.project }, { projection: { path: 0 } });
|
|
837
985
|
}
|
|
838
|
-
getPathByFilename(filename, app, options) {
|
|
986
|
+
getPathByFilename(filename, app, customer, list, options) {
|
|
839
987
|
const filenameObject = filenameHelper(filename);
|
|
840
|
-
|
|
988
|
+
const query = {
|
|
841
989
|
filename: filenameObject.filename,
|
|
842
990
|
prefix: filenameObject.directory,
|
|
843
991
|
company: options.company,
|
|
844
992
|
project: options.project,
|
|
845
993
|
app,
|
|
846
|
-
}
|
|
994
|
+
};
|
|
995
|
+
if (customer)
|
|
996
|
+
query.customer = customer;
|
|
997
|
+
if (list)
|
|
998
|
+
query.list = list;
|
|
999
|
+
return StorageModel.db.findOne(query, {
|
|
847
1000
|
projection: { path: 1, metadata: 1 },
|
|
848
1001
|
});
|
|
849
1002
|
}
|
|
850
|
-
getByFilename(filename, prefix,
|
|
851
|
-
|
|
1003
|
+
getByFilename(filename, prefix, app, customer, list, options) {
|
|
1004
|
+
const query = {
|
|
852
1005
|
filename,
|
|
853
1006
|
prefix,
|
|
854
1007
|
company: options.company,
|
|
855
1008
|
project: options.project,
|
|
856
|
-
|
|
857
|
-
}
|
|
1009
|
+
app,
|
|
1010
|
+
};
|
|
1011
|
+
if (customer)
|
|
1012
|
+
query.customer = customer;
|
|
1013
|
+
if (list)
|
|
1014
|
+
query.list = list;
|
|
1015
|
+
return StorageModel.db.findOne(query);
|
|
1016
|
+
}
|
|
1017
|
+
async removeBlobMembers(internalId, customer, options) {
|
|
1018
|
+
const storage = await this.model.db.findOne({ _id: internalId, company: options.company, project: options.project, customer });
|
|
1019
|
+
if (!storage)
|
|
1020
|
+
throw new Error('BLOB_NOT_FOUND');
|
|
1021
|
+
await fs.promises.unlink(`${this.options.dataFolder}/${storage.path}`);
|
|
1022
|
+
await this.removeBlobVersion(internalId, options);
|
|
1023
|
+
await this.delete(internalId, options.user, options.app);
|
|
1024
|
+
await jetstreamService.publish(`edge.${storage.app}.removed`, storage);
|
|
858
1025
|
}
|
|
859
1026
|
async removeBlob(internalId, options) {
|
|
860
1027
|
const storage = await this.model.db.findOne({ _id: internalId, company: options.company, project: options.project });
|
|
@@ -862,14 +1029,16 @@ export class StorageService extends Immutable {
|
|
|
862
1029
|
throw new Error('BLOB_NOT_FOUND');
|
|
863
1030
|
await fs.promises.unlink(`${this.options.dataFolder}/${storage.path}`);
|
|
864
1031
|
await this.removeBlobVersion(internalId, options);
|
|
865
|
-
|
|
1032
|
+
await this.delete(internalId, options.user, options.app);
|
|
1033
|
+
await jetstreamService.publish(`edge.${storage.app}.removed`, storage);
|
|
866
1034
|
}
|
|
867
1035
|
async removeBlobVersion(parentId, options) {
|
|
868
1036
|
const versions = await this.model.db.find({ parent: parentId, company: options.company, project: options.project }).toArray();
|
|
869
1037
|
if (versions.length) {
|
|
870
1038
|
await Promise.allSettled(versions.map(async (single) => {
|
|
871
1039
|
await fs.promises.unlink(`${this.options.dataFolder}/${single.path}`);
|
|
872
|
-
|
|
1040
|
+
await this.delete(single._id, options.user, options.app);
|
|
1041
|
+
await jetstreamService.publish(`edge.${single.app}.removed`, single);
|
|
873
1042
|
}));
|
|
874
1043
|
}
|
|
875
1044
|
}
|