rewrite html generator with html/template

This commit is contained in:
Altair-sh
2026-11-06 22:58:18 +01:00
parent 0bfa5ed35c
commit 509668088f
10 changed files with 363 additions and 110 deletions
+181
View File
@@ -0,0 +1,181 @@
{{- /* Directory listing page.
Shows directory content and has upload form. */ -}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
<style>
body { font-family: sans-serif; margin: 2em; }
td { padding: .2em 1em .2em 0; }
td.size, td.date { color: #666; white-space: nowrap; }
caption { text-align: left; color: #666; padding-bottom: .5em; }
#status p { margin: .3em 0; }
.error { color: #c00; }
</style>
</head>
<body>
<h1>{{.Path}}</h1>
{{- /* upload form, visible only if share has write_password */}}
{{if .Writable}}
<form id="upload" data-base="{{.PathUrl}}">
<input type="file" id="files" multiple required>
<input type="password" id="password" placeholder="write password" autocomplete="current-password" required>
<button>Upload</button>
</form>
<div id="status"></div>
{{- /* dialog that appears if file with such name exists in the directory */}}
<dialog id="replace">
<p></p>
<form method="dialog">
<button value="yes">Yes</button>
<button value="no">No</button>
</form>
</dialog>
{{end}}
{{- /* listing element is replaced by id after file upload */}}
<table id="listing">
<caption>Free space: {{.FreeSpace}}</caption>
<tr><td><a href="{{.ParentUrl}}">../</a></td></tr>
{{- range .Entries}}
<tr>
{{- if .IsDir}}
{{- /*Directory links end with '/', because browser remembers password for directory with trailing slash.*/}}
<td><a href="{{.Url}}/">{{.Name}}/</a></td>
{{- else}}
{{- /* download attribute: clicking a file link saves the file instead of opening it in the browser. */}}
<td><a href="{{.Url}}" download>{{.Name}}</a></td>
{{- end}}
<td class="size">{{.Size}}</td>
<td class="date">{{.Date}}</td>
</tr>
{{- end}}
</table>
{{- /*
Upload: for each file asks the server whether the upload would be accepted
(OPTIONS with X-Upload-Size), asks before replacing an existing file,
uploads with progress, then reloads the file table.
The OPTIONS check comes first, because a PUT rejected while the browser is still sending the file
ends with a connection reset, and the browser reports only "NetworkError" without reading response error.
Error messages are the response bodies.
The file is sent as the raw PUT body, the server doesn't need to parse multipart forms.
*/}}
{{if .Writable}}
<script>
const form = document.getElementById("upload");
const filesInput = document.getElementById("files");
const passwordInput = document.getElementById("password");
const uploadButton = form.querySelector("button");
const statusBox = document.getElementById("status");
const replaceDialog = document.getElementById("replace");
const base = form.dataset.base;
function formatSize(n) {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
for (; n >= 1024 && i < units.length - 1; i++)
n /= 1024;
return (i === 0 ? n : n.toFixed(1)) + units[i];
}
function authorization() {
const bytes = new TextEncoder().encode(":" + passwordInput.value);
return "Basic " + btoa(String.fromCharCode(...bytes));
}
// the server explains errors in the response body
function errorText(status, body) {
return body.trim() || "HTTP " + status;
}
function askReplace(name) {
replaceDialog.querySelector("p").textContent = 'File "' + name + '" already exists. Replace it?';
replaceDialog.returnValue = "";
replaceDialog.showModal();
return new Promise(resolve => replaceDialog.addEventListener("close",
() => resolve(replaceDialog.returnValue === "yes"), { once: true }));
}
function put(url, file, line) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", url);
xhr.setRequestHeader("Authorization", authorization());
const start = performance.now();
xhr.upload.onprogress = (e) => {
const seconds = (performance.now() - start) / 1000;
const speed = seconds > 0 ? e.loaded / seconds : 0;
line.textContent = 'Uploading file "' + file.name + '" '
+ formatSize(e.loaded) + "/" + formatSize(e.total) + " (" + formatSize(speed) + "/s)";
};
xhr.onload = () => xhr.status >= 200 && xhr.status < 300
? resolve() : reject(new Error(errorText(xhr.status, xhr.responseText)));
xhr.onerror = () => reject(new Error("connection failed"));
xhr.send(file);
});
}
async function upload(file, line) {
const url = base + encodeURIComponent(file.name);
line.textContent = 'Checking file "' + file.name + '"...';
const check = await fetch(url, {
method: "OPTIONS",
headers: { "Authorization": authorization(), "X-Upload-Size": String(file.size) },
});
// 200: the file exists and would be replaced, 201: it would be created,
// anything else: the upload would be rejected
if (check.status === 200) {
if (!await askReplace(file.name)) {
line.textContent = 'File "' + file.name + '" skipped';
return false;
}
}
else if (check.status !== 201) {
throw new Error(errorText(check.status, await check.text()));
}
await put(url, file, line);
line.textContent = 'File "' + file.name + '" uploaded';
return true;
}
async function refreshListing() {
const res = await fetch(location.href, { cache: "no-store" });
if (!res.ok)
return;
const page = new DOMParser().parseFromString(await res.text(), "text/html");
const table = page.getElementById("listing");
if (table)
document.getElementById("listing").replaceWith(table);
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
uploadButton.disabled = true;
statusBox.replaceChildren();
const notUploaded = new DataTransfer();
for (const file of filesInput.files) {
const line = document.createElement("p");
statusBox.append(line);
try {
if (!await upload(file, line))
notUploaded.items.add(file);
}
catch (err) {
line.textContent = 'Error: file "' + file.name + '": ' + err.message;
line.className = "error";
notUploaded.items.add(file);
}
}
// uploaded files are removed from the selection, the rest stays for another try
filesInput.files = notUploaded.files;
uploadButton.disabled = false;
await refreshListing();
});
</script>
{{end}}
</body>
</html>