first commit

This commit is contained in:
2025-08-26 21:54:30 +02:00
commit 4478d70d15
21 changed files with 1617 additions and 0 deletions

65
app/assets/favicon.svg Normal file
View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<polygon style="fill:#D0FBFD;" points="449.362,175.932 449.362,438.468 394.894,460.255 416.681,175.932 "/>
<path style="fill:#F0FEFF;" d="M314.281,175.932v51.2H62.638V503.83H384l32.681-54.468v-273.43H314.281z"/>
<g>
<polygon style="fill:#50D1DD;" points="384,438.468 384,503.83 449.362,438.468 "/>
<rect x="62.638" y="175.932" style="fill:#50D1DD;" width="51.2" height="51.2"/>
</g>
<rect x="146.519" y="175.932" style="fill:#FFEAB5;" width="51.2" height="51.2"/>
<rect x="230.4" y="175.932" style="fill:#D0FBFD;" width="51.2" height="51.2"/>
<rect x="62.638" y="92.051" style="fill:#FF9269;" width="51.2" height="51.2"/>
<rect x="146.519" y="92.051" style="fill:#D0FBFD;" width="51.2" height="51.2"/>
<rect x="230.4" y="92.051" style="fill:#FFB082;" width="51.2" height="51.2"/>
<rect x="314.281" y="92.051" style="fill:#FFEAB5;" width="51.2" height="51.2"/>
<rect x="398.162" y="8.17" style="fill:#D0FBFD;" width="51.2" height="51.2"/>
<rect x="146.519" y="259.813" style="fill:#FFDB8A;" width="51.2" height="51.2"/>
<rect x="230.4" y="259.813" style="fill:#FFEAB5;" width="51.2" height="51.2"/>
<rect x="314.281" y="343.694" style="fill:#FF9269;" width="51.2" height="51.2"/>
<rect x="398.162" y="92.051" style="fill:#FFB082;" width="51.2" height="51.2"/>
<rect x="146.519" y="8.17" style="fill:#FFEAB5;" width="51.2" height="51.2"/>
<rect x="230.4" y="8.17" style="fill:#FFDB8A;" width="51.2" height="51.2"/>
<path d="M62.638,151.421h51.2c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2
c-4.512,0-8.17,3.658-8.17,8.17v51.2C54.468,147.763,58.126,151.421,62.638,151.421z M70.809,100.221h34.86v34.86h-34.86V100.221z"
/>
<path d="M146.519,151.421h51.2c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2
c-4.512,0-8.17,3.658-8.17,8.17v51.2C138.349,147.763,142.007,151.421,146.519,151.421z M154.689,100.221h34.86v34.86h-34.86
V100.221z"/>
<path d="M197.719,319.183c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17
v51.2c0,4.512,3.658,8.17,8.17,8.17H197.719z M154.689,267.983h34.86v34.86h-34.86V267.983z"/>
<path d="M281.6,319.183c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17v51.2
c0,4.512,3.658,8.17,8.17,8.17H281.6z M238.57,267.983h34.86v34.86h-34.86V267.983z"/>
<path d="M365.481,403.064c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17
v51.2c0,4.512,3.658,8.17,8.17,8.17H365.481z M322.451,351.864h34.86v34.86h-34.86V351.864z"/>
<path d="M230.4,151.421h51.2c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17
v51.2C222.23,147.763,225.888,151.421,230.4,151.421z M238.57,100.221h34.86v34.86h-34.86V100.221z"/>
<path d="M314.281,151.421h51.2c4.512,0,8.17-3.658,8.17-8.17v-51.2c0-4.512-3.658-8.17-8.17-8.17h-51.2
c-4.512,0-8.17,3.658-8.17,8.17v51.2C306.111,147.763,309.769,151.421,314.281,151.421z M322.451,100.221h34.86v34.86h-34.86
V100.221z"/>
<path d="M449.362,83.881h-51.2c-4.512,0-8.17,3.658-8.17,8.17v51.2c0,4.512,3.658,8.17,8.17,8.17h51.2c4.512,0,8.17-3.658,8.17-8.17
v-51.2C457.532,87.539,453.874,83.881,449.362,83.881z M441.191,135.081h-34.86v-34.86h34.86V135.081z"/>
<path d="M146.519,67.54h51.2c4.512,0,8.17-3.658,8.17-8.17V8.17c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17
v51.2C138.349,63.882,142.007,67.54,146.519,67.54z M154.689,16.34h34.86V51.2h-34.86V16.34z"/>
<path d="M230.4,67.54h51.2c4.512,0,8.17-3.658,8.17-8.17V8.17c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17
v51.2C222.23,63.882,225.888,67.54,230.4,67.54z M238.57,16.34h34.86V51.2h-34.86V16.34z"/>
<path d="M449.362,0h-51.2c-4.512,0-8.17,3.658-8.17,8.17v51.2c0,4.512,3.658,8.17,8.17,8.17h51.2c4.512,0,8.17-3.658,8.17-8.17V8.17
C457.532,3.658,453.874,0,449.362,0z M441.191,51.2h-34.86V16.34h34.86V51.2z"/>
<path d="M449.362,167.762H314.281c-4.512,0-8.17,3.658-8.17,8.17v43.03h-16.34v-43.03c0-4.512-3.658-8.17-8.17-8.17h-51.2
c-4.512,0-8.17,3.658-8.17,8.17v43.03h-16.34v-43.03c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17v43.03
h-16.34v-43.03c0-4.512-3.658-8.17-8.17-8.17h-51.2c-4.512,0-8.17,3.658-8.17,8.17v135.081c0,4.512,3.658,8.17,8.17,8.17
s8.17-3.658,8.17-8.17v-75.711h243.472c4.512,0,8.17-3.658,8.17-8.17v-43.03h118.74v246.196H384c-4.512,0-8.17,3.658-8.17,8.17
v57.191H70.809V343.694c0-4.512-3.658-8.17-8.17-8.17s-8.17,3.658-8.17,8.17V503.83c0,4.512,3.658,8.17,8.17,8.17H384
c0.273,0,0.546-0.014,0.816-0.041c0.19-0.019,0.375-0.052,0.561-0.084c0.077-0.013,0.156-0.02,0.233-0.035
c0.221-0.045,0.436-0.102,0.65-0.163c0.041-0.012,0.083-0.02,0.124-0.032c0.217-0.066,0.427-0.145,0.636-0.228
c0.038-0.015,0.077-0.026,0.115-0.042c0.195-0.082,0.385-0.174,0.572-0.27c0.05-0.025,0.102-0.047,0.153-0.073
c0.169-0.09,0.33-0.192,0.491-0.294c0.064-0.04,0.132-0.076,0.196-0.119c0.145-0.098,0.282-0.205,0.42-0.312
c0.073-0.057,0.15-0.107,0.221-0.166c0.163-0.134,0.318-0.279,0.471-0.426c0.039-0.037,0.082-0.07,0.119-0.108l65.359-65.361
c0.188-0.188,0.368-0.387,0.537-0.594c0.044-0.053,0.081-0.11,0.123-0.164c0.122-0.156,0.243-0.313,0.353-0.477
c0.035-0.051,0.063-0.106,0.096-0.158c0.11-0.173,0.219-0.348,0.317-0.529c0.022-0.039,0.038-0.082,0.059-0.122
c0.101-0.197,0.199-0.397,0.284-0.601c0.012-0.029,0.021-0.059,0.033-0.088c0.087-0.217,0.169-0.437,0.237-0.662
c0.01-0.033,0.016-0.066,0.025-0.099c0.064-0.221,0.123-0.446,0.169-0.674c0.014-0.069,0.02-0.139,0.032-0.208
c0.034-0.194,0.068-0.387,0.087-0.585c0.027-0.27,0.041-0.542,0.041-0.816V175.932C457.532,171.42,453.874,167.762,449.362,167.762z
M238.57,184.102h34.86v34.86h-34.86V184.102z M154.689,184.102h34.86v34.86h-34.86V184.102z M70.809,184.102h34.86v34.86h-34.86
V184.102z M429.638,446.638l-37.468,37.468v-37.468H429.638z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

40
app/assets/index.html Normal file
View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>Ablage</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" sizes="any" />
<link rel="stylesheet" href="/style.css" />
<script src="/script.js"></script>
</head>
<body>
<a href="/" class="logo"><h1>Ablage</h1></a>
<div id="dropzone" style="display: none">
Drag & drop files here or click to select
</div>
<input
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>
</html>

243
app/assets/script.js Normal file
View File

@@ -0,0 +1,243 @@
(() => {
"use strict";
let AppConfig = null;
let UI = {};
async function appLoop() {
if (AppConfig === null) {
return;
}
updateUI();
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() {
UI.currentFileName = document.getElementById("currentFileName");
UI.dropzone = document.getElementById("dropzone");
UI.fileInput = document.getElementById("fileInput");
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();
appLoop();
setInterval(appLoop, 5 * 1000);
setInterval(loadAppConfig, 60 * 1000);
}
async function loadAppConfig() {
try {
const res = await fetch("/config/", { cache: "no-store" });
if (!res.ok) {
console.error("HTTP error:", res.status);
}
AppConfig = await res.json();
} catch (err) {
console.error("Failed to load config:", err);
AppConfig = null;
}
}
async function fetchFiles() {
if (AppConfig.Modes.Sinkhole) {
UI.fileList.innerHTML = "";
return;
}
try {
const res = await fetch(AppConfig.Endpoints.Files, { cache: "no-store" });
if (!res.ok) throw new Error("HTTP " + res.status);
const files = await res.json();
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) {
console.error("fetchFiles failed:", err);
}
}
function humanReadableSize(bytes) {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return `${bytes.toFixed(1)} ${units[i]}`;
}
function humanReadableSpeed(bytesPerSec) {
if (!isFinite(bytesPerSec) || bytesPerSec <= 0) return "—";
if (bytesPerSec < 1024) return bytesPerSec.toFixed(0) + " B/s";
if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s";
}
function uploadFiles(fileListLike) {
const files = Array.from(fileListLike);
if (files.length === 0) return;
UI.overallProgressContainer.style.display = "block";
UI.overallProgress.value = 0;
UI.overallStatus.textContent = "";
UI.currentFileName.textContent = "";
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
let uploadedBytes = 0;
const t0 = Date.now();
let idx = 0;
const uploadNext = () => {
if (idx >= files.length) {
UI.overallProgressContainer.style.display = "none";
UI.overallProgress.value = 0;
UI.overallStatus.textContent = "";
UI.currentFileName.textContent = "";
fetchFiles();
return;
}
const file = files[idx];
UI.currentFileName.textContent = file.name;
const xhr = new XMLHttpRequest();
const form = new FormData();
form.append("uploadfile", file);
xhr.upload.addEventListener("progress", (e) => {
if (!e.lengthComputable) return;
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", () => {
if (xhr.status === 200) {
uploadedBytes += file.size;
} else {
console.error("Upload failed with status", xhr.status);
}
idx++;
uploadNext();
});
xhr.addEventListener("error", () => {
console.error("Network/server error during upload.");
idx++;
uploadNext();
});
xhr.open("POST", AppConfig.Endpoints.Upload);
xhr.send(form);
};
fetchFiles();
uploadNext();
}
document.addEventListener("DOMContentLoaded", initApp);
})();

130
app/assets/style.css Normal file
View File

@@ -0,0 +1,130 @@
/* Base */
body {
background-color: #0d1117;
color: #fefefe;
font-family: monospace, monospace;
margin: 20px auto;
max-width: 800px;
padding: 0 10px;
}
/* Dropzone */
#dropzone {
border: 2px dashed #888;
border-radius: 10px;
color: #fefefe;
cursor: pointer;
margin-bottom: 20px;
padding: 30px;
text-align: center;
transition: all 0.3s ease;
}
#dropzone:hover {
color: #0fff50;
}
/* File list */
#file-list {
list-style: none;
margin-top: 20px;
padding-left: 0;
}
#file-list li {
align-items: center;
display: flex;
flex-wrap: wrap;
margin-bottom: 8px;
}
.sinkholeModeInfo {
color: #888;
text-align: center;
}
/* Links */
.delete-link {
color: #fefefe;
font-size: 14px;
margin-left: 8px;
text-decoration: none;
}
.delete-link:hover {
color: #0fff50;
}
.download-link {
color: #fefefe;
text-decoration: none;
word-break: break-word;
}
.download-link:hover {
color: #0fff50;
}
.logo {
color: #0fff50;
text-decoration: none;
text-align: center;
}
/* Progress */
#currentFileName {
font-weight: bold;
margin-bottom: 5px;
word-break: break-word;
}
#overallProgress {
accent-color: #0fff50;
height: 20px;
width: 100%;
}
#overallProgress::-moz-progress-bar {
background-color: #0fff50;
}
#overallProgress::-webkit-progress-bar {
background-color: #333;
border-radius: 5px;
}
#overallProgress::-webkit-progress-value {
background-color: #0fff50;
border-radius: 5px;
}
#overallProgressContainer {
margin-bottom: 20px;
}
.status {
color: #fefefe;
font-size: 14px;
margin-top: 4px;
}
/* Responsive */
@media (max-width: 600px) {
body {
padding: 0 15px;
}
#dropzone {
font-size: 14px;
padding: 20px;
}
.delete-link {
font-size: 12px;
margin-left: 5px;
}
.download-link {
font-size: 14px;
}
}