Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
f07defc814
|
|||
0c6f6b65ef
|
|||
ccefdd23f3
|
|||
5a3551a0f5
|
|||
7773ab1b9c
|
|||
e7a58dcead
|
|||
f2f6f0f24c
|
|||
9d1d2b3299
|
|||
673d15b1b1
|
|||
b6b4b720df
|
|||
c8af56f1dc
|
|||
3f0b9fa625
|
|||
2b0597db0b
|
|||
9fc8b370d0
|
|||
da0dfeab46
|
@@ -12,29 +12,11 @@
|
|||||||
<script src="/script.js"></script>
|
<script src="/script.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="/" class="logo"><h1>Ablage</h1></a>
|
<a href="/" class="logo">
|
||||||
|
<h1>Ablage</h1>
|
||||||
<div id="dropzone" style="display: none">
|
</a>
|
||||||
Drag & drop files here or click to select
|
<h3 style="color: red; text-align: center; margin-top: 100px">
|
||||||
</div>
|
We will need JavaScript from here on, sorry...
|
||||||
<input
|
</h3>
|
||||||
type="file"
|
|
||||||
id="fileInput"
|
|
||||||
name="uploadfile"
|
|
||||||
multiple
|
|
||||||
style="display: none"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div id="overallProgressContainer" style="display: none">
|
|
||||||
<div id="currentFileName"></div>
|
|
||||||
<progress id="overallProgress" value="0" max="100"></progress>
|
|
||||||
<div id="overallStatus" class="status"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul id="file-list"></ul>
|
|
||||||
|
|
||||||
<div id="sinkholeModeInfo" class="sinkholeModeInfo" style="display: none">
|
|
||||||
- Sinkhole mode enabled, no files will get listed -
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,11 +1,16 @@
|
|||||||
(() => {
|
(() => {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
let AppConfig = null;
|
const DEFAULT_DROPZONE_TEXT = "Drag & drop files here or click to select";
|
||||||
let UI = {};
|
const state = {
|
||||||
|
config: null,
|
||||||
|
files: {},
|
||||||
|
ui: {},
|
||||||
|
errorTimeout: null,
|
||||||
|
};
|
||||||
|
|
||||||
async function appLoop() {
|
async function appLoop() {
|
||||||
if (AppConfig === null) {
|
if (state.config === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,50 +18,10 @@
|
|||||||
fetchFiles();
|
fetchFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
if (AppConfig.Modes.Readonly) {
|
|
||||||
UI.dropzone.style.display = "none";
|
|
||||||
} else {
|
|
||||||
UI.dropzone.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppConfig.Modes.Sinkhole) {
|
|
||||||
UI.fileList.style.display = "none";
|
|
||||||
UI.sinkholeModeInfo.style.display = "block";
|
|
||||||
} else {
|
|
||||||
UI.fileList.style.display = "block";
|
|
||||||
UI.sinkholeModeInfo.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initApp() {
|
async function initApp() {
|
||||||
UI.currentFileName = document.getElementById("currentFileName");
|
addUIElementsToBody();
|
||||||
UI.dropzone = document.getElementById("dropzone");
|
getUIElements();
|
||||||
UI.fileInput = document.getElementById("fileInput");
|
addEventListeners();
|
||||||
UI.fileList = document.getElementById("file-list");
|
|
||||||
UI.overallProgress = document.getElementById("overallProgress");
|
|
||||||
UI.overallStatus = document.getElementById("overallStatus");
|
|
||||||
UI.overallProgressContainer = document.getElementById(
|
|
||||||
"overallProgressContainer"
|
|
||||||
);
|
|
||||||
UI.sinkholeModeInfo = document.getElementById("sinkholeModeInfo");
|
|
||||||
|
|
||||||
UI.dropzone.addEventListener("click", () => UI.fileInput.click());
|
|
||||||
UI.fileInput.addEventListener("change", () => {
|
|
||||||
if (UI.fileInput.files.length > 0) uploadFiles(UI.fileInput.files);
|
|
||||||
});
|
|
||||||
UI.dropzone.addEventListener("dragover", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
UI.dropzone.style.borderColor = "#0fff50";
|
|
||||||
});
|
|
||||||
UI.dropzone.addEventListener("dragleave", () => {
|
|
||||||
UI.dropzone.style.borderColor = "#888";
|
|
||||||
});
|
|
||||||
UI.dropzone.addEventListener("drop", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
UI.dropzone.style.borderColor = "#888";
|
|
||||||
if (e.dataTransfer.files.length > 0) uploadFiles(e.dataTransfer.files);
|
|
||||||
});
|
|
||||||
|
|
||||||
await loadAppConfig();
|
await loadAppConfig();
|
||||||
appLoop();
|
appLoop();
|
||||||
@@ -71,76 +36,190 @@
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error("HTTP error:", res.status);
|
console.error("HTTP error:", res.status);
|
||||||
}
|
}
|
||||||
AppConfig = await res.json();
|
state.config = await res.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load config:", err);
|
console.error("Failed to load config:", err);
|
||||||
AppConfig = null;
|
state.config = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFiles() {
|
async function fetchFiles() {
|
||||||
if (AppConfig.Modes.Sinkhole) {
|
if (state.config.Modes.Sinkhole) {
|
||||||
UI.fileList.innerHTML = "";
|
clearFileList();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(AppConfig.Endpoints.Files, { cache: "no-store" });
|
const files = await fetchFileList();
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
state.files = {};
|
||||||
const files = await res.json();
|
clearFileList();
|
||||||
|
renderFileList(files);
|
||||||
if (!UI.fileList) return;
|
|
||||||
UI.fileList.innerHTML = "";
|
|
||||||
files.forEach((file) => {
|
|
||||||
const size = humanReadableSize(file.Size);
|
|
||||||
|
|
||||||
const li = document.createElement("li");
|
|
||||||
|
|
||||||
const downloadLink = document.createElement("a");
|
|
||||||
downloadLink.className = "download-link";
|
|
||||||
downloadLink.href = AppConfig.Endpoints.FilesGet.replace(
|
|
||||||
":filename",
|
|
||||||
encodeURIComponent(file.Name)
|
|
||||||
);
|
|
||||||
downloadLink.textContent = `${file.Name} (${size})`;
|
|
||||||
|
|
||||||
li.appendChild(downloadLink);
|
|
||||||
|
|
||||||
if (!AppConfig.Modes.Readonly) {
|
|
||||||
const deleteLink = document.createElement("a");
|
|
||||||
deleteLink.className = "delete-link";
|
|
||||||
deleteLink.href = "#";
|
|
||||||
deleteLink.textContent = " [Delete]";
|
|
||||||
deleteLink.title = "Delete file";
|
|
||||||
deleteLink.addEventListener("click", async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!confirm(`Do you really want to delete "${file.Name}"?`))
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
const r = await fetch(
|
|
||||||
AppConfig.Endpoints.FilesDelete.replace(
|
|
||||||
":filename",
|
|
||||||
encodeURIComponent(file.Name)
|
|
||||||
),
|
|
||||||
{ method: "GET" }
|
|
||||||
);
|
|
||||||
if (!r.ok) throw new Error("Delete failed " + r.status);
|
|
||||||
fetchFiles();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
li.appendChild(deleteLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
UI.fileList.appendChild(li);
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("fetchFiles failed:", err);
|
console.error("fetchFiles failed:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchFileList() {
|
||||||
|
const res = await fetch(state.config.Endpoints.Files, {
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteClick(event, file) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!confirm(`Do you really want to delete "${file.Name}"?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
state.config.Endpoints.FilesDelete.replace(
|
||||||
|
":filename",
|
||||||
|
encodeURIComponent(file.Name)
|
||||||
|
),
|
||||||
|
{ method: "GET" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showSuccess("File deleted: " + file.Name);
|
||||||
|
} else {
|
||||||
|
showError("Delete failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFiles();
|
||||||
|
} catch (err) {
|
||||||
|
showError("Delete failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEventListeners() {
|
||||||
|
state.ui.dropzone.addEventListener("click", () =>
|
||||||
|
state.ui.fileInput.click()
|
||||||
|
);
|
||||||
|
state.ui.fileInput.addEventListener("change", () => {
|
||||||
|
if (state.ui.fileInput.files.length > 0)
|
||||||
|
uploadFiles(state.ui.fileInput.files);
|
||||||
|
});
|
||||||
|
state.ui.dropzone.addEventListener("dragover", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.ui.dropzone.style.borderColor = "#0fff50";
|
||||||
|
});
|
||||||
|
state.ui.dropzone.addEventListener("dragleave", () => {
|
||||||
|
state.ui.dropzone.style.borderColor = "#888";
|
||||||
|
});
|
||||||
|
state.ui.dropzone.addEventListener("drop", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.ui.dropzone.style.borderColor = "#888";
|
||||||
|
if (e.dataTransfer.files.length > 0) uploadFiles(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUIElementsToBody() {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
|
||||||
|
const aLogo = document.createElement("a");
|
||||||
|
aLogo.href = "/";
|
||||||
|
aLogo.className = "logo";
|
||||||
|
const h1Logo = document.createElement("h1");
|
||||||
|
h1Logo.textContent = "Ablage";
|
||||||
|
aLogo.appendChild(h1Logo);
|
||||||
|
document.body.appendChild(aLogo);
|
||||||
|
|
||||||
|
const divDropzone = document.createElement("div");
|
||||||
|
divDropzone.className = "dropzone";
|
||||||
|
divDropzone.id = "dropzone";
|
||||||
|
divDropzone.innerHTML = DEFAULT_DROPZONE_TEXT;
|
||||||
|
divDropzone.style.display = "none";
|
||||||
|
document.body.appendChild(divDropzone);
|
||||||
|
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.id = "fileInput";
|
||||||
|
fileInput.name = "uploadfile";
|
||||||
|
fileInput.multiple = true;
|
||||||
|
fileInput.style.display = "none";
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
|
||||||
|
const divOverallProgressContainer = document.createElement("div");
|
||||||
|
divOverallProgressContainer.id = "overallProgressContainer";
|
||||||
|
divOverallProgressContainer.style.display = "none";
|
||||||
|
const divCurrentFileName = document.createElement("div");
|
||||||
|
divCurrentFileName.id = "currentFileName";
|
||||||
|
const progressOverall = document.createElement("progress");
|
||||||
|
progressOverall.id = "overallProgress";
|
||||||
|
progressOverall.value = 0;
|
||||||
|
progressOverall.max = 100;
|
||||||
|
const divOverallStatus = document.createElement("div");
|
||||||
|
divOverallStatus.id = "overallStatus";
|
||||||
|
divOverallStatus.className = "status";
|
||||||
|
divOverallProgressContainer.appendChild(divCurrentFileName);
|
||||||
|
divOverallProgressContainer.appendChild(progressOverall);
|
||||||
|
divOverallProgressContainer.appendChild(divOverallStatus);
|
||||||
|
document.body.appendChild(divOverallProgressContainer);
|
||||||
|
|
||||||
|
const ulFileList = document.createElement("ul");
|
||||||
|
ulFileList.id = "file-list";
|
||||||
|
document.body.appendChild(ulFileList);
|
||||||
|
|
||||||
|
const divSinkholeModeInfo = document.createElement("div");
|
||||||
|
divSinkholeModeInfo.id = "sinkholeModeInfo";
|
||||||
|
divSinkholeModeInfo.className = "sinkholeModeInfo";
|
||||||
|
divSinkholeModeInfo.style.display = "none";
|
||||||
|
divSinkholeModeInfo.textContent =
|
||||||
|
"- Sinkhole mode enabled, no files will get listed -";
|
||||||
|
document.body.appendChild(divSinkholeModeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFileList() {
|
||||||
|
if (state.ui.fileList) state.ui.fileList.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDownloadLink(file) {
|
||||||
|
const size = humanReadableSize(file.Size);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.className = "download-link";
|
||||||
|
link.href = state.config.Endpoints.FilesGet.replace(
|
||||||
|
":filename",
|
||||||
|
encodeURIComponent(file.Name)
|
||||||
|
);
|
||||||
|
link.textContent = `${file.Name} (${size})`;
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeleteLink(file) {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.className = "delete-link";
|
||||||
|
link.href = "#";
|
||||||
|
link.textContent = " [Delete]";
|
||||||
|
link.title = "Delete file";
|
||||||
|
link.addEventListener("click", (e) => handleDeleteClick(e, file));
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishUpload(success) {
|
||||||
|
state.ui.overallProgressContainer.style.display = "none";
|
||||||
|
state.ui.overallProgress.value = 0;
|
||||||
|
state.ui.overallStatus.textContent = "";
|
||||||
|
state.ui.currentFileName.textContent = "";
|
||||||
|
fetchFiles();
|
||||||
|
if (success) {
|
||||||
|
showSuccess("Upload successful");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUIElements() {
|
||||||
|
state.ui.currentFileName = document.getElementById("currentFileName");
|
||||||
|
state.ui.dropzone = document.getElementById("dropzone");
|
||||||
|
state.ui.fileInput = document.getElementById("fileInput");
|
||||||
|
state.ui.fileList = document.getElementById("file-list");
|
||||||
|
state.ui.overallProgress = document.getElementById("overallProgress");
|
||||||
|
state.ui.overallStatus = document.getElementById("overallStatus");
|
||||||
|
state.ui.overallProgressContainer = document.getElementById(
|
||||||
|
"overallProgressContainer"
|
||||||
|
);
|
||||||
|
state.ui.sinkholeModeInfo = document.getElementById("sinkholeModeInfo");
|
||||||
|
}
|
||||||
|
|
||||||
function humanReadableSize(bytes) {
|
function humanReadableSize(bytes) {
|
||||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -159,85 +238,215 @@
|
|||||||
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s";
|
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initUIProgress() {
|
||||||
|
state.ui.overallProgressContainer.style.display = "block";
|
||||||
|
state.ui.overallProgress.value = 0;
|
||||||
|
state.ui.overallStatus.textContent = "";
|
||||||
|
state.ui.currentFileName.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFileList(files) {
|
||||||
|
if (!state.ui.fileList) return;
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
state.files[file.Name] = true;
|
||||||
|
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.appendChild(createDownloadLink(file));
|
||||||
|
|
||||||
|
if (!state.config.Modes.Readonly) {
|
||||||
|
li.appendChild(createDeleteLink(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ui.fileList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFilename(dirtyFilename) {
|
||||||
|
if (!dirtyFilename || dirtyFilename.trim() === "") {
|
||||||
|
return "upload.bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
const filenameWithoutPath = dirtyFilename.split(/[\\/]/).pop();
|
||||||
|
|
||||||
|
const lastDot = filenameWithoutPath.lastIndexOf(".");
|
||||||
|
const extension = lastDot !== -1 ? filenameWithoutPath.slice(lastDot) : "";
|
||||||
|
let nameOnly =
|
||||||
|
lastDot !== -1
|
||||||
|
? filenameWithoutPath.slice(0, lastDot)
|
||||||
|
: filenameWithoutPath;
|
||||||
|
|
||||||
|
const charMap = {
|
||||||
|
Ä: "Ae",
|
||||||
|
ä: "ae",
|
||||||
|
Ö: "Oe",
|
||||||
|
ö: "oe",
|
||||||
|
Ü: "Ue",
|
||||||
|
ü: "ue",
|
||||||
|
ß: "ss",
|
||||||
|
};
|
||||||
|
|
||||||
|
let cleanedFilename = nameOnly.replace(/./g, (char) => {
|
||||||
|
if (charMap[char]) {
|
||||||
|
return charMap[char];
|
||||||
|
}
|
||||||
|
if (char === " ") {
|
||||||
|
return "_";
|
||||||
|
}
|
||||||
|
return char;
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanedFilename = cleanedFilename.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
||||||
|
|
||||||
|
while (cleanedFilename.includes("__")) {
|
||||||
|
cleanedFilename = cleanedFilename.replace(/__+/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedFilename = cleanedFilename.replace(/^_+|_+$/g, "");
|
||||||
|
|
||||||
|
const MAX_LEN = 128;
|
||||||
|
if (cleanedFilename.length > MAX_LEN) {
|
||||||
|
cleanedFilename = cleanedFilename.slice(0, MAX_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedFilename + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
showMessage(msg, "error", 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(msg, type, duration = 2000) {
|
||||||
|
state.ui.dropzone.innerHTML = msg;
|
||||||
|
state.ui.dropzone.classList.add(type);
|
||||||
|
|
||||||
|
if (state.errorTimeout) clearTimeout(state.errorTimeout);
|
||||||
|
|
||||||
|
state.errorTimeout = setTimeout(() => {
|
||||||
|
state.ui.dropzone.innerHTML = DEFAULT_DROPZONE_TEXT;
|
||||||
|
state.ui.dropzone.classList.remove(type);
|
||||||
|
state.errorTimeout = null;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess(msg) {
|
||||||
|
showMessage(msg, "success", 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (state.config.Modes.Readonly) {
|
||||||
|
state.ui.dropzone.style.display = "none";
|
||||||
|
} else {
|
||||||
|
state.ui.dropzone.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.config.Modes.Sinkhole) {
|
||||||
|
state.ui.fileList.style.display = "none";
|
||||||
|
state.ui.sinkholeModeInfo.style.display = "block";
|
||||||
|
} else {
|
||||||
|
state.ui.fileList.style.display = "block";
|
||||||
|
state.ui.sinkholeModeInfo.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function uploadFiles(fileListLike) {
|
function uploadFiles(fileListLike) {
|
||||||
const files = Array.from(fileListLike);
|
const files = Array.from(fileListLike);
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
|
||||||
UI.overallProgressContainer.style.display = "block";
|
if (!validateFiles(files)) return;
|
||||||
UI.overallProgress.value = 0;
|
|
||||||
UI.overallStatus.textContent = "";
|
initUIProgress();
|
||||||
UI.currentFileName.textContent = "";
|
|
||||||
|
|
||||||
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
||||||
let uploadedBytes = 0;
|
let uploadedBytes = 0;
|
||||||
const t0 = Date.now();
|
let currentIndex = 0;
|
||||||
let idx = 0;
|
const startTime = Date.now();
|
||||||
|
let allSuccessful = true;
|
||||||
|
|
||||||
const uploadNext = () => {
|
function uploadNext() {
|
||||||
if (idx >= files.length) {
|
if (currentIndex >= files.length) {
|
||||||
UI.overallProgressContainer.style.display = "none";
|
finishUpload(allSuccessful);
|
||||||
UI.overallProgress.value = 0;
|
|
||||||
UI.overallStatus.textContent = "";
|
|
||||||
UI.currentFileName.textContent = "";
|
|
||||||
fetchFiles();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = files[idx];
|
const file = files[currentIndex];
|
||||||
UI.currentFileName.textContent = file.name;
|
state.ui.currentFileName.textContent = file.name;
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("uploadfile", file);
|
form.append("uploadfile", file);
|
||||||
|
|
||||||
xhr.upload.addEventListener("progress", (e) => {
|
xhr.upload.addEventListener("progress", (e) => {
|
||||||
if (!e.lengthComputable) return;
|
if (e.lengthComputable) {
|
||||||
|
updateProgressUI(uploadedBytes + e.loaded, totalSize, startTime);
|
||||||
const totalUploaded = uploadedBytes + e.loaded;
|
}
|
||||||
const percent = (totalUploaded / totalSize) * 100;
|
|
||||||
UI.overallProgress.value = percent;
|
|
||||||
|
|
||||||
const elapsed = (Date.now() - t0) / 1000;
|
|
||||||
const speed = totalUploaded / elapsed;
|
|
||||||
const speedStr = humanReadableSpeed(speed);
|
|
||||||
|
|
||||||
const remainingBytes = totalSize - totalUploaded;
|
|
||||||
const etaSec = speed > 0 ? remainingBytes / speed : Infinity;
|
|
||||||
const min = Math.floor(etaSec / 60);
|
|
||||||
const sec = Math.floor(etaSec % 60);
|
|
||||||
|
|
||||||
UI.overallStatus.textContent =
|
|
||||||
`${percent.toFixed(1)}% (${(totalSize / 1024 / 1024).toFixed(
|
|
||||||
1
|
|
||||||
)} MB total) — ` +
|
|
||||||
`Speed: ${speedStr}, Est. time left: ${
|
|
||||||
isFinite(etaSec) ? `${min}m ${sec}s` : "calculating…"
|
|
||||||
}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener("load", () => {
|
xhr.addEventListener("load", () => {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
uploadedBytes += file.size;
|
uploadedBytes += file.size;
|
||||||
|
} else if (xhr.status === 409) {
|
||||||
|
showError("File already exists: " + file.name);
|
||||||
|
allSuccessful = false;
|
||||||
} else {
|
} else {
|
||||||
console.error("Upload failed with status", xhr.status);
|
showError("Upload failed: " + file.name);
|
||||||
|
allSuccessful = false;
|
||||||
}
|
}
|
||||||
idx++;
|
currentIndex++;
|
||||||
uploadNext();
|
uploadNext();
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener("error", () => {
|
xhr.addEventListener("error", () => {
|
||||||
console.error("Network/server error during upload.");
|
showError("Network or server error during upload.");
|
||||||
idx++;
|
allSuccessful = false;
|
||||||
|
currentIndex++;
|
||||||
uploadNext();
|
uploadNext();
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.open("POST", AppConfig.Endpoints.Upload);
|
xhr.open("POST", state.config.Endpoints.Upload);
|
||||||
xhr.send(form);
|
xhr.send(form);
|
||||||
};
|
}
|
||||||
|
|
||||||
fetchFiles();
|
fetchFiles();
|
||||||
uploadNext();
|
uploadNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateProgressUI(totalUploaded, totalSize, startTime) {
|
||||||
|
const percent = (totalUploaded / totalSize) * 100;
|
||||||
|
state.ui.overallProgress.value = percent;
|
||||||
|
|
||||||
|
const elapsed = (Date.now() - startTime) / 1000;
|
||||||
|
const speed = totalUploaded / elapsed;
|
||||||
|
const speedStr = humanReadableSpeed(speed);
|
||||||
|
|
||||||
|
const remainingBytes = totalSize - totalUploaded;
|
||||||
|
const etaSec = speed > 0 ? remainingBytes / speed : Infinity;
|
||||||
|
const min = Math.floor(etaSec / 60);
|
||||||
|
const sec = Math.floor(etaSec % 60);
|
||||||
|
|
||||||
|
state.ui.overallStatus.textContent =
|
||||||
|
`${percent.toFixed(1)}% (${(totalSize / 1024 / 1024).toFixed(
|
||||||
|
1
|
||||||
|
)} MB total) — ` +
|
||||||
|
`Speed: ${speedStr}, Est. time left: ${
|
||||||
|
isFinite(etaSec) ? `${min}m ${sec}s` : "calculating…"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFiles(files) {
|
||||||
|
for (const f of files) {
|
||||||
|
const safeName = sanitizeFilename(f.name);
|
||||||
|
if (safeName === ".upload") {
|
||||||
|
showError("Invalid filename: .upload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (safeName in state.files) {
|
||||||
|
showError("File already exists: " + f.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", initApp);
|
document.addEventListener("DOMContentLoaded", initApp);
|
||||||
})();
|
})();
|
||||||
|
@@ -9,7 +9,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Dropzone */
|
/* Dropzone */
|
||||||
#dropzone {
|
.dropzone {
|
||||||
border: 2px dashed #888;
|
border: 2px dashed #888;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: #fefefe;
|
color: #fefefe;
|
||||||
@@ -20,10 +20,22 @@ body {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dropzone:hover {
|
.dropzone:hover {
|
||||||
color: #0fff50;
|
color: #0fff50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropzone.error {
|
||||||
|
border: 2px solid #ff4d4d;
|
||||||
|
color: #ff4d4d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone.success {
|
||||||
|
border: 2px solid #0fff50;
|
||||||
|
color: #0fff50;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
/* File list */
|
/* File list */
|
||||||
#file-list {
|
#file-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@@ -253,9 +253,9 @@ func httpPostUpload(w http.ResponseWriter, r *http.Request, ps httprouter.Params
|
|||||||
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer uploadFile.Close()
|
|
||||||
|
|
||||||
bytesWritten, err := io.Copy(uploadFile, part)
|
bytesWritten, err := io.Copy(uploadFile, part)
|
||||||
|
uploadFile.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = os.Remove(pathToFileInUploadFolder)
|
_ = os.Remove(pathToFileInUploadFolder)
|
||||||
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
||||||
|
2
build.sh
Executable file
2
build.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
GOOS=windows GOARCH=amd64 go build -o build/ablage-windows-amd64.exe .
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o build/ablage-linux-amd64 .
|
66
config/banner.go
Normal file
66
config/banner.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintStartupBanner() {
|
||||||
|
fmt.Println(getBanner() + "\n")
|
||||||
|
fmt.Printf("Basic Auth mode: %v\n", GetBasicAuthMode())
|
||||||
|
fmt.Printf("HTTP mode : %v\n", GetHttpMode())
|
||||||
|
fmt.Printf("Readonly mode : %v\n", GetReadonlyMode())
|
||||||
|
fmt.Printf("Sinkhole mode : %v\n", GetSinkholeMode())
|
||||||
|
fmt.Printf("Path : %s\n", GetPathDataFolder())
|
||||||
|
|
||||||
|
if GetBasicAuthMode() {
|
||||||
|
fmt.Printf("Username : %s\n", GetBasicAuthUsername())
|
||||||
|
fmt.Printf("Password : %s\n", GetBasicAuthPassword())
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetHttpMode() {
|
||||||
|
fmt.Printf("Listening on : http://0.0.0.0:%d\n", GetPortToListenOn())
|
||||||
|
} else {
|
||||||
|
if pathTLSCertFile == "" || pathTLSKeyFile == "" {
|
||||||
|
fmt.Printf("TLS cert : self-signed\n")
|
||||||
|
fmt.Printf("TLS key : self-signed\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("TLS cert : %s\n", pathTLSCertFile)
|
||||||
|
fmt.Printf("TLS key : %s\n", pathTLSKeyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Listening on : https://0.0.0.0:%d\n", GetPortToListenOn())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func centerTextWithWhitespaces(text string, maxWidth int) string {
|
||||||
|
textLength := utf8.RuneCountInString(text)
|
||||||
|
if textLength >= maxWidth {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPadding := maxWidth - textLength
|
||||||
|
leftPadding := int(math.Ceil((float64(totalPadding) / 2.0)))
|
||||||
|
rightPadding := totalPadding - leftPadding
|
||||||
|
|
||||||
|
return strings.Repeat(" ", leftPadding) + text + strings.Repeat(" ", rightPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBanner() string {
|
||||||
|
return strings.Join(
|
||||||
|
[]string{
|
||||||
|
"┌──────────────────────────────────────┐",
|
||||||
|
"│ Ablage │",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"│%s│",
|
||||||
|
centerTextWithWhitespaces("v"+VersionString, 38),
|
||||||
|
),
|
||||||
|
"└──────────────────────────────────────┘",
|
||||||
|
},
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
}
|
@@ -7,10 +7,10 @@ import (
|
|||||||
|
|
||||||
const DefaultBasicAuthUsername string = "ablage"
|
const DefaultBasicAuthUsername string = "ablage"
|
||||||
const DefaultNameDataFolder string = "data"
|
const DefaultNameDataFolder string = "data"
|
||||||
const DefaultNameUploadFolder string = "upload"
|
const DefaultNameUploadFolder string = ".upload"
|
||||||
const DefaultPortToListenOn int = 13692
|
const DefaultPortToListenOn int = 13692
|
||||||
const LengthOfRandomBasicAuthPassword int = 16
|
const LengthOfRandomBasicAuthPassword int = 16
|
||||||
const VersionString string = "1.0"
|
const VersionString string = "1.1"
|
||||||
|
|
||||||
var randomBasicAuthPassword string = generateRandomPassword()
|
var randomBasicAuthPassword string = generateRandomPassword()
|
||||||
|
|
||||||
@@ -33,36 +33,3 @@ func Init() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintStartupBanner() {
|
|
||||||
fmt.Println("****************************************")
|
|
||||||
fmt.Println("* Ablage *")
|
|
||||||
fmt.Println("****************************************")
|
|
||||||
fmt.Printf("Version : %s\n", VersionString)
|
|
||||||
fmt.Printf("Basic Auth mode: %v\n", GetBasicAuthMode())
|
|
||||||
fmt.Printf("HTTP mode : %v\n", GetHttpMode())
|
|
||||||
fmt.Printf("Readonly mode : %v\n", GetReadonlyMode())
|
|
||||||
fmt.Printf("Sinkhole mode : %v\n", GetSinkholeMode())
|
|
||||||
fmt.Printf("Path : %s\n", GetPathDataFolder())
|
|
||||||
|
|
||||||
if GetBasicAuthMode() {
|
|
||||||
fmt.Printf("Username : %s\n", GetBasicAuthUsername())
|
|
||||||
fmt.Printf("Password : %s\n", GetBasicAuthPassword())
|
|
||||||
}
|
|
||||||
|
|
||||||
if GetHttpMode() {
|
|
||||||
fmt.Printf("Listening on : http://0.0.0.0:%d\n", GetPortToListenOn())
|
|
||||||
} else {
|
|
||||||
if pathTLSCertFile == "" || pathTLSKeyFile == "" {
|
|
||||||
fmt.Printf("TLS cert : self-signed\n")
|
|
||||||
fmt.Printf("TLS key : self-signed\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("TLS cert : %s\n", pathTLSCertFile)
|
|
||||||
fmt.Printf("TLS key : %s\n", pathTLSKeyFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Listening on : https://0.0.0.0:%d\n", GetPortToListenOn())
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("")
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user