MyFSIO v0.2.1 Release #13
@@ -1985,12 +1985,16 @@
|
|||||||
const floatingProgressStatus = document.getElementById('floatingUploadStatus');
|
const floatingProgressStatus = document.getElementById('floatingUploadStatus');
|
||||||
const floatingProgressTitle = document.getElementById('floatingUploadTitle');
|
const floatingProgressTitle = document.getElementById('floatingUploadTitle');
|
||||||
const floatingProgressExpand = document.getElementById('floatingUploadExpand');
|
const floatingProgressExpand = document.getElementById('floatingUploadExpand');
|
||||||
|
const floatingProgressCancel = document.getElementById('floatingUploadCancel');
|
||||||
const uploadQueueContainer = document.getElementById('uploadQueueContainer');
|
const uploadQueueContainer = document.getElementById('uploadQueueContainer');
|
||||||
const uploadQueueList = document.getElementById('uploadQueueList');
|
const uploadQueueList = document.getElementById('uploadQueueList');
|
||||||
const uploadQueueCount = document.getElementById('uploadQueueCount');
|
const uploadQueueCount = document.getElementById('uploadQueueCount');
|
||||||
const clearUploadQueueBtn = document.getElementById('clearUploadQueueBtn');
|
const clearUploadQueueBtn = document.getElementById('clearUploadQueueBtn');
|
||||||
let isUploading = false;
|
let isUploading = false;
|
||||||
let uploadQueue = [];
|
let uploadQueue = [];
|
||||||
|
let activeXHRs = [];
|
||||||
|
let activeMultipartUpload = null;
|
||||||
|
let uploadCancelled = false;
|
||||||
let uploadStats = {
|
let uploadStats = {
|
||||||
totalFiles: 0,
|
totalFiles: 0,
|
||||||
completedFiles: 0,
|
completedFiles: 0,
|
||||||
@@ -2056,6 +2060,38 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cancelAllUploads = async () => {
|
||||||
|
uploadCancelled = true;
|
||||||
|
|
||||||
|
activeXHRs.forEach(xhr => {
|
||||||
|
try { xhr.abort(); } catch {}
|
||||||
|
});
|
||||||
|
activeXHRs = [];
|
||||||
|
|
||||||
|
if (activeMultipartUpload) {
|
||||||
|
const { abortUrl } = activeMultipartUpload;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value;
|
||||||
|
try {
|
||||||
|
await fetch(abortUrl, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken || '' } });
|
||||||
|
} catch {}
|
||||||
|
activeMultipartUpload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadQueue = [];
|
||||||
|
isProcessingQueue = false;
|
||||||
|
isUploading = false;
|
||||||
|
setUploadLockState(false);
|
||||||
|
hideFloatingProgress();
|
||||||
|
resetUploadUI();
|
||||||
|
|
||||||
|
showMessage({ title: 'Upload cancelled', body: 'All uploads have been cancelled.', variant: 'info' });
|
||||||
|
loadObjects(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
floatingProgressCancel?.addEventListener('click', () => {
|
||||||
|
cancelAllUploads();
|
||||||
|
});
|
||||||
|
|
||||||
const refreshUploadDropLabel = () => {
|
const refreshUploadDropLabel = () => {
|
||||||
if (!uploadDropZoneLabel) return;
|
if (!uploadDropZoneLabel) return;
|
||||||
if (isUploading) {
|
if (isUploading) {
|
||||||
@@ -2177,6 +2213,8 @@
|
|||||||
const uploadMultipart = async (file, objectKey, metadata, progressItem) => {
|
const uploadMultipart = async (file, objectKey, metadata, progressItem) => {
|
||||||
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value;
|
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value;
|
||||||
|
|
||||||
|
if (uploadCancelled) throw new Error('Upload cancelled');
|
||||||
|
|
||||||
updateProgressItem(progressItem, { status: 'Initiating...', loaded: 0, total: file.size });
|
updateProgressItem(progressItem, { status: 'Initiating...', loaded: 0, total: file.size });
|
||||||
const initResp = await fetch(multipartInitUrl, {
|
const initResp = await fetch(multipartInitUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -2193,12 +2231,16 @@
|
|||||||
const completeUrl = multipartCompleteTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id);
|
const completeUrl = multipartCompleteTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id);
|
||||||
const abortUrl = multipartAbortTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id);
|
const abortUrl = multipartAbortTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id);
|
||||||
|
|
||||||
|
activeMultipartUpload = { upload_id, abortUrl };
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
|
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
|
||||||
let uploadedBytes = 0;
|
let uploadedBytes = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
|
for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
|
||||||
|
if (uploadCancelled) throw new Error('Upload cancelled');
|
||||||
|
|
||||||
const start = (partNumber - 1) * CHUNK_SIZE;
|
const start = (partNumber - 1) * CHUNK_SIZE;
|
||||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||||
const chunk = file.slice(start, end);
|
const chunk = file.slice(start, end);
|
||||||
@@ -2220,6 +2262,8 @@
|
|||||||
body: chunk
|
body: chunk
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (uploadCancelled) throw new Error('Upload cancelled');
|
||||||
|
|
||||||
if (!partResp.ok) {
|
if (!partResp.ok) {
|
||||||
const err = await partResp.json().catch(() => ({}));
|
const err = await partResp.json().catch(() => ({}));
|
||||||
throw new Error(err.error || `Part ${partNumber} failed`);
|
throw new Error(err.error || `Part ${partNumber} failed`);
|
||||||
@@ -2249,11 +2293,15 @@
|
|||||||
throw new Error(err.error || 'Failed to complete upload');
|
throw new Error(err.error || 'Failed to complete upload');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeMultipartUpload = null;
|
||||||
return await completeResp.json();
|
return await completeResp.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!uploadCancelled) {
|
||||||
try {
|
try {
|
||||||
await fetch(abortUrl, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken || '' } });
|
await fetch(abortUrl, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken || '' } });
|
||||||
} catch {}
|
} catch {}
|
||||||
|
}
|
||||||
|
activeMultipartUpload = null;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2268,9 +2316,15 @@
|
|||||||
if (csrfToken) formData.append('csrf_token', csrfToken);
|
if (csrfToken) formData.append('csrf_token', csrfToken);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
activeXHRs.push(xhr);
|
||||||
xhr.open('POST', uploadForm.action, true);
|
xhr.open('POST', uploadForm.action, true);
|
||||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||||
|
|
||||||
|
const removeXHR = () => {
|
||||||
|
const idx = activeXHRs.indexOf(xhr);
|
||||||
|
if (idx > -1) activeXHRs.splice(idx, 1);
|
||||||
|
};
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', (e) => {
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
updateProgressItem(progressItem, {
|
updateProgressItem(progressItem, {
|
||||||
@@ -2284,6 +2338,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener('load', () => {
|
xhr.addEventListener('load', () => {
|
||||||
|
removeXHR();
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(xhr.responseText);
|
const data = JSON.parse(xhr.responseText);
|
||||||
@@ -2305,8 +2360,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener('error', () => reject(new Error('Network error')));
|
xhr.addEventListener('error', () => { removeXHR(); reject(new Error('Network error')); });
|
||||||
xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));
|
xhr.addEventListener('abort', () => { removeXHR(); reject(new Error('Upload cancelled')); });
|
||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
@@ -2399,7 +2454,7 @@
|
|||||||
if (isProcessingQueue) return;
|
if (isProcessingQueue) return;
|
||||||
isProcessingQueue = true;
|
isProcessingQueue = true;
|
||||||
|
|
||||||
while (uploadQueue.length > 0) {
|
while (uploadQueue.length > 0 && !uploadCancelled) {
|
||||||
const item = uploadQueue.shift();
|
const item = uploadQueue.shift();
|
||||||
const { file, keyPrefix, metadata } = item;
|
const { file, keyPrefix, metadata } = item;
|
||||||
updateQueueListDisplay();
|
updateQueueListDisplay();
|
||||||
@@ -2440,7 +2495,7 @@
|
|||||||
|
|
||||||
isProcessingQueue = false;
|
isProcessingQueue = false;
|
||||||
|
|
||||||
if (uploadQueue.length === 0) {
|
if (uploadQueue.length === 0 && !uploadCancelled) {
|
||||||
finishUploadSession();
|
finishUploadSession();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2507,6 +2562,7 @@
|
|||||||
|
|
||||||
if (!isUploading) {
|
if (!isUploading) {
|
||||||
isUploading = true;
|
isUploading = true;
|
||||||
|
uploadCancelled = false;
|
||||||
uploadSuccessFiles = [];
|
uploadSuccessFiles = [];
|
||||||
uploadErrorFiles = [];
|
uploadErrorFiles = [];
|
||||||
uploadStats = {
|
uploadStats = {
|
||||||
|
|||||||
@@ -2009,12 +2009,19 @@
|
|||||||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
||||||
<span class="fw-semibold" id="floatingUploadTitle">Uploading files...</span>
|
<span class="fw-semibold" id="floatingUploadTitle">Uploading files...</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="floatingUploadExpand" title="Show upload modal">
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button type="button" class="btn btn-outline-danger" id="floatingUploadCancel" title="Cancel upload">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="floatingUploadExpand" title="Show upload modal">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
|
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="progress mb-1" style="height: 6px;">
|
<div class="progress mb-1" style="height: 6px;">
|
||||||
<div class="progress-bar bg-primary" id="floatingUploadProgressBar" role="progressbar" style="width: 0%;"></div>
|
<div class="progress-bar bg-primary" id="floatingUploadProgressBar" role="progressbar" style="width: 0%;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user