Fix multipart upload failure; Improve upload UX;
This commit is contained in:
@@ -403,7 +403,7 @@
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary preset-btn {% if preset_choice == 'public' %}active{% endif %}" data-preset="public">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5z"/>
|
||||
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8Zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022ZM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816ZM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"/>
|
||||
</svg>
|
||||
Public Read
|
||||
</button>
|
||||
@@ -628,27 +628,25 @@
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_versioning %}
|
||||
<form method="post" action="{{ url_for('ui.update_bucket_versioning', bucket_name=bucket_name) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if versioning_enabled %}
|
||||
<input type="hidden" name="state" value="suspend" />
|
||||
<button class="btn btn-outline-danger" type="submit" data-confirm-suspend>
|
||||
<button class="btn btn-outline-danger" type="button" data-bs-toggle="modal" data-bs-target="#suspendVersioningModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
|
||||
</svg>
|
||||
Suspend Versioning
|
||||
</button>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('ui.update_bucket_versioning', bucket_name=bucket_name) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="state" value="enable" />
|
||||
<button class="btn btn-success" type="submit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10.854 8.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L7.5 10.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
Enable Versioning
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="text-muted mb-2" viewBox="0 0 16 16">
|
||||
@@ -1656,7 +1654,7 @@
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<div class="bg-primary bg-opacity-10 rounded p-2 me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="text-primary" viewBox="0 0 16 16">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077z"/>
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855.173-.324.33-.682.468-1.068H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1829,6 +1827,27 @@
|
||||
<div class="col-12">
|
||||
<div class="upload-progress-stack" data-upload-progress></div>
|
||||
</div>
|
||||
<div class="col-12 d-none" id="uploadQueueContainer">
|
||||
<div class="border rounded p-2" style="max-height: 150px; overflow-y: auto;">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="small fw-medium text-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 0 1 2 15.5V2zm2-1a1 1 0 0 0-1 1v12.566l4.723-2.482a.5.5 0 0 1 .554 0L13 14.566V2a1 1 0 0 0-1-1H4z"/>
|
||||
</svg>
|
||||
Queued files
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary-subtle text-secondary" id="uploadQueueCount">0</span>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-1" id="clearUploadQueueBtn" title="Clear queue">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-unstyled mb-0 small" id="uploadQueueList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 d-none" id="bulkUploadProgress">
|
||||
<div class="alert alert-info small mb-0">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
@@ -1874,6 +1893,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="floatingUploadProgress" class="floating-upload-progress d-none">
|
||||
<div class="floating-upload-content">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
||||
<span class="fw-semibold" id="floatingUploadTitle">Uploading files...</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm 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">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress mb-1" style="height: 6px;">
|
||||
<div class="progress-bar bg-primary" id="floatingUploadProgressBar" role="progressbar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="text-muted small" id="floatingUploadStatus">Preparing...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="deleteBucketModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
@@ -2092,6 +2131,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="suspendVersioningModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-warning" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
Suspend Versioning
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning d-flex align-items-start mb-3" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 me-2 mt-1" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Important:</strong> Suspending versioning will stop creating new versions for uploaded objects, but <strong>existing versioned objects will remain stored</strong> and continue to consume storage space.
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small mb-0">To permanently remove old versions, you must re-enable versioning and manually delete them, or configure a lifecycle rule to expire noncurrent versions.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="POST" action="{{ url_for('ui.update_bucket_versioning', bucket_name=bucket_name) }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="state" value="suspend" />
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
|
||||
</svg>
|
||||
Suspend Versioning
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="disableReplicationModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
@@ -2318,100 +2397,37 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', filename='js/bucket-detail-utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bucket-detail-upload.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bucket-detail-operations.js') }}"></script>
|
||||
<script>
|
||||
function setupJsonAutoIndent(textarea) {
|
||||
if (!textarea) return;
|
||||
|
||||
textarea.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
const value = this.value;
|
||||
|
||||
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
||||
const currentLine = value.substring(lineStart, start);
|
||||
|
||||
const indentMatch = currentLine.match(/^(\s*)/);
|
||||
let indent = indentMatch ? indentMatch[1] : '';
|
||||
|
||||
const trimmedLine = currentLine.trim();
|
||||
const lastChar = trimmedLine.slice(-1);
|
||||
|
||||
let newIndent = indent;
|
||||
let insertAfter = '';
|
||||
|
||||
if (lastChar === '{' || lastChar === '[') {
|
||||
newIndent = indent + ' ';
|
||||
|
||||
const charAfterCursor = value.substring(start, start + 1).trim();
|
||||
if ((lastChar === '{' && charAfterCursor === '}') ||
|
||||
(lastChar === '[' && charAfterCursor === ']')) {
|
||||
insertAfter = '\n' + indent;
|
||||
}
|
||||
} else if (lastChar === ',' || lastChar === ':') {
|
||||
newIndent = indent;
|
||||
}
|
||||
|
||||
const insertion = '\n' + newIndent + insertAfter;
|
||||
const newValue = value.substring(0, start) + insertion + value.substring(end);
|
||||
|
||||
this.value = newValue;
|
||||
|
||||
const newCursorPos = start + 1 + newIndent.length;
|
||||
this.selectionStart = this.selectionEnd = newCursorPos;
|
||||
|
||||
this.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const { formatBytes, escapeHtml, fallbackCopy, setupJsonAutoIndent } = window.BucketDetailUtils || {
|
||||
formatBytes: (bytes) => {
|
||||
if (!Number.isFinite(bytes)) return `${bytes} bytes`;
|
||||
const units = ['bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let size = bytes;
|
||||
while (size >= 1024 && i < units.length - 1) {
|
||||
size /= 1024;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
|
||||
if (e.shiftKey) {
|
||||
const lineStart = this.value.lastIndexOf('\n', start - 1) + 1;
|
||||
const lineContent = this.value.substring(lineStart, start);
|
||||
if (lineContent.startsWith(' ')) {
|
||||
this.value = this.value.substring(0, lineStart) +
|
||||
this.value.substring(lineStart + 2);
|
||||
this.selectionStart = this.selectionEnd = Math.max(lineStart, start - 2);
|
||||
}
|
||||
} else {
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 2;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
},
|
||||
escapeHtml: (value) => {
|
||||
if (value === null || value === undefined) return '';
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
fallbackCopy: () => false,
|
||||
setupJsonAutoIndent: () => {}
|
||||
};
|
||||
|
||||
setupJsonAutoIndent(document.getElementById('policyDocument'));
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (!Number.isFinite(bytes)) return `${bytes} bytes`;
|
||||
const units = ['bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let size = bytes;
|
||||
while (size >= 1024 && i < units.length - 1) {
|
||||
size /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
};
|
||||
|
||||
const escapeHtml = (value) => {
|
||||
if (value === null || value === undefined) return '';
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const selectAllCheckbox = document.querySelector('[data-select-all]');
|
||||
const bulkDeleteButton = document.querySelector('[data-bulk-delete-trigger]');
|
||||
const bulkDeleteLabel = bulkDeleteButton?.querySelector('[data-bulk-delete-label]');
|
||||
@@ -4137,10 +4153,89 @@
|
||||
const bulkUploadErrorList = document.getElementById('bulkUploadErrorList');
|
||||
const uploadKeyPrefix = document.getElementById('uploadKeyPrefix');
|
||||
const singleFileOptions = document.getElementById('singleFileOptions');
|
||||
const floatingProgress = document.getElementById('floatingUploadProgress');
|
||||
const floatingProgressBar = document.getElementById('floatingUploadProgressBar');
|
||||
const floatingProgressStatus = document.getElementById('floatingUploadStatus');
|
||||
const floatingProgressTitle = document.getElementById('floatingUploadTitle');
|
||||
const floatingProgressExpand = document.getElementById('floatingUploadExpand');
|
||||
const uploadQueueContainer = document.getElementById('uploadQueueContainer');
|
||||
const uploadQueueList = document.getElementById('uploadQueueList');
|
||||
const uploadQueueCount = document.getElementById('uploadQueueCount');
|
||||
const clearUploadQueueBtn = document.getElementById('clearUploadQueueBtn');
|
||||
let isUploading = false;
|
||||
let uploadQueue = [];
|
||||
let uploadStats = {
|
||||
totalFiles: 0,
|
||||
completedFiles: 0,
|
||||
totalBytes: 0,
|
||||
uploadedBytes: 0,
|
||||
currentFileBytes: 0,
|
||||
currentFileLoaded: 0,
|
||||
currentFileName: ''
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (isUploading) {
|
||||
e.preventDefault();
|
||||
e.returnValue = 'Upload in progress. Are you sure you want to leave?';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
const showFloatingProgress = () => {
|
||||
if (floatingProgress) {
|
||||
floatingProgress.classList.remove('d-none');
|
||||
}
|
||||
};
|
||||
|
||||
const hideFloatingProgress = () => {
|
||||
if (floatingProgress) {
|
||||
floatingProgress.classList.add('d-none');
|
||||
}
|
||||
};
|
||||
|
||||
const updateFloatingProgress = () => {
|
||||
const { totalFiles, completedFiles, totalBytes, uploadedBytes, currentFileLoaded, currentFileName } = uploadStats;
|
||||
const effectiveUploaded = uploadedBytes + currentFileLoaded;
|
||||
|
||||
if (floatingProgressBar && totalBytes > 0) {
|
||||
const percent = Math.round((effectiveUploaded / totalBytes) * 100);
|
||||
floatingProgressBar.style.width = `${percent}%`;
|
||||
}
|
||||
if (floatingProgressStatus) {
|
||||
const bytesText = `${formatBytes(effectiveUploaded)} / ${formatBytes(totalBytes)}`;
|
||||
const queuedCount = uploadQueue.length;
|
||||
let statusText = `${completedFiles}/${totalFiles} files`;
|
||||
if (queuedCount > 0) {
|
||||
statusText += ` (+${queuedCount} queued)`;
|
||||
}
|
||||
statusText += ` • ${bytesText}`;
|
||||
floatingProgressStatus.textContent = statusText;
|
||||
}
|
||||
if (floatingProgressTitle) {
|
||||
const remaining = totalFiles - completedFiles;
|
||||
const queuedCount = uploadQueue.length;
|
||||
let title = `Uploading ${remaining} file${remaining !== 1 ? 's' : ''}`;
|
||||
if (queuedCount > 0) {
|
||||
title += ` (+${queuedCount} queued)`;
|
||||
}
|
||||
floatingProgressTitle.textContent = title + '...';
|
||||
}
|
||||
};
|
||||
|
||||
floatingProgressExpand?.addEventListener('click', () => {
|
||||
if (uploadModal) {
|
||||
uploadModal.show();
|
||||
}
|
||||
});
|
||||
|
||||
const refreshUploadDropLabel = () => {
|
||||
if (!uploadDropZoneLabel) return;
|
||||
if (isUploading) {
|
||||
uploadDropZoneLabel.textContent = 'Drop files here to add to queue';
|
||||
if (singleFileOptions) singleFileOptions.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
const files = uploadFileInput.files;
|
||||
if (!files || files.length === 0) {
|
||||
uploadDropZoneLabel.textContent = 'No file selected';
|
||||
@@ -4156,6 +4251,16 @@
|
||||
|
||||
const updateUploadBtnText = () => {
|
||||
if (!uploadBtnText) return;
|
||||
if (isUploading) {
|
||||
const files = uploadFileInput.files;
|
||||
if (files && files.length > 0) {
|
||||
uploadBtnText.textContent = `Add ${files.length} to queue`;
|
||||
if (uploadSubmitBtn) uploadSubmitBtn.disabled = false;
|
||||
} else {
|
||||
uploadBtnText.textContent = 'Uploading...';
|
||||
}
|
||||
return;
|
||||
}
|
||||
const files = uploadFileInput.files;
|
||||
if (!files || files.length <= 1) {
|
||||
uploadBtnText.textContent = 'Upload';
|
||||
@@ -4179,6 +4284,7 @@
|
||||
uploadDropZone.style.pointerEvents = '';
|
||||
}
|
||||
isUploading = false;
|
||||
hideFloatingProgress();
|
||||
};
|
||||
|
||||
const MULTIPART_THRESHOLD = 8 * 1024 * 1024;
|
||||
@@ -4275,10 +4381,15 @@
|
||||
loaded: uploadedBytes,
|
||||
total: file.size
|
||||
});
|
||||
uploadStats.currentFileLoaded = uploadedBytes;
|
||||
updateFloatingProgress();
|
||||
|
||||
const partResp = await fetch(`${partUrl}?partNumber=${partNumber}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'X-CSRFToken': csrfToken || '' },
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken || '',
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
body: chunk
|
||||
});
|
||||
|
||||
@@ -4295,6 +4406,8 @@
|
||||
loaded: uploadedBytes,
|
||||
total: file.size
|
||||
});
|
||||
uploadStats.currentFileLoaded = uploadedBytes;
|
||||
updateFloatingProgress();
|
||||
}
|
||||
|
||||
updateProgressItem(progressItem, { status: 'Completing...', loaded: file.size, total: file.size });
|
||||
@@ -4338,6 +4451,8 @@
|
||||
loaded: e.loaded,
|
||||
total: e.total
|
||||
});
|
||||
uploadStats.currentFileLoaded = e.loaded;
|
||||
updateFloatingProgress();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4399,18 +4514,148 @@
|
||||
const setUploadLockState = (locked) => {
|
||||
if (uploadDropZone) {
|
||||
uploadDropZone.classList.toggle('upload-locked', locked);
|
||||
uploadDropZone.style.pointerEvents = locked ? 'none' : '';
|
||||
}
|
||||
if (uploadFileInput) {
|
||||
uploadFileInput.disabled = locked;
|
||||
};
|
||||
|
||||
let uploadSuccessFiles = [];
|
||||
let uploadErrorFiles = [];
|
||||
let isProcessingQueue = false;
|
||||
|
||||
const updateQueueListDisplay = () => {
|
||||
if (!uploadQueueList || !uploadQueueContainer || !uploadQueueCount) return;
|
||||
if (uploadQueue.length === 0) {
|
||||
uploadQueueContainer.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
uploadQueueContainer.classList.remove('d-none');
|
||||
uploadQueueCount.textContent = uploadQueue.length;
|
||||
uploadQueueList.innerHTML = uploadQueue.map((item, idx) => `
|
||||
<li class="d-flex align-items-center justify-content-between py-1 ${idx > 0 ? 'border-top' : ''}">
|
||||
<span class="text-truncate me-2" style="max-width: 300px;" title="${escapeHtml(item.file.name)}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="text-muted me-1" viewBox="0 0 16 16">
|
||||
<path d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3z"/>
|
||||
</svg>
|
||||
${escapeHtml(item.file.name)}
|
||||
</span>
|
||||
<span class="text-muted">${formatBytes(item.file.size)}</span>
|
||||
</li>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const addFilesToQueue = (files, keyPrefix, metadata) => {
|
||||
for (const file of files) {
|
||||
uploadQueue.push({ file, keyPrefix, metadata });
|
||||
uploadStats.totalFiles++;
|
||||
uploadStats.totalBytes += file.size;
|
||||
}
|
||||
updateFloatingProgress();
|
||||
updateQueueListDisplay();
|
||||
};
|
||||
|
||||
const clearUploadQueue = () => {
|
||||
const clearedCount = uploadQueue.length;
|
||||
if (clearedCount === 0) return;
|
||||
for (const item of uploadQueue) {
|
||||
uploadStats.totalFiles--;
|
||||
uploadStats.totalBytes -= item.file.size;
|
||||
}
|
||||
uploadQueue.length = 0;
|
||||
updateFloatingProgress();
|
||||
updateQueueListDisplay();
|
||||
};
|
||||
|
||||
if (clearUploadQueueBtn) {
|
||||
clearUploadQueueBtn.addEventListener('click', clearUploadQueue);
|
||||
}
|
||||
|
||||
const processUploadQueue = async () => {
|
||||
if (isProcessingQueue) return;
|
||||
isProcessingQueue = true;
|
||||
|
||||
while (uploadQueue.length > 0) {
|
||||
const item = uploadQueue.shift();
|
||||
const { file, keyPrefix, metadata } = item;
|
||||
updateQueueListDisplay();
|
||||
|
||||
uploadStats.currentFileName = file.name;
|
||||
uploadStats.currentFileBytes = file.size;
|
||||
uploadStats.currentFileLoaded = 0;
|
||||
|
||||
if (bulkUploadCounter) {
|
||||
const queuedCount = uploadQueue.length;
|
||||
let counterText = `${uploadStats.completedFiles + 1}/${uploadStats.totalFiles}`;
|
||||
if (queuedCount > 0) {
|
||||
counterText += ` (+${queuedCount} queued)`;
|
||||
}
|
||||
bulkUploadCounter.textContent = counterText;
|
||||
}
|
||||
if (bulkUploadCurrentFile) {
|
||||
bulkUploadCurrentFile.textContent = `Uploading: ${file.name}`;
|
||||
}
|
||||
if (bulkUploadProgressBar) {
|
||||
const percent = Math.round(((uploadStats.completedFiles + 1) / uploadStats.totalFiles) * 100);
|
||||
bulkUploadProgressBar.style.width = `${percent}%`;
|
||||
}
|
||||
updateFloatingProgress();
|
||||
|
||||
try {
|
||||
await uploadSingleFile(file, keyPrefix, metadata);
|
||||
uploadSuccessFiles.push(file.name);
|
||||
} catch (error) {
|
||||
uploadErrorFiles.push({ name: file.name, error: error.message || 'Unknown error' });
|
||||
}
|
||||
|
||||
uploadStats.uploadedBytes += file.size;
|
||||
uploadStats.completedFiles++;
|
||||
uploadStats.currentFileLoaded = 0;
|
||||
updateFloatingProgress();
|
||||
}
|
||||
|
||||
isProcessingQueue = false;
|
||||
|
||||
if (uploadQueue.length === 0) {
|
||||
finishUploadSession();
|
||||
}
|
||||
};
|
||||
|
||||
const finishUploadSession = () => {
|
||||
if (bulkUploadProgress) bulkUploadProgress.classList.add('d-none');
|
||||
if (bulkUploadResults) bulkUploadResults.classList.remove('d-none');
|
||||
|
||||
if (bulkUploadSuccessCount) bulkUploadSuccessCount.textContent = uploadSuccessFiles.length;
|
||||
if (uploadSuccessFiles.length === 0 && bulkUploadSuccessAlert) {
|
||||
bulkUploadSuccessAlert.classList.add('d-none');
|
||||
}
|
||||
|
||||
if (uploadErrorFiles.length > 0) {
|
||||
if (bulkUploadErrorCount) bulkUploadErrorCount.textContent = uploadErrorFiles.length;
|
||||
if (bulkUploadErrorAlert) bulkUploadErrorAlert.classList.remove('d-none');
|
||||
if (bulkUploadErrorList) {
|
||||
bulkUploadErrorList.innerHTML = uploadErrorFiles
|
||||
.map(f => `<li><strong>${escapeHtml(f.name)}</strong>: ${escapeHtml(f.error)}</li>`)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
isUploading = false;
|
||||
setUploadLockState(false);
|
||||
refreshUploadDropLabel();
|
||||
updateUploadBtnText();
|
||||
updateQueueListDisplay();
|
||||
|
||||
if (uploadSuccessFiles.length > 0) {
|
||||
if (uploadBtnText) uploadBtnText.textContent = 'Refreshing...';
|
||||
const objectsTabUrl = window.location.pathname + '?tab=objects';
|
||||
window.setTimeout(() => window.location.href = objectsTabUrl, 800);
|
||||
} else {
|
||||
if (uploadSubmitBtn) uploadSubmitBtn.disabled = false;
|
||||
if (uploadFileInput) uploadFileInput.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const performBulkUpload = async (files) => {
|
||||
if (isUploading || !files || files.length === 0) return;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
isUploading = true;
|
||||
setUploadLockState(true);
|
||||
const keyPrefix = (uploadKeyPrefix?.value || '').trim();
|
||||
const metadataRaw = uploadForm.querySelector('textarea[name="metadata"]')?.value?.trim();
|
||||
let metadata = null;
|
||||
@@ -4419,78 +4664,52 @@
|
||||
metadata = JSON.parse(metadataRaw);
|
||||
} catch {
|
||||
showMessage({ title: 'Invalid metadata', body: 'Metadata must be valid JSON.', variant: 'danger' });
|
||||
resetUploadUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (bulkUploadProgress) bulkUploadProgress.classList.remove('d-none');
|
||||
if (bulkUploadResults) bulkUploadResults.classList.add('d-none');
|
||||
if (uploadSubmitBtn) uploadSubmitBtn.disabled = true;
|
||||
if (uploadFileInput) uploadFileInput.disabled = true;
|
||||
if (!isUploading) {
|
||||
isUploading = true;
|
||||
uploadSuccessFiles = [];
|
||||
uploadErrorFiles = [];
|
||||
uploadStats = {
|
||||
totalFiles: 0,
|
||||
completedFiles: 0,
|
||||
totalBytes: 0,
|
||||
uploadedBytes: 0,
|
||||
currentFileBytes: 0,
|
||||
currentFileLoaded: 0,
|
||||
currentFileName: ''
|
||||
};
|
||||
|
||||
const successFiles = [];
|
||||
const errorFiles = [];
|
||||
const total = files.length;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const file = files[i];
|
||||
const current = i + 1;
|
||||
|
||||
if (bulkUploadCounter) bulkUploadCounter.textContent = `${current}/${total}`;
|
||||
if (bulkUploadCurrentFile) bulkUploadCurrentFile.textContent = `Uploading: ${file.name}`;
|
||||
if (bulkUploadProgressBar) {
|
||||
const percent = Math.round((current / total) * 100);
|
||||
bulkUploadProgressBar.style.width = `${percent}%`;
|
||||
}
|
||||
|
||||
try {
|
||||
await uploadSingleFile(file, keyPrefix, metadata);
|
||||
successFiles.push(file.name);
|
||||
} catch (error) {
|
||||
errorFiles.push({ name: file.name, error: error.message || 'Unknown error' });
|
||||
}
|
||||
if (bulkUploadProgress) bulkUploadProgress.classList.remove('d-none');
|
||||
if (bulkUploadResults) bulkUploadResults.classList.add('d-none');
|
||||
if (uploadSubmitBtn) uploadSubmitBtn.disabled = true;
|
||||
refreshUploadDropLabel();
|
||||
updateUploadBtnText();
|
||||
}
|
||||
|
||||
if (bulkUploadProgress) bulkUploadProgress.classList.add('d-none');
|
||||
if (bulkUploadResults) bulkUploadResults.classList.remove('d-none');
|
||||
|
||||
if (bulkUploadSuccessCount) bulkUploadSuccessCount.textContent = successFiles.length;
|
||||
if (successFiles.length === 0 && bulkUploadSuccessAlert) {
|
||||
bulkUploadSuccessAlert.classList.add('d-none');
|
||||
}
|
||||
|
||||
if (errorFiles.length > 0) {
|
||||
if (bulkUploadErrorCount) bulkUploadErrorCount.textContent = errorFiles.length;
|
||||
if (bulkUploadErrorAlert) bulkUploadErrorAlert.classList.remove('d-none');
|
||||
if (bulkUploadErrorList) {
|
||||
bulkUploadErrorList.innerHTML = errorFiles
|
||||
.map(f => `<li><strong>${escapeHtml(f.name)}</strong>: ${escapeHtml(f.error)}</li>`)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
const fileCount = files.length;
|
||||
addFilesToQueue(Array.from(files), keyPrefix, metadata);
|
||||
|
||||
isUploading = false;
|
||||
setUploadLockState(false);
|
||||
|
||||
if (successFiles.length > 0) {
|
||||
if (uploadBtnText) uploadBtnText.textContent = 'Refreshing...';
|
||||
window.setTimeout(() => window.location.reload(), 800);
|
||||
} else {
|
||||
if (uploadSubmitBtn) uploadSubmitBtn.disabled = false;
|
||||
if (uploadFileInput) uploadFileInput.disabled = false;
|
||||
if (uploadFileInput) {
|
||||
uploadFileInput.value = '';
|
||||
}
|
||||
refreshUploadDropLabel();
|
||||
updateUploadBtnText();
|
||||
|
||||
processUploadQueue();
|
||||
};
|
||||
|
||||
refreshUploadDropLabel();
|
||||
uploadFileInput.addEventListener('change', () => {
|
||||
if (isUploading) return;
|
||||
refreshUploadDropLabel();
|
||||
updateUploadBtnText();
|
||||
resetUploadUI();
|
||||
if (!isUploading) {
|
||||
resetUploadUI();
|
||||
}
|
||||
});
|
||||
uploadDropZone?.addEventListener('click', () => {
|
||||
if (isUploading) return;
|
||||
uploadFileInput?.click();
|
||||
});
|
||||
|
||||
@@ -4537,11 +4756,25 @@
|
||||
}
|
||||
});
|
||||
|
||||
uploadModalEl?.addEventListener('hide.bs.modal', (event) => {
|
||||
if (isUploading) {
|
||||
showFloatingProgress();
|
||||
}
|
||||
});
|
||||
|
||||
uploadModalEl?.addEventListener('hidden.bs.modal', () => {
|
||||
resetUploadUI();
|
||||
uploadFileInput.value = '';
|
||||
refreshUploadDropLabel();
|
||||
updateUploadBtnText();
|
||||
if (!isUploading) {
|
||||
resetUploadUI();
|
||||
uploadFileInput.value = '';
|
||||
refreshUploadDropLabel();
|
||||
updateUploadBtnText();
|
||||
}
|
||||
});
|
||||
|
||||
uploadModalEl?.addEventListener('show.bs.modal', () => {
|
||||
if (isUploading) {
|
||||
hideFloatingProgress();
|
||||
}
|
||||
});
|
||||
|
||||
const preventDefaults = (event) => {
|
||||
@@ -4554,7 +4787,6 @@
|
||||
['dragenter', 'dragover'].forEach((eventName) => {
|
||||
target.addEventListener(eventName, (event) => {
|
||||
preventDefaults(event);
|
||||
if (isUploading) return;
|
||||
if (highlightClass) {
|
||||
target.classList.add(highlightClass);
|
||||
}
|
||||
@@ -4569,14 +4801,19 @@
|
||||
});
|
||||
});
|
||||
target.addEventListener('drop', (event) => {
|
||||
if (isUploading) return;
|
||||
if (!event.dataTransfer?.files?.length || !uploadFileInput) {
|
||||
if (!event.dataTransfer?.files?.length) {
|
||||
return;
|
||||
}
|
||||
uploadFileInput.files = event.dataTransfer.files;
|
||||
uploadFileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (autoOpenModal && uploadModal) {
|
||||
uploadModal.show();
|
||||
if (isUploading) {
|
||||
performBulkUpload(event.dataTransfer.files);
|
||||
} else {
|
||||
if (uploadFileInput) {
|
||||
uploadFileInput.files = event.dataTransfer.files;
|
||||
uploadFileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (autoOpenModal && uploadModal) {
|
||||
uploadModal.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user