mbkbucket 4.0.0 → 4.1.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.
Files changed (40) hide show
  1. package/.env.example +16 -15
  2. package/README.md +613 -561
  3. package/index.js +36 -33
  4. package/lib/cli.js +239 -239
  5. package/lib/public/bucketadmin.css +2042 -2042
  6. package/lib/public/bucketadmin.js +1844 -1622
  7. package/lib/public/mbkbucket-helper-demo.html +0 -58
  8. package/lib/public/mbkbucket-helper.js +1 -9
  9. package/lib/src/config/validation.js +91 -86
  10. package/lib/src/controllers/api.controller.js +486 -459
  11. package/lib/src/controllers/dashboard.controller.js +69 -69
  12. package/lib/src/controllers/info.controller.js +13 -13
  13. package/lib/src/controllers/view.controller.js +234 -233
  14. package/lib/src/core/metrics.js +134 -0
  15. package/lib/src/core/storage/capabilities.js +58 -0
  16. package/lib/src/core/storage/errors.js +0 -5
  17. package/lib/src/core/storage/gdrive-auth.js +0 -36
  18. package/lib/src/core/storage/gdrive-provider.js +101 -43
  19. package/lib/src/core/storage/index.js +4 -1
  20. package/lib/src/core/storage/local-provider.js +507 -0
  21. package/lib/src/core/storage/models.js +90 -50
  22. package/lib/src/core/storage/path-strategy.js +14 -1
  23. package/lib/src/core/storage/s3-provider.js +17 -37
  24. package/lib/src/core/storage/storage-manager.js +70 -50
  25. package/lib/src/core/storage/storage-provider.js +49 -120
  26. package/lib/src/middleware/bucket-resolver.js +37 -37
  27. package/lib/src/routes/api.routes.js +4 -1
  28. package/lib/src/routes/index.js +60 -60
  29. package/lib/src/routes/view.routes.js +28 -28
  30. package/lib/src/services/s3.service.js +42 -42
  31. package/lib/src/services/storage.service.js +311 -213
  32. package/lib/src/utils/cleanup-scheduler.js +91 -0
  33. package/lib/src/utils/errors.js +56 -56
  34. package/lib/src/utils/helpers.js +6 -6
  35. package/package.json +3 -5
  36. package/views/bucket.hbs +0 -5
  37. package/views/bucketadmin_move_modal.hbs +35 -35
  38. package/views/bucketadmin_preview_modal.hbs +108 -108
  39. package/views/bucketadmincontent.hbs +193 -193
  40. package/views/mbkbucket_info.hbs +134 -134
@@ -13,11 +13,6 @@
13
13
  <title>Helper Utils Demo — MBKBucket</title>
14
14
  <meta name="description" content="Interactive demo of MBKBucket frontend helper utilities: gallery picker, file input, upload widget, multi-select, live preview binding, and API helpers.">
15
15
 
16
- <!-- Fonts (same as bucket.hbs) -->
17
- <link rel="preconnect" href="https://fonts.googleapis.com">
18
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
- <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
20
-
21
16
  <!-- MBKBucket design system + helper styles -->
22
17
  <link rel="stylesheet" href="/mbkbucket/bucketadmin.css">
23
18
  <link rel="stylesheet" href="/mbkbucket/mbkbucket-helper.css">
@@ -834,14 +829,12 @@ uploader.<span class="fn">clear</span>();</pre>
834
829
  <span class="chip">MBKBucket.url.*</span>
835
830
  <span class="chip">listFiles</span>
836
831
  <span class="chip">uploadFile</span>
837
- <span class="chip">getSignedUrl</span>
838
832
  </div>
839
833
  </div>
840
834
 
841
835
  <div class="demo-tabs">
842
836
  <div class="demo-tab active" data-tab="urls" data-group="api">URL Builders</div>
843
837
  <div class="demo-tab" data-tab="list" data-group="api">List Files</div>
844
- <div class="demo-tab" data-tab="signed" data-group="api">Signed URL</div>
845
838
  <div class="demo-tab" data-tab="code" data-group="api">Code Reference</div>
846
839
  </div>
847
840
 
@@ -899,30 +892,6 @@ uploader.<span class="fn">clear</span>();</pre>
899
892
  </div>
900
893
  </div>
901
894
 
902
- <!-- Signed URL -->
903
- <div class="demo-pane" data-pane="api-signed">
904
- <div class="form-row">
905
- <label class="form-label" for="api-signed-key">File key:</label>
906
- <div class="flex-row">
907
- <input id="api-signed-key"
908
- class="demo-input"
909
- type="text"
910
- style="max-width:320px"
911
- placeholder="e.g. images/photo.jpg">
912
- <button class="btn btn-primary btn-sm" onclick="demoSignedUrl()">Get URL</button>
913
- </div>
914
- <div class="form-hint">Calls <code class="inline-code">MBKBucket.api.getSignedUrl(key, { expiresIn: 3600 })</code></div>
915
- </div>
916
-
917
- <div class="api-result" id="api-signed-result" style="display:none">
918
- <div class="api-result-header">
919
- <div class="api-dot" id="api-signed-dot"></div>
920
- <span id="api-signed-status">—</span>
921
- </div>
922
- <pre id="api-signed-pre"></pre>
923
- </div>
924
- </div>
925
-
926
895
  <!-- Code reference -->
927
896
  <div class="code-pane" data-pane="api-code">
928
897
  <pre class="code-block"><span class="cm">// URL helpers</span>
@@ -1132,33 +1101,6 @@ MBKBucket.toast.<span class="fn">info</span>(<span class="str">'Loading…'</spa
1132
1101
  pre.textContent = JSON.stringify({ error: err.message, status: err.status }, null, 2);
1133
1102
  }
1134
1103
  }
1135
-
1136
- // ── 4. API: signed URL ───────────────────────────────────────
1137
- async function demoSignedUrl() {
1138
- const key = document.getElementById('api-signed-key').value.trim();
1139
- if (!key) { MBKBucket.toast.warning('Enter a file key first'); return; }
1140
-
1141
- const result = document.getElementById('api-signed-result');
1142
- const dot = document.getElementById('api-signed-dot');
1143
- const status = document.getElementById('api-signed-status');
1144
- const pre = document.getElementById('api-signed-pre');
1145
-
1146
- result.style.display = '';
1147
- dot.className = 'api-dot';
1148
- status.textContent = 'Loading…';
1149
- pre.textContent = '';
1150
-
1151
- try {
1152
- const data = await MBKBucket.api.getSignedUrl(key, { expiresIn: 3600 });
1153
- dot.className = 'api-dot ok';
1154
- status.textContent = `200 OK — expires in ${data.expiresIn}s`;
1155
- pre.textContent = JSON.stringify(data, null, 2);
1156
- } catch (err) {
1157
- dot.className = 'api-dot err';
1158
- status.textContent = `Error: ${err.message}`;
1159
- pre.textContent = JSON.stringify({ error: err.message }, null, 2);
1160
- }
1161
- }
1162
1104
  </script>
1163
1105
  </body>
1164
1106
  </html>
@@ -330,17 +330,9 @@
330
330
  }
331
331
  return this.uploadSingle(prefix, file, opts);
332
332
  },
333
-
334
- getSignedUrl(key, opts) {
335
- opts = opts || {};
336
- return apiFetch(buildUrl(`${BASE}/api/signed-url`, {
337
- key,
338
- bucket: opts.bucket || '',
339
- expiresIn: opts.expiresIn || 3600,
340
- }));
341
- },
342
333
  };
343
334
 
335
+
344
336
  // ─────────────────────────────────────────────────────────────────────────
345
337
  // Gallery Modal (with In-Modal Upload & Multi-Select)
346
338
  // ─────────────────────────────────────────────────────────────────────────
@@ -1,86 +1,91 @@
1
- const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
2
- const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
3
- const REQUIRED_S3_FIELDS = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
4
-
5
- export function normalizeBooleanLike(value) {
6
- if (typeof value === 'boolean') return value;
7
- if (typeof value === 'string') {
8
- const norm = value.trim().toLowerCase();
9
- if (TRUE_VALUES.has(norm)) return true;
10
- if (FALSE_VALUES.has(norm)) return false;
11
- }
12
- return value;
13
- }
14
-
15
- export function parseAndValidateMbkbucketVar(rawValue) {
16
- let parsed;
17
- try {
18
- parsed = rawValue ? JSON.parse(rawValue) : {};
19
- } catch {
20
- throw new Error('Invalid JSON in process.env.mbkbucketVar');
21
- }
22
-
23
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
24
- throw new Error('mbkbucketVar must be a valid object');
25
- }
26
-
27
- const defaults = { p_view_inline: true, publiView_enabled: false };
28
- for (const [key, defaultVal] of Object.entries(defaults)) {
29
- const val = parsed[key];
30
- if (val === undefined || (typeof val === 'string' && val.trim() === '')) {
31
- parsed[key] = defaultVal;
32
- }
33
- }
34
-
35
- parsed.p_view_inline = normalizeBooleanLike(parsed.p_view_inline);
36
- parsed.publiView_enabled = normalizeBooleanLike(parsed.publiView_enabled);
37
-
38
- const errors = [];
39
- if (typeof parsed.p_view_inline !== 'boolean') errors.push('mbkbucketVar.p_view_inline must be a boolean if provided');
40
- if (typeof parsed.publiView_enabled !== 'boolean') errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
41
- if (errors.length) throw new Error(errors.join(' | '));
42
-
43
- return parsed;
44
- }
45
-
46
- export function parseAndValidateBucketConnection(rawValue) {
47
- if (!rawValue) return null;
48
-
49
- let parsed;
50
- try {
51
- parsed = JSON.parse(rawValue);
52
- } catch {
53
- throw new Error('BucketConnection is not valid JSON');
54
- }
55
-
56
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Object.keys(parsed).length) {
57
- throw new Error('BucketConnection must be a non-empty object');
58
- }
59
-
60
- for (const [bucketName, config] of Object.entries(parsed)) {
61
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
62
- throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
63
- }
64
-
65
- const providerType = String(config.type || config.provider || '').toLowerCase();
66
-
67
- if (providerType === 'gdrive' || providerType === 'google-drive' || providerType === 'googledrive') {
68
- const hasOAuth = Boolean(config.client_id && config.client_secret && (config.refresh_token || config.access_token || config.code));
69
- const hasServiceAccount = Boolean((config.client_email || config.service_account_email) && (config.private_key || config.service_account_key));
70
- const hasAccessTokenOnly = Boolean(config.access_token);
71
- const hasServiceAccountKeyObj = Boolean(config.service_account && typeof config.service_account === 'object');
72
-
73
- if (!hasOAuth && !hasServiceAccount && !hasAccessTokenOnly && !hasServiceAccountKeyObj) {
74
- throw new Error(`Google Drive connection '${bucketName}' requires OAuth credentials (client_id, client_secret, refresh_token) or Service Account credentials (client_email, private_key).`);
75
- }
76
- } else {
77
- // Default is S3-compatible provider
78
- const missingFields = REQUIRED_S3_FIELDS.filter(field => !config[field]);
79
- if (missingFields.length) {
80
- throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
81
- }
82
- }
83
- }
84
-
85
- return parsed;
86
- }
1
+ const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
2
+ const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
3
+ const REQUIRED_S3_FIELDS = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
4
+
5
+ export function normalizeBooleanLike(value) {
6
+ if (typeof value === 'boolean') return value;
7
+ if (typeof value === 'string') {
8
+ const norm = value.trim().toLowerCase();
9
+ if (TRUE_VALUES.has(norm)) return true;
10
+ if (FALSE_VALUES.has(norm)) return false;
11
+ }
12
+ return value;
13
+ }
14
+
15
+ export function parseAndValidateMbkbucketVar(rawValue) {
16
+ let parsed;
17
+ try {
18
+ parsed = rawValue ? JSON.parse(rawValue) : {};
19
+ } catch {
20
+ throw new Error('Invalid JSON in process.env.mbkbucketVar');
21
+ }
22
+
23
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
24
+ throw new Error('mbkbucketVar must be a valid object');
25
+ }
26
+
27
+ const defaults = { p_view_inline: true, publiView_enabled: false };
28
+ for (const [key, defaultVal] of Object.entries(defaults)) {
29
+ const val = parsed[key];
30
+ if (val === undefined || (typeof val === 'string' && val.trim() === '')) {
31
+ parsed[key] = defaultVal;
32
+ }
33
+ }
34
+
35
+ parsed.p_view_inline = normalizeBooleanLike(parsed.p_view_inline);
36
+ parsed.publiView_enabled = normalizeBooleanLike(parsed.publiView_enabled);
37
+
38
+ const errors = [];
39
+ if (typeof parsed.p_view_inline !== 'boolean') errors.push('mbkbucketVar.p_view_inline must be a boolean if provided');
40
+ if (typeof parsed.publiView_enabled !== 'boolean') errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
41
+ if (errors.length) throw new Error(errors.join(' | '));
42
+
43
+ return parsed;
44
+ }
45
+
46
+ export function parseAndValidateBucketConnection(rawValue) {
47
+ if (!rawValue) return null;
48
+
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(rawValue);
52
+ } catch {
53
+ throw new Error('BucketConnection is not valid JSON');
54
+ }
55
+
56
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Object.keys(parsed).length) {
57
+ throw new Error('BucketConnection must be a non-empty object');
58
+ }
59
+
60
+ for (const [bucketName, config] of Object.entries(parsed)) {
61
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
62
+ throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
63
+ }
64
+
65
+ const providerType = String(config.type || config.provider || '').toLowerCase();
66
+ const isLocal = providerType === 'local' || providerType === 'fs' || providerType === 'filesystem' || Boolean(config.basePath || config.rootPath || config.directory);
67
+
68
+ if (isLocal) {
69
+ // Local filesystem provider needs no cloud credentials, basePath is optional (defaults to ./mbkbucket_storage)
70
+ continue;
71
+ } else if (providerType === 'gdrive' || providerType === 'google-drive' || providerType === 'googledrive') {
72
+ const hasOAuth = Boolean(config.client_id && config.client_secret && (config.refresh_token || config.access_token || config.code));
73
+ const hasServiceAccount = Boolean((config.client_email || config.service_account_email) && (config.private_key || config.service_account_key));
74
+ const hasAccessTokenOnly = Boolean(config.access_token);
75
+ const hasServiceAccountKeyObj = Boolean(config.service_account && typeof config.service_account === 'object');
76
+
77
+ if (!hasOAuth && !hasServiceAccount && !hasAccessTokenOnly && !hasServiceAccountKeyObj) {
78
+ throw new Error(`Google Drive connection '${bucketName}' requires OAuth credentials (client_id, client_secret, refresh_token) or Service Account credentials (client_email, private_key).`);
79
+ }
80
+ } else {
81
+ // Default is S3-compatible provider
82
+ const missingFields = REQUIRED_S3_FIELDS.filter(field => !config[field]);
83
+ if (missingFields.length) {
84
+ throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
85
+ }
86
+ }
87
+
88
+ }
89
+
90
+ return parsed;
91
+ }