Files
ReSendit/static/js/upload.js

331 lines
8.8 KiB
JavaScript

const zone = document.getElementById('drop-zone');
const input = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const cancelBtn = document.getElementById('cancelBtn');
const progressText = document.getElementById("progress-text");
const speedText = document.getElementById("speed-text");
const etaText = document.getElementById("eta-text");
const progressBar = document.getElementById("progress-bar");
const progressContainer = document.getElementById("progress-container");
let currentXhr = null;
const CHUNK_THRESHOLD = 1024 * 1024 * 1024;
const CHUNK_SIZE = 10 * 1024 * 1024;
const MAX_PARALLEL_UPLOADS = 4;
const MAX_RETRIES = 3;
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
function formatTime(seconds) {
if (!isFinite(seconds) || seconds < 0) return "--:--";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return [
h > 0 ? h : null,
(h > 0 ? m.toString().padStart(2, '0') : m),
s.toString().padStart(2, '0')
].filter(Boolean).join(':');
}
function startUI() {
uploadBtn.disabled = true;
uploadBtn.innerText = "UPLOADING...";
cancelBtn.classList.remove('hidden');
progressContainer.classList.remove("hidden");
progressText.classList.remove("hidden");
document.getElementById("stats-text").classList.remove("hidden");
}
function updateProgress(loaded, total, startTime) {
const percent = Math.round((loaded / total) * 100);
progressBar.style.width = percent + "%";
progressText.innerText = percent + "%";
const elapsed = (Date.now() - startTime) / 1000;
if (elapsed <= 0) return;
const speed = loaded / elapsed;
const remaining = total - loaded;
speedText.innerText = formatBytes(speed) + "/S";
etaText.innerText = formatTime(remaining / speed);
}
function redirect(data) {
const key = data.view_key;
if (!key) {
alert("Invalid server response");
return;
}
window.location.href = "/f/" + key;
}
zone.onclick = () => input.click();
zone.ondragover = e => {
e.preventDefault();
zone.classList.add('active');
};
zone.ondragleave = () => zone.classList.remove('active');
zone.ondrop = e => {
e.preventDefault();
zone.classList.remove('active');
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
input.dispatchEvent(new Event('change'));
}
};
input.onchange = () => {
const files = Array.from(input.files || []);
if (!files.length) {
uploadBtn.disabled = true;
return;
}
const total = files.reduce((a, f) => a + f.size, 0);
document.getElementById('dz-text').innerText =
files.length === 1
? `${files[0].name} [${formatBytes(files[0].size)}]`
: `${files.length} FILES [${formatBytes(total)}]`;
uploadBtn.disabled = false;
};
uploadBtn.onclick = () => {
const files = input.files;
if (!files.length) return;
if (files.length === 1 && files[0].size > CHUNK_THRESHOLD) {
uploadChunked(files[0]);
} else if (files.length === 1) {
uploadSingle(files[0]);
} else {
uploadMulti(files);
}
};
cancelBtn.onclick = e => {
e.stopPropagation();
if (currentXhr) currentXhr.abort();
localStorage.clear();
location.reload();
};
function commonFormData() {
const fd = new FormData();
fd.append("once", document.getElementById("once").checked ? "true" : "false");
fd.append("duration", parseInt(document.getElementById("duration").value, 10));
return fd;
}
function uploadSingle(file) {
startUI();
const fd = commonFormData();
fd.append("file", file);
const xhr = new XMLHttpRequest();
currentXhr = xhr;
const startTime = Date.now();
xhr.upload.onprogress = e => {
if (e.lengthComputable) updateProgress(e.loaded, file.size, startTime);
};
xhr.onload = () => {
try {
if (xhr.status < 200 || xhr.status >= 300) throw new Error();
redirect(JSON.parse(xhr.responseText));
} catch {
alert("Server error");
}
};
xhr.onerror = () => {
if (xhr.statusText !== "abort") {
alert("Upload failed");
location.reload();
}
};
xhr.open("POST", "/api/files/upload");
xhr.send(fd);
}
function uploadMulti(files) {
startUI();
const fd = commonFormData();
const list = Array.from(files);
list.forEach(f => fd.append("files", f));
const total = list.reduce((a, f) => a + f.size, 0);
const xhr = new XMLHttpRequest();
currentXhr = xhr;
const startTime = Date.now();
xhr.upload.onprogress = e => {
if (e.lengthComputable) updateProgress(e.loaded, total, startTime);
};
xhr.onload = () => {
try {
if (xhr.status < 200 || xhr.status >= 300) throw new Error();
redirect(JSON.parse(xhr.responseText));
} catch {
alert("Server error");
}
};
xhr.onerror = () => {
if (xhr.statusText !== "abort") {
alert("Upload failed");
location.reload();
}
};
xhr.open("POST", "/api/files/upload-multi");
xhr.send(fd);
}
async function uploadChunked(file) {
startUI();
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const initRes = await fetch("/api/files/upload/init", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
filename: file.name,
totalChunks,
size: file.size
})
});
const { fileId } = await initRes.json();
let uploadedBytes = 0;
const startTime = Date.now();
const chunks = Array.from({ length: totalChunks }, (_, i) => ({
index: i,
start: i * CHUNK_SIZE,
end: Math.min((i + 1) * CHUNK_SIZE, file.size),
retries: 0,
uploading: false,
done: false
}));
let active = 0;
let completed = 0;
function uploadChunk(chunk) {
return new Promise((res, rej) => {
const blob = file.slice(chunk.start, chunk.end);
const fd = new FormData();
fd.append("chunk", blob);
const xhr = new XMLHttpRequest();
currentXhr = xhr;
let last = 0;
xhr.upload.onprogress = e => {
if (!e.lengthComputable) return;
const delta = e.loaded - last;
last = e.loaded;
uploadedBytes += delta;
updateProgress(uploadedBytes, file.size, startTime);
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
chunk.done = true;
completed++;
res();
} else {
rej();
}
};
xhr.onerror = rej;
xhr.open("POST", "/api/files/upload/chunk");
xhr.setRequestHeader("fileId", fileId);
xhr.setRequestHeader("chunkIndex", chunk.index);
xhr.send(fd);
});
}
return new Promise((resolve, reject) => {
function next() {
if (completed === totalChunks) return finish();
while (active < MAX_PARALLEL_UPLOADS) {
const chunk = chunks.find(c => !c.done && !c.uploading);
if (!chunk) break;
chunk.uploading = true;
active++;
uploadChunk(chunk)
.then(() => {
active--;
next();
})
.catch(() => {
active--;
if (chunk.retries++ < MAX_RETRIES) {
chunk.uploading = false;
} else {
reject();
}
next();
});
}
}
async function finish() {
try {
const res = await fetch("/api/files/upload/complete", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
fileId,
filename: file.name,
totalChunks
})
});
redirect(await res.json());
resolve();
} catch {
reject();
}
}
next();
});
}
function copy(id) {
const el = document.getElementById(id);
el.select();
document.execCommand('copy');
}