mbkbucket 1.3.0 → 1.4.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.
@@ -347,6 +347,9 @@ function renderFilesTable(files, folders = [], prefix = '') {
347
347
  // 1. Show files where FILENAME matches the search term
348
348
  const searchLower = currentSearch.toLowerCase();
349
349
  displayFiles = files.filter(f => {
350
+ // Skip folder marker objects (keys ending with /)
351
+ if (f.Key.endsWith('/')) return false;
352
+
350
353
  const fileName = f.Key.split('/').pop();
351
354
  return fileName.toLowerCase().includes(searchLower);
352
355
  });
@@ -355,13 +358,17 @@ function renderFilesTable(files, folders = [], prefix = '') {
355
358
  displayFolders = extractMatchingFolders(files, currentSearch);
356
359
  } else {
357
360
  // Normal Flat Mode: Show all files, no folders
358
- displayFiles = files;
361
+ // Filter out folder marker objects (keys ending with /)
362
+ displayFiles = files.filter(f => !f.Key.endsWith('/'));
359
363
  displayFolders = [];
360
364
  }
361
365
  } else {
362
366
  // Normal Folder View logic
363
367
  // Filter files to show only those at current level (not in subfolders)
364
368
  displayFiles = files.filter(file => {
369
+ // Skip folder marker objects (keys ending with /)
370
+ if (file.Key.endsWith('/')) return false;
371
+
365
372
  const relativePath = prefix ? file.Key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : file.Key;
366
373
  // File is at current level if it has no '/' in relative path
367
374
  return !relativePath.includes('/');
@@ -823,7 +830,7 @@ function createId() { return Date.now().toString(36) + '-' + Math.random().toStr
823
830
 
824
831
  function addFilesToQueue(fileList) {
825
832
  for (const f of Array.from(fileList)) {
826
- uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0 });
833
+ uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0, uploadedBytes: 0, uploadRate: 0 });
827
834
  }
828
835
  renderUploadQueue();
829
836
  }
@@ -839,7 +846,27 @@ function renderUploadQueue() {
839
846
  return;
840
847
  }
841
848
 
842
- container.innerHTML = uploadQueue.map(item => `
849
+ // Local size formatter so it's always in scope regardless of IIFE load order
850
+ function fmtSize(bytes) {
851
+ if (!bytes || bytes === 0) return '0 B';
852
+ const units = ['B', 'KB', 'MB', 'GB'];
853
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
854
+ return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
855
+ }
856
+
857
+ container.innerHTML = uploadQueue.map(item => {
858
+ let statusText;
859
+ if (item.status === 'uploading') {
860
+ const pct = Math.round(item.progress || 0);
861
+ statusText = fmtSize(item.uploadedBytes || 0) + ' / ' + fmtSize(item.file.size) + ' (' + pct + '%)';
862
+ if (item.uploadRate > 0) statusText += ' \u00b7 ' + fmtSize(item.uploadRate) + '/s';
863
+ } else {
864
+ statusText = item.status;
865
+ }
866
+ const actionBtn = item.status === 'uploading'
867
+ ? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>`
868
+ : `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`;
869
+ return `
843
870
  <div class="upload-item" data-id="${item.id}">
844
871
  <div class="meta">
845
872
  <div style="display:flex; align-items:center; gap:0.75rem; min-width:0; width:100%; margin-bottom:5px;">
@@ -853,14 +880,12 @@ function renderUploadQueue() {
853
880
  <div class="progress-bar" style="width: ${Math.round(item.progress)}%; height: 8px;"></div>
854
881
  </div>
855
882
  <div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem;">
856
- <small class="text-muted status">${item.status}</small>
857
- <div class="controls">
858
- ${item.status === 'uploading' ? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>` : `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`}
859
- </div>
883
+ <small class="text-muted status">${statusText}</small>
884
+ <div class="controls">${actionBtn}</div>
860
885
  </div>
861
886
  </div>
862
- </div>
863
- `).join('');
887
+ </div>`;
888
+ }).join('');
864
889
 
865
890
  // Wire up controls
866
891
  document.querySelectorAll('.remove-queued').forEach(btn => btn.addEventListener('click', function (e) {
@@ -870,15 +895,27 @@ function renderUploadQueue() {
870
895
  renderUploadQueue();
871
896
  }));
872
897
 
873
- document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', function (e) {
898
+ document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', async function (e) {
874
899
  const id = this.dataset.id;
875
900
  const handle = xhrMap.get(id);
876
- if (handle && handle.abort) {
877
- handle.abort();
878
- } else if (handle && handle.xhr) {
879
- handle.xhr.abort();
880
- }
881
901
  const entry = uploadQueue.find(i => i.id === id);
902
+
903
+ // Abort in-flight fetch
904
+ if (handle && handle.controller) handle.controller.abort();
905
+ // Abort legacy XHR (small file path)
906
+ if (handle && handle.xhr) handle.xhr.abort();
907
+
908
+ // Tell S3 to discard the incomplete multipart upload
909
+ if (entry && entry.s3UploadId && entry.s3Key) {
910
+ try {
911
+ await fetch('/mbkbucket/upload-abort', {
912
+ method: 'POST',
913
+ headers: { 'Content-Type': 'application/json' },
914
+ body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
915
+ });
916
+ } catch (_) { /* best-effort */ }
917
+ }
918
+
882
919
  if (entry) { entry.status = 'cancelled'; entry.progress = 0; }
883
920
  xhrMap.delete(id);
884
921
  renderUploadQueue();
@@ -913,41 +950,90 @@ async function startUploadEntry(entry) {
913
950
  const file = entry.file;
914
951
  try {
915
952
  if (file.size > CHUNK_SIZE) {
916
- // chunked upload
917
- const uploadId = createId();
918
- entry.uploadId = uploadId;
953
+ // ---------------------------------------------------------------
954
+ // S3 Multipart Upload
955
+ // Step 1 — Initiate: get uploadId + key from the server
956
+ // ---------------------------------------------------------------
957
+ const initResp = await fetch('/mbkbucket/upload-init', {
958
+ method: 'POST',
959
+ headers: { 'Content-Type': 'application/json' },
960
+ body: JSON.stringify({
961
+ fileName: file.name,
962
+ prefix: currentPrefix,
963
+ contentType: file.type || 'application/octet-stream'
964
+ })
965
+ });
966
+ const initJson = await initResp.json();
967
+ if (!initResp.ok || !initJson.success) throw new Error(initJson.error || 'Failed to initiate upload');
968
+
969
+ const { uploadId, key } = initJson;
970
+ entry.s3UploadId = uploadId; // stored so cancel handler can abort
971
+ entry.s3Key = key;
972
+
973
+ // ---------------------------------------------------------------
974
+ // Step 2 — Upload parts (1-based partNumber, min 5 MB except last)
975
+ // ---------------------------------------------------------------
919
976
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
977
+ const parts = []; // [{ partNumber, ETag }]
920
978
  let uploadedBytes = 0;
979
+
921
980
  for (let i = 0; i < totalChunks; i++) {
922
- if (entry.status === 'cancelled') throw new Error('Upload cancelled');
981
+ const _chunkStart = Date.now();
982
+ if (entry.status === 'cancelled') {
983
+ // Clean up on S3
984
+ await fetch('/mbkbucket/upload-abort', {
985
+ method: 'POST',
986
+ headers: { 'Content-Type': 'application/json' },
987
+ body: JSON.stringify({ uploadId, key })
988
+ }).catch(() => {});
989
+ throw new Error('Upload cancelled');
990
+ }
991
+
923
992
  const start = i * CHUNK_SIZE;
924
- const end = Math.min(file.size, start + CHUNK_SIZE);
925
- const blob = file.slice(start, end);
993
+ const blob = file.slice(start, Math.min(file.size, start + CHUNK_SIZE));
994
+ const partNumber = i + 1; // S3 parts are 1-based
926
995
 
927
996
  const fd = new FormData();
928
997
  fd.append('chunk', blob, file.name);
929
998
  fd.append('uploadId', uploadId);
930
- fd.append('fileName', file.name);
931
- fd.append('chunkIndex', String(i));
932
- fd.append('totalChunks', String(totalChunks));
999
+ fd.append('key', key);
1000
+ fd.append('partNumber', String(partNumber));
933
1001
 
934
1002
  const controller = new AbortController();
935
- xhrMap.set(entry.id, { controller });
1003
+ xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
1004
+
1005
+ const chunkResp = await fetch('/mbkbucket/upload-chunk', {
1006
+ method: 'POST',
1007
+ body: fd,
1008
+ signal: controller.signal
1009
+ });
1010
+ const chunkJson = await chunkResp.json();
1011
+ if (!chunkResp.ok || !chunkJson.success) throw new Error(chunkJson.error || `Part ${partNumber} upload failed`);
936
1012
 
937
- const resp = await fetch('/mbkbucket/upload-chunk', { method: 'POST', body: fd, signal: controller.signal });
938
- if (!resp.ok) throw new Error('Chunk upload failed');
1013
+ parts.push({ partNumber: chunkJson.partNumber, ETag: chunkJson.ETag });
939
1014
  uploadedBytes += blob.size;
940
- entry.progress = (uploadedBytes / file.size) * 95; // up to 95% until assembly
1015
+ const _chunkDuration = (Date.now() - _chunkStart) / 1000;
1016
+ entry.uploadRate = _chunkDuration > 0 ? blob.size / _chunkDuration : 0;
1017
+ entry.uploadedBytes = uploadedBytes;
1018
+ entry.progress = (uploadedBytes / file.size) * 95; // reserve last 5% for complete call
941
1019
  renderUploadQueue();
942
1020
  }
943
1021
 
944
- // complete
945
- const completeResp = await fetch('/mbkbucket/upload-complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uploadId: entry.uploadId, fileName: file.name, prefix: currentPrefix, contentType: file.type || 'application/octet-stream' }) });
1022
+ // ---------------------------------------------------------------
1023
+ // Step 3 — Complete: assemble all parts on S3
1024
+ // ---------------------------------------------------------------
1025
+ const completeResp = await fetch('/mbkbucket/upload-complete', {
1026
+ method: 'POST',
1027
+ headers: { 'Content-Type': 'application/json' },
1028
+ body: JSON.stringify({ uploadId, key, parts })
1029
+ });
946
1030
  const completeJson = await completeResp.json();
947
- if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Assemble failed');
1031
+ if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Failed to complete upload');
948
1032
 
949
- entry.status = 'done'; entry.progress = 100; xhrMap.delete(entry.id); renderUploadQueue();
950
- showAlert('File uploaded successfully', 'success');
1033
+ entry.status = 'done'; entry.progress = 100;
1034
+ xhrMap.delete(entry.id);
1035
+ renderUploadQueue();
1036
+ showAlert(`${file.name} uploaded successfully`, 'success');
951
1037
  loadFiles(currentPage, currentPrefix);
952
1038
  } else {
953
1039
  // small file XHR
@@ -960,9 +1046,20 @@ async function startUploadEntry(entry) {
960
1046
 
961
1047
  xhr.upload.addEventListener('progress', (e) => {
962
1048
  if (!e.lengthComputable) return;
1049
+ const _now = Date.now();
1050
+ const _dt = (_now - (entry._rateLastTime || _now)) / 1000;
1051
+ const _db = e.loaded - (entry._rateLastBytes || 0);
1052
+ if (_dt > 0.2) {
1053
+ entry.uploadRate = _db / _dt;
1054
+ entry._rateLastTime = _now;
1055
+ entry._rateLastBytes = e.loaded;
1056
+ }
1057
+ entry.uploadedBytes = e.loaded;
963
1058
  entry.progress = (e.loaded / e.total) * 100;
964
1059
  renderUploadQueue();
965
1060
  });
1061
+ entry._rateLastTime = Date.now();
1062
+ entry._rateLastBytes = 0;
966
1063
 
967
1064
  xhr.addEventListener('load', () => {
968
1065
  try {
@@ -990,7 +1087,16 @@ async function startUploadEntry(entry) {
990
1087
  }
991
1088
  } catch (err) {
992
1089
  console.error('Upload failed for', entry.file.name, err);
1090
+ // If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
1091
+ if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
1092
+ fetch('/mbkbucket/upload-abort', {
1093
+ method: 'POST',
1094
+ headers: { 'Content-Type': 'application/json' },
1095
+ body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
1096
+ }).catch(() => {});
1097
+ }
993
1098
  entry.status = entry.status === 'cancelled' ? 'cancelled' : 'error';
1099
+ xhrMap.delete(entry.id);
994
1100
  renderUploadQueue();
995
1101
  showAlert('Upload failed: ' + (err.message || 'unknown'), 'error');
996
1102
  }
@@ -13,8 +13,9 @@
13
13
  <p id="filterInfo">Loading file information...</p>
14
14
  </div>
15
15
  <div style="margin-left: auto;">
16
- <option value="{{bucketvar}}">{{bucketvar}}</option>
17
- </select>
16
+ <span style="background:rgba(255,255,255,0.2); padding:0.35rem 0.75rem; border-radius:0.5rem; font-size:0.85rem; font-weight:600; color:white; letter-spacing:0.02em;">
17
+ <i class="fas fa-database" style="margin-right:0.4rem;"></i>{{bucketvar}}
18
+ </span>
18
19
  </div>
19
20
  </div>
20
21
  </div>
@@ -0,0 +1,226 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description"
8
+ content="Log in to portal.mbktech.org to access your resources and manage projects securely.">
9
+ <meta name="keywords" content="MBK Tech, Web-Portal, Web, Portal, Admin-Panel, Admin, login">
10
+ <meta property="og:title" content="Version and Configuration Information | MBK Tech" />
11
+ <meta property="og:image" content="https://mbktech.org/Assets/Images/Icon/logo.png" />
12
+ <meta property="og:url" content="/mbkbucket/info">
13
+ <title>Version and Configuration Information | MBK Tech Portal</title>
14
+ <link rel="icon" type="image/svg+xml" href="/icon.svg">
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
16
+ {{> sharedStyles}}
17
+ <style>
18
+ .info-box {
19
+ background: rgba(10, 20, 20, 0.95);
20
+ backdrop-filter: blur(10px);
21
+ border-radius: var(--border-radius);
22
+ padding: 2.5rem;
23
+ width: 100%;
24
+ max-width: 900px;
25
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
26
+ border: 1px solid rgba(0, 184, 148, 0.2);
27
+ position: relative;
28
+ z-index: 2;
29
+ transition: var(--transition);
30
+ }
31
+
32
+ .info-box:hover {
33
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
34
+ border-color: rgba(0, 184, 148, 0.3);
35
+ }
36
+
37
+ .info-title {
38
+ text-align: center;
39
+ margin-bottom: 2rem;
40
+ font-size: 2rem;
41
+ position: relative;
42
+ color: var(--light);
43
+ }
44
+
45
+ .info-title::after {
46
+ content: '';
47
+ position: absolute;
48
+ bottom: -10px;
49
+ left: 50%;
50
+ transform: translateX(-50%);
51
+ width: 100px;
52
+ height: 4px;
53
+ background-color: var(--accent);
54
+ border-radius: 2px;
55
+ }
56
+
57
+ .info-section {
58
+ margin-bottom: 2rem;
59
+ padding: 1.5rem;
60
+ border: 1px solid rgba(0, 184, 148, 0.2);
61
+ border-radius: var(--border-radius);
62
+ background-color: rgba(0, 0, 0, 0.2);
63
+ transition: var(--transition);
64
+ }
65
+
66
+ .info-section:hover {
67
+ border-color: rgba(0, 184, 148, 0.4);
68
+ background-color: rgba(0, 0, 0, 0.3);
69
+ }
70
+
71
+ .info-section h2 {
72
+ color: var(--accent);
73
+ border-bottom: 2px solid rgba(0, 184, 148, 0.3);
74
+ padding-bottom: 10px;
75
+ margin-top: 0;
76
+ margin-bottom: 1.5rem;
77
+ font-size: 1.4rem;
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 10px;
81
+ }
82
+
83
+ .info-section h2 i {
84
+ font-size: 1.2rem;
85
+ }
86
+
87
+ .info-row {
88
+ display: flex;
89
+ margin-bottom: 1rem;
90
+ padding-bottom: 1rem;
91
+ border-bottom: 1px solid rgba(0, 184, 148, 0.1);
92
+ align-items: center;
93
+ }
94
+
95
+ .info-row:last-child {
96
+ border-bottom: none;
97
+ margin-bottom: 0;
98
+ padding-bottom: 0;
99
+ }
100
+
101
+ .info-label {
102
+ font-weight: 600;
103
+ color: var(--text-light);
104
+ min-width: 240px;
105
+ font-size: 1rem;
106
+ }
107
+
108
+ .info-value {
109
+ flex: 1;
110
+ word-break: break-word;
111
+ color: var(--text);
112
+ font-size: 1rem;
113
+ }
114
+
115
+ .version-status {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 6px;
119
+ padding: 4px 12px;
120
+ border-radius: 20px;
121
+ font-size: 0.85rem;
122
+ font-weight: 600;
123
+ margin-left: 10px;
124
+ }
125
+
126
+ .version-up-to-date {
127
+ background: rgba(67, 233, 123, 0.2);
128
+ color: var(--success);
129
+ border: 1px solid var(--success);
130
+ }
131
+
132
+ .version-outdated {
133
+ background: rgba(255, 118, 117, 0.2);
134
+ color: var(--danger);
135
+ border: 1px solid var(--danger);
136
+ }
137
+
138
+ .version-fetch-error {
139
+ background: rgba(255, 209, 102, 0.2);
140
+ color: var(--warning);
141
+ border: 1px solid var(--warning);
142
+ }
143
+
144
+ @media (max-width: 768px) {
145
+ .info-box {
146
+ padding: 2rem;
147
+ max-width: 100%;
148
+ }
149
+
150
+ .info-title {
151
+ font-size: 1.8rem;
152
+ }
153
+
154
+ .info-row {
155
+ flex-direction: column;
156
+ align-items: flex-start;
157
+ gap: 0.5rem;
158
+ }
159
+
160
+ .info-label {
161
+ min-width: unset;
162
+ }
163
+
164
+ .info-section h2 {
165
+ font-size: 1.2rem;
166
+ }
167
+ }
168
+
169
+ @media (max-width: 576px) {
170
+ .info-box {
171
+ padding: 1.5rem;
172
+ }
173
+
174
+ .info-section {
175
+ padding: 1rem;
176
+ }
177
+ }
178
+ </style>
179
+ </head>
180
+
181
+ <body>
182
+ <header>
183
+ <div class="header-container">
184
+ <a class="logo">
185
+ <img src="/icon.svg" alt="Logo" class="logo-image" />
186
+ <span class="logo-text">BK <span>Tech</span></span>
187
+ <span class="logo-comp">{{#if APP_NAME}}{{APP_NAME}}{{else}}mbkbucket{{/if}}
188
+ </a>
189
+
190
+ {{> profilemenu}}
191
+ </div>
192
+ </header>
193
+ <section class="login-container">
194
+ <i class="fas fa-info-circle ai-element"></i>
195
+ <i class="fas fa-cogs ai-element"></i>
196
+ <i class="fas fa-server ai-element"></i>
197
+ <i class="fas fa-code-branch ai-element"></i>
198
+
199
+ <div class="info-box">
200
+ <h1 class="info-title">System Information</h1>
201
+
202
+ <div class="info-section">
203
+ <h2><i class="fas fa-code-branch"></i> Version Information</h2>
204
+ <div class="info-row">
205
+ <div class="info-label">Current Version:</div>
206
+ <div class="info-value" id="CurrentVersion">{{CurrentVersion}}</div>
207
+ </div>
208
+
209
+ <div class="info-row">
210
+ <div class="info-label">Latest Version:</div>
211
+ <div class="info-value">
212
+ {{latestVersion}}
213
+ {{#if (eq latestVersion CurrentVersion)}}
214
+ <span class="version-status version-up-to-date">
215
+ <i class="fas fa-check-circle"></i> Up to date
216
+ </span>
217
+ {{/if}}
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </section>
223
+
224
+ </body>
225
+
226
+ </html>