422 lines
11 KiB
JavaScript
422 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
const STORAGE_KEY = "filehost.uploads";
|
|
|
|
const dropzone = $("dropzone");
|
|
const fileInput = $("fileInput");
|
|
const chosenFile = $("chosenFile");
|
|
const pwInput = $("pwInput");
|
|
const uploadBtn = $("uploadBtn");
|
|
const newUploadBtn = $("newUploadBtn");
|
|
const clearListBtn = $("clearListBtn");
|
|
const progress = $("progress");
|
|
const progressBar = $("progressBar");
|
|
const progressPct = $("progressPct");
|
|
const progressName = $("progressName");
|
|
const result = $("result");
|
|
const resultUrl = $("resultUrl");
|
|
const resultName = $("resultName");
|
|
const copyBtn = $("copyBtn");
|
|
const notice = $("notice");
|
|
const fileList = $("fileList");
|
|
const fileCount = $("fileCount");
|
|
const emptyState = $("emptyState");
|
|
|
|
let currentFile = null;
|
|
let uploading = false;
|
|
|
|
function formatBytes(bytes) {
|
|
if (!Number.isFinite(bytes) || bytes < 0) return "-";
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
const units = ["KB", "MB", "GB", "TB"];
|
|
let value = bytes;
|
|
let i = -1;
|
|
do {
|
|
value /= 1024;
|
|
i += 1;
|
|
} while (value >= 1024 && i < units.length - 1);
|
|
return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[i]}`;
|
|
}
|
|
|
|
function showNotice(message, kind) {
|
|
notice.textContent = message;
|
|
notice.className = kind;
|
|
}
|
|
|
|
function clearNotice() {
|
|
notice.className = "hidden";
|
|
notice.textContent = "";
|
|
}
|
|
|
|
function setProgress(loaded, total) {
|
|
const pct = total > 0 ? Math.round((loaded / total) * 100) : 0;
|
|
progressBar.style.width = `${pct}%`;
|
|
progressPct.textContent = `${pct}%`;
|
|
}
|
|
|
|
function reset() {
|
|
uploading = false;
|
|
currentFile = null;
|
|
result.classList.add("hidden");
|
|
progress.classList.add("hidden");
|
|
progressBar.style.width = "0%";
|
|
progressPct.textContent = "0%";
|
|
chosenFile.classList.add("hidden");
|
|
chosenFile.textContent = "";
|
|
fileInput.value = "";
|
|
pwInput.value = "";
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = "Upload";
|
|
clearNotice();
|
|
}
|
|
|
|
// ---- client-side history (localStorage) ----
|
|
|
|
function loadUploads() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
const parsed = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveUploads(list) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
|
} catch {
|
|
/* storage unavailable — ignore */
|
|
}
|
|
}
|
|
|
|
function addUpload(record) {
|
|
const list = loadUploads();
|
|
const idx = list.findIndex((item) => item.id === record.id);
|
|
if (idx !== -1) list[idx] = record;
|
|
else list.unshift(record);
|
|
saveUploads(list);
|
|
renderList();
|
|
}
|
|
|
|
function removeUpload(id) {
|
|
saveUploads(loadUploads().filter((item) => item.id !== id));
|
|
renderList();
|
|
}
|
|
|
|
function downloadUrl(item) {
|
|
return new URL(item.url, window.location.origin).href;
|
|
}
|
|
|
|
async function copyText(text) {
|
|
const fallback = () => {
|
|
const ta = document.createElement("textarea");
|
|
ta.value = text;
|
|
ta.style.position = "fixed";
|
|
ta.style.opacity = "0";
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
let ok = false;
|
|
try {
|
|
ok = document.execCommand("copy");
|
|
} catch {
|
|
ok = false;
|
|
}
|
|
ta.remove();
|
|
return ok;
|
|
};
|
|
if (navigator.clipboard) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch {
|
|
return fallback();
|
|
}
|
|
}
|
|
return fallback();
|
|
}
|
|
|
|
function setCopyFeedback(btn, label) {
|
|
const original = btn.textContent;
|
|
btn.textContent = label;
|
|
setTimeout(() => {
|
|
btn.textContent = original;
|
|
}, 1500);
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
try {
|
|
return new Date(iso).toLocaleString();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function renderList() {
|
|
const list = loadUploads();
|
|
fileList.innerHTML = "";
|
|
emptyState.classList.toggle("hidden", list.length > 0);
|
|
fileCount.textContent = list.length === 0 ? "" : `${list.length} file${list.length === 1 ? "" : "s"}`;
|
|
|
|
for (const item of list) {
|
|
const tr = document.createElement("tr");
|
|
|
|
const nameTd = document.createElement("td");
|
|
const nameWrap = document.createElement("div");
|
|
nameWrap.className = "file-name";
|
|
const name = document.createElement("span");
|
|
name.textContent = item.originalName;
|
|
name.title = item.originalName;
|
|
const lock = document.createElement("span");
|
|
lock.className = "lock";
|
|
lock.textContent = "Protected";
|
|
nameWrap.appendChild(name);
|
|
nameWrap.appendChild(lock);
|
|
nameTd.appendChild(nameWrap);
|
|
|
|
const sizeTd = document.createElement("td");
|
|
sizeTd.className = "file-size";
|
|
sizeTd.textContent = formatBytes(item.size);
|
|
|
|
const dateTd = document.createElement("td");
|
|
dateTd.className = "file-date";
|
|
dateTd.textContent = formatDate(item.createdAt);
|
|
|
|
const actionsTd = document.createElement("td");
|
|
const actions = document.createElement("div");
|
|
actions.className = "row-actions";
|
|
|
|
const dl = document.createElement("a");
|
|
dl.className = "btn btn-primary";
|
|
dl.href = downloadUrl(item);
|
|
dl.textContent = "Download";
|
|
dl.title = "Open download link";
|
|
|
|
const copy = document.createElement("button");
|
|
copy.type = "button";
|
|
copy.className = "btn btn-ghost";
|
|
copy.textContent = "Copy";
|
|
copy.addEventListener("click", async () => {
|
|
const ok = await copyText(downloadUrl(item));
|
|
setCopyFeedback(copy, ok ? "Copied" : "Failed");
|
|
});
|
|
|
|
const del = document.createElement("button");
|
|
del.type = "button";
|
|
del.className = "btn btn-danger";
|
|
del.textContent = "Delete";
|
|
del.addEventListener("click", () => deleteFile(item, () => removeUpload(item.id)));
|
|
|
|
actions.appendChild(dl);
|
|
actions.appendChild(copy);
|
|
actions.appendChild(del);
|
|
actionsTd.appendChild(actions);
|
|
|
|
tr.appendChild(nameTd);
|
|
tr.appendChild(sizeTd);
|
|
tr.appendChild(dateTd);
|
|
tr.appendChild(actionsTd);
|
|
fileList.appendChild(tr);
|
|
}
|
|
}
|
|
|
|
async function deleteFile(item, onSuccess) {
|
|
const ok = window.confirm(`Delete "${item.originalName}" permanently? This cannot be undone.`);
|
|
if (!ok) return;
|
|
try {
|
|
const res = await fetch(`/api/files/${item.id}`, {
|
|
method: "DELETE",
|
|
headers: { "X-Delete-Key": item.key },
|
|
});
|
|
if (res.ok) {
|
|
onSuccess();
|
|
showNotice(`"${item.originalName}" was deleted.`, "info");
|
|
} else {
|
|
let message = "Deletion failed.";
|
|
try {
|
|
const data = await res.json();
|
|
if (data && data.error) message = data.error;
|
|
} catch {
|
|
/* keep fallback message */
|
|
}
|
|
showNotice(message, "error");
|
|
}
|
|
} catch {
|
|
showNotice("Deletion failed: network error.", "error");
|
|
}
|
|
}
|
|
|
|
// ---- upload flow ----
|
|
|
|
function showResult(data) {
|
|
progress.classList.add("hidden");
|
|
result.classList.remove("hidden");
|
|
resultName.textContent = data.originalName;
|
|
resultUrl.value = downloadUrl(data);
|
|
addUpload({
|
|
id: data.id,
|
|
key: data.key,
|
|
url: data.url,
|
|
originalName: data.originalName,
|
|
size: data.size,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
function startUpload(file, password) {
|
|
if (uploading) return;
|
|
clearNotice();
|
|
progress.classList.remove("hidden");
|
|
progressName.textContent = file.name;
|
|
setProgress(0, file.size);
|
|
uploading = true;
|
|
uploadBtn.disabled = true;
|
|
uploadBtn.textContent = "Uploading\u2026";
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
const form = new FormData();
|
|
form.append("password", password);
|
|
form.append("file", file);
|
|
|
|
xhr.upload.addEventListener("progress", (e) => {
|
|
if (e.lengthComputable) setProgress(e.loaded, e.total);
|
|
});
|
|
|
|
xhr.addEventListener("load", () => {
|
|
uploading = false;
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = "Upload";
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
progress.classList.add("hidden");
|
|
try {
|
|
showResult(JSON.parse(xhr.responseText));
|
|
} catch {
|
|
showNotice("Invalid server response.", "error");
|
|
}
|
|
return;
|
|
}
|
|
progress.classList.add("hidden");
|
|
let message = `Upload failed (HTTP ${xhr.status})`;
|
|
try {
|
|
const data = JSON.parse(xhr.responseText);
|
|
if (data && data.error) message = data.error;
|
|
} catch {
|
|
/* keep fallback message */
|
|
}
|
|
showNotice(message, "error");
|
|
});
|
|
|
|
xhr.addEventListener("error", () => {
|
|
uploading = false;
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = "Upload";
|
|
progress.classList.add("hidden");
|
|
showNotice("Network error during upload.", "error");
|
|
});
|
|
|
|
xhr.addEventListener("abort", () => {
|
|
uploading = false;
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = "Upload";
|
|
progress.classList.add("hidden");
|
|
});
|
|
|
|
xhr.open("POST", "/api/upload");
|
|
xhr.send(form);
|
|
}
|
|
|
|
function selectFile(file) {
|
|
if (!file) return;
|
|
if (file.size === 0) {
|
|
showNotice("Empty files are not supported.", "error");
|
|
return;
|
|
}
|
|
currentFile = file;
|
|
chosenFile.textContent = file.name;
|
|
chosenFile.classList.remove("hidden");
|
|
clearNotice();
|
|
}
|
|
|
|
function submitUpload() {
|
|
if (!currentFile) {
|
|
showNotice("Choose a file first.", "error");
|
|
return;
|
|
}
|
|
const password = pwInput.value;
|
|
if (!password) {
|
|
showNotice("A password is required to protect this file.", "error");
|
|
pwInput.focus();
|
|
return;
|
|
}
|
|
startUpload(currentFile, password);
|
|
}
|
|
|
|
dropzone.addEventListener("click", () => {
|
|
if (!uploading) fileInput.click();
|
|
});
|
|
|
|
dropzone.addEventListener("keydown", (e) => {
|
|
if ((e.key === "Enter" || e.key === " ") && !uploading) {
|
|
e.preventDefault();
|
|
fileInput.click();
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener("change", () => {
|
|
const file = fileInput.files ? fileInput.files[0] : null;
|
|
selectFile(file);
|
|
});
|
|
|
|
pwInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") submitUpload();
|
|
});
|
|
|
|
uploadBtn.addEventListener("click", submitUpload);
|
|
|
|
newUploadBtn.addEventListener("click", () => {
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
pwInput.focus();
|
|
});
|
|
|
|
let dragDepth = 0;
|
|
document.addEventListener("dragenter", (e) => {
|
|
e.preventDefault();
|
|
if (!uploading) {
|
|
dragDepth += 1;
|
|
dropzone.classList.add("drag");
|
|
}
|
|
});
|
|
document.addEventListener("dragover", (e) => e.preventDefault());
|
|
document.addEventListener("dragleave", (e) => {
|
|
e.preventDefault();
|
|
dragDepth = Math.max(0, dragDepth - 1);
|
|
if (dragDepth === 0) dropzone.classList.remove("drag");
|
|
});
|
|
document.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
dragDepth = 0;
|
|
dropzone.classList.remove("drag");
|
|
if (uploading) return;
|
|
const file = e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files[0] : null;
|
|
selectFile(file);
|
|
});
|
|
|
|
copyBtn.addEventListener("click", async () => {
|
|
const ok = await copyText(resultUrl.value);
|
|
setCopyFeedback(copyBtn, ok ? "Copied" : "Failed");
|
|
});
|
|
|
|
clearListBtn.addEventListener("click", () => {
|
|
if (loadUploads().length === 0) return;
|
|
const ok = window.confirm(
|
|
"Clear this list? This only removes the entries from THIS browser. The files remain on the server.",
|
|
);
|
|
if (!ok) return;
|
|
saveUploads([]);
|
|
renderList();
|
|
showNotice("List cleared. Files on the server were not touched.", "info");
|
|
});
|
|
|
|
renderList(); |