MyFSIO v0.2.1 Release #13

Merged
kqjy merged 6 commits from next into main 2026-01-12 08:03:30 +00:00
2 changed files with 75 additions and 12 deletions
Showing only changes of commit 1c30200db0 - Show all commits

View File

@@ -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 = {

View File

@@ -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>