upload-service/web/static/upload.js

284 lines
11 KiB
JavaScript

(() => {
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
const fileListEl = document.getElementById('file-list');
const options = document.getElementById('options');
const slugGroup = document.getElementById('slug-group');
const uploadBtn = document.getElementById('upload-btn');
const progressSection = document.getElementById('progress-section');
const progressOverall = document.getElementById('progress-overall');
const progressFilename = document.getElementById('progress-filename');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const result = document.getElementById('result');
const resultHeading = document.getElementById('result-heading');
const resultSingle = document.getElementById('result-single');
const resultUrl = document.getElementById('result-url');
const copyBtn = document.getElementById('copy-btn');
const resultDelete = document.getElementById('result-delete');
const resultExpiry = document.getElementById('result-expiry');
const resultMulti = document.getElementById('result-multi');
const resultList = document.getElementById('result-list');
const copyAllBtn = document.getElementById('copy-all-btn');
const errorDiv = document.getElementById('error');
let selectedFiles = [];
let uploading = false;
// Drag and drop
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drag-over');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
if (e.dataTransfer.files.length > 0) {
addFiles(e.dataTransfer.files);
}
});
dropzone.addEventListener('click', () => {
if (!uploading) fileInput.click();
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
addFiles(fileInput.files);
}
fileInput.value = '';
});
function addFiles(fileListObj) {
for (const f of fileListObj) {
// Skip duplicates by name+size
if (!selectedFiles.some(s => s.name === f.name && s.size === f.size)) {
selectedFiles.push(f);
}
}
renderFileList();
result.classList.add('hidden');
errorDiv.classList.add('hidden');
}
function removeFile(index) {
selectedFiles.splice(index, 1);
renderFileList();
}
function renderFileList() {
if (selectedFiles.length === 0) {
fileListEl.classList.add('hidden');
options.classList.add('hidden');
dropzone.querySelector('p').innerHTML = 'Drop files here or <label for="file-input" class="link">browse</label>';
return;
}
fileListEl.classList.remove('hidden');
options.classList.remove('hidden');
// Hide custom slug for multi-file uploads
slugGroup.classList.toggle('hidden', selectedFiles.length > 1);
if (selectedFiles.length > 1) {
document.getElementById('slug').value = '';
}
const totalSize = selectedFiles.reduce((sum, f) => sum + f.size, 0);
fileListEl.innerHTML = '<div class="file-list-header">' +
'<span>' + selectedFiles.length + ' file' + (selectedFiles.length > 1 ? 's' : '') +
' (' + formatSize(totalSize) + ')</span>' +
'<button class="file-list-clear link" type="button">Clear all</button>' +
'</div>' +
selectedFiles.map((f, i) =>
'<div class="file-list-item">' +
'<span class="file-list-name" title="' + escapeAttr(f.name) + '">' + escapeHtml(f.name) + '</span>' +
'<span class="file-list-size">' + formatSize(f.size) + '</span>' +
'<button class="file-list-remove" data-index="' + i + '" type="button" title="Remove">&times;</button>' +
'</div>'
).join('');
fileListEl.querySelector('.file-list-clear').addEventListener('click', () => {
selectedFiles = [];
renderFileList();
});
fileListEl.querySelectorAll('.file-list-remove').forEach(btn => {
btn.addEventListener('click', () => removeFile(parseInt(btn.dataset.index)));
});
dropzone.querySelector('p').innerHTML = 'Drop more files or <label for="file-input" class="link">browse</label>';
}
uploadBtn.addEventListener('click', () => {
if (selectedFiles.length === 0 || uploading) return;
uploadAll();
});
async function uploadAll() {
uploading = true;
const files = [...selectedFiles];
const results = [];
const errors = [];
options.classList.add('hidden');
fileListEl.classList.add('hidden');
dropzone.classList.add('hidden');
progressSection.classList.remove('hidden');
errorDiv.classList.add('hidden');
result.classList.add('hidden');
const expires = document.getElementById('expires').value;
const password = document.getElementById('password').value;
const slug = document.getElementById('slug').value.trim();
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (files.length > 1) {
progressOverall.textContent = 'File ' + (i + 1) + ' of ' + files.length;
} else {
progressOverall.textContent = '';
}
progressFilename.textContent = file.name;
progressFill.style.width = '0%';
progressText.textContent = '0%';
try {
const data = await uploadOne(file, {
expires,
password,
slug: files.length === 1 ? slug : ''
});
results.push(data);
} catch (err) {
errors.push({ name: file.name, error: err.message });
}
}
progressSection.classList.add('hidden');
uploading = false;
if (results.length > 0) {
showResults(results);
}
if (errors.length > 0) {
showError(errors.map(e => e.name + ': ' + e.error).join('\n'));
}
// Reset
selectedFiles = [];
dropzone.classList.remove('hidden');
dropzone.querySelector('p').innerHTML = 'Drop files here or <label for="file-input" class="link">browse</label>';
}
function uploadOne(file, opts) {
return new Promise((resolve, reject) => {
const formData = new FormData();
if (opts.slug) formData.append('slug', opts.slug);
formData.append('file', file);
if (opts.expires) formData.append('expires_in', opts.expires);
if (opts.password) formData.append('password', opts.password);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
progressFill.style.width = pct + '%';
progressText.textContent = pct + '% — ' + formatSize(e.loaded) + ' / ' + formatSize(e.total);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 201) {
resolve(JSON.parse(xhr.responseText));
} else if (xhr.status === 429) {
// Rate limited — retry after a short delay
setTimeout(() => {
uploadOne(file, opts).then(resolve, reject);
}, 1500);
} else {
reject(new Error(xhr.responseText || 'Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', '/upload');
xhr.send(formData);
});
}
function showResults(results) {
result.classList.remove('hidden');
if (results.length === 1) {
resultHeading.textContent = 'Uploaded!';
resultSingle.classList.remove('hidden');
resultMulti.classList.add('hidden');
const data = results[0];
resultUrl.value = data.url;
resultDelete.textContent = 'Delete: curl -X DELETE -H "Authorization: Bearer ' + data.delete_token + '" ' + data.delete_url;
resultExpiry.textContent = data.expires_at
? 'Expires: ' + new Date(data.expires_at).toLocaleString()
: '';
} else {
resultHeading.textContent = results.length + ' files uploaded!';
resultSingle.classList.add('hidden');
resultMulti.classList.remove('hidden');
resultList.innerHTML = results.map(data =>
'<div class="result-item">' +
'<a href="' + escapeAttr(data.url) + '" target="_blank" class="result-item-name">' + escapeHtml(data.filename) + '</a>' +
'<span class="result-item-url">' + escapeHtml(data.url) + '</span>' +
'</div>'
).join('');
copyAllBtn.onclick = () => {
const urls = results.map(d => d.url).join('\n');
navigator.clipboard.writeText(urls).then(() => {
copyAllBtn.textContent = 'Copied!';
setTimeout(() => { copyAllBtn.textContent = 'Copy all URLs'; }, 2000);
});
};
}
}
function showError(msg) {
errorDiv.textContent = msg;
errorDiv.classList.remove('hidden');
}
copyBtn.addEventListener('click', () => {
resultUrl.select();
navigator.clipboard.writeText(resultUrl.value).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000);
});
});
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function escapeAttr(s) {
return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
})();