Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
5689189c8c
|
|||
95486e86e1
|
|||
0edc734766
|
|||
419a7e5c1a
|
|||
62064721d0
|
|||
7b6db3c5b4
|
|||
5c0800b5ce
|
|||
863171f66b
|
|||
6db6127522
|
|||
3172f90999
|
16
app/app.go
16
app/app.go
@@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.0x0001f346.de/andreas/ablage/config"
|
"git.0x0001f346.de/andreas/ablage/config"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
@@ -25,7 +24,7 @@ var assetScriptJS []byte
|
|||||||
//go:embed assets/style.css
|
//go:embed assets/style.css
|
||||||
var assetStyleCSS []byte
|
var assetStyleCSS []byte
|
||||||
|
|
||||||
func Init() {
|
func Init() error {
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
|
|
||||||
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -57,16 +56,14 @@ func Init() {
|
|||||||
config.PrintStartupBanner()
|
config.PrintStartupBanner()
|
||||||
err := http.ListenAndServe(fmt.Sprintf(":%d", config.GetPortToListenOn()), handler)
|
err := http.ListenAndServe(fmt.Sprintf(":%d", config.GetPortToListenOn()), handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Ablage exited with error:\n%v\n", err)
|
return fmt.Errorf("Webserver exited with error: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsCert, err := tls.X509KeyPair(config.GetTLSCertificate(), config.GetTLSKey())
|
tlsCert, err := tls.X509KeyPair(config.GetTLSCertificate(), config.GetTLSKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Faild to parse PEM encoded public/private key pair:\n%v\n", err)
|
return fmt.Errorf("Faild to parse PEM encoded public/private key pair: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
@@ -83,9 +80,10 @@ func Init() {
|
|||||||
|
|
||||||
err = server.ListenAndServeTLS("", "")
|
err = server.ListenAndServeTLS("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Ablage exited with error:\n%v\n", err)
|
return fmt.Errorf("Webserver exited with error: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientIP(r *http.Request) string {
|
func getClientIP(r *http.Request) string {
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
(() => {
|
(() => {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const DEFAULT_DROPZONE_TEXT = "Drag & drop files here or click to select";
|
|
||||||
const state = {
|
const state = {
|
||||||
config: null,
|
config: null,
|
||||||
files: {},
|
files: {},
|
||||||
@@ -9,28 +8,32 @@
|
|||||||
errorTimeout: null,
|
errorTimeout: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function appLoop() {
|
// ===== app ==============================
|
||||||
|
|
||||||
|
async function appInit() {
|
||||||
|
uiBuildElements();
|
||||||
|
uiCacheElements();
|
||||||
|
uiBindEvents();
|
||||||
|
|
||||||
|
await configLoad();
|
||||||
|
appUpdate();
|
||||||
|
|
||||||
|
setInterval(appUpdate, 5 * 1000);
|
||||||
|
setInterval(configLoad, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appUpdate() {
|
||||||
if (state.config === null) {
|
if (state.config === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI();
|
uiUpdate();
|
||||||
fetchFiles();
|
fileListFetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initApp() {
|
// ===== config ===========================
|
||||||
addUIElementsToBody();
|
|
||||||
getUIElements();
|
|
||||||
addEventListeners();
|
|
||||||
|
|
||||||
await loadAppConfig();
|
async function configLoad() {
|
||||||
appLoop();
|
|
||||||
|
|
||||||
setInterval(appLoop, 5 * 1000);
|
|
||||||
setInterval(loadAppConfig, 60 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAppConfig() {
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/config/", { cache: "no-store" });
|
const res = await fetch("/config/", { cache: "no-store" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -43,31 +46,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFiles() {
|
// ===== files ============================
|
||||||
if (state.config.Modes.Sinkhole) {
|
|
||||||
clearFileList();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
async function fileDeleteClickHandler(event, file) {
|
||||||
const files = await fetchFileList();
|
|
||||||
state.files = {};
|
|
||||||
clearFileList();
|
|
||||||
renderFileList(files);
|
|
||||||
} catch (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();
|
event.preventDefault();
|
||||||
if (!confirm(`Do you really want to delete "${file.Name}"?`)) return;
|
if (!confirm(`Do you really want to delete "${file.Name}"?`)) return;
|
||||||
|
|
||||||
@@ -81,188 +62,64 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showSuccess("File deleted: " + file.Name);
|
uiShowSuccess("File deleted: " + file.Name);
|
||||||
} else {
|
} else {
|
||||||
showError("Delete failed");
|
uiShowError("Delete failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchFiles();
|
fileListFetch();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError("Delete failed");
|
uiShowError("Delete failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEventListeners() {
|
async function fileListFetch() {
|
||||||
state.ui.dropzone.addEventListener("click", () =>
|
if (state.config.Modes.Sinkhole) {
|
||||||
state.ui.fileInput.click()
|
fileListClear();
|
||||||
);
|
return;
|
||||||
state.ui.fileInput.addEventListener("change", () => {
|
}
|
||||||
if (state.ui.fileInput.files.length > 0)
|
|
||||||
uploadFiles(state.ui.fileInput.files);
|
try {
|
||||||
});
|
let files = await fileListRequest();
|
||||||
state.ui.dropzone.addEventListener("dragover", (e) => {
|
files = fileSortFiles(files, "name-asc");
|
||||||
e.preventDefault();
|
state.files = {};
|
||||||
state.ui.dropzone.style.borderColor = "#0fff50";
|
fileListClear();
|
||||||
});
|
fileListRender(files);
|
||||||
state.ui.dropzone.addEventListener("dragleave", () => {
|
} catch (err) {
|
||||||
state.ui.dropzone.style.borderColor = "#888";
|
console.error("fileListFetch failed:", err);
|
||||||
});
|
}
|
||||||
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() {
|
async function fileListRequest() {
|
||||||
document.body.innerHTML = "";
|
const res = await fetch(state.config.Endpoints.Files, {
|
||||||
|
cache: "no-store",
|
||||||
const aLogo = document.createElement("a");
|
});
|
||||||
aLogo.href = "/";
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
aLogo.className = "logo";
|
return res.json();
|
||||||
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() {
|
function fileListClear() {
|
||||||
if (state.ui.fileList) state.ui.fileList.innerHTML = "";
|
if (state.ui.fileList) state.ui.fileList.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDownloadLink(file) {
|
function fileListRender(files) {
|
||||||
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) {
|
|
||||||
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 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;
|
if (!state.ui.fileList) return;
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
state.files[file.Name] = true;
|
state.files[file.Name] = true;
|
||||||
|
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.appendChild(createDownloadLink(file));
|
li.appendChild(uiCreateDownloadLink(file));
|
||||||
|
|
||||||
if (!state.config.Modes.Readonly) {
|
if (!state.config.Modes.Readonly) {
|
||||||
li.appendChild(createDeleteLink(file));
|
li.appendChild(uiCreateDeleteLink(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
state.ui.fileList.appendChild(li);
|
state.ui.fileList.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeFilename(dirtyFilename) {
|
function fileSanitizeName(dirtyFilename) {
|
||||||
if (!dirtyFilename || dirtyFilename.trim() === "") {
|
if (!dirtyFilename || dirtyFilename.trim() === "") {
|
||||||
return "upload.bin";
|
return "upload.bin";
|
||||||
}
|
}
|
||||||
@@ -312,28 +169,223 @@
|
|||||||
return cleanedFilename + extension;
|
return cleanedFilename + extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function fileSortFiles(files, mode = "name-asc") {
|
||||||
showMessage(msg, "error", 2000);
|
function cmpCodePoint(a, b) {
|
||||||
|
if (a === b) return 0;
|
||||||
|
return a < b ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr = files.slice();
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case "name-asc":
|
||||||
|
arr.sort((a, b) => cmpCodePoint(a.Name, b.Name));
|
||||||
|
break;
|
||||||
|
case "name-desc":
|
||||||
|
arr.sort((a, b) => cmpCodePoint(b.Name, a.Name));
|
||||||
|
break;
|
||||||
|
case "size-asc":
|
||||||
|
arr.sort((a, b) => a.Size - b.Size);
|
||||||
|
break;
|
||||||
|
case "size-desc":
|
||||||
|
arr.sort((a, b) => b.Size - a.Size);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMessage(msg, type, duration = 2000) {
|
function fileValidateBeforeUpload(files) {
|
||||||
|
for (const f of files) {
|
||||||
|
const safeName = fileSanitizeName(f.name);
|
||||||
|
if (safeName === ".upload") {
|
||||||
|
uiShowError("Invalid filename: .upload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (safeName in state.files) {
|
||||||
|
uiShowError("File already exists: " + f.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ui ===============================
|
||||||
|
|
||||||
|
function uiBindEvents() {
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
() => (state.ui.dropzone.innerHTML = uiGetDropzoneText())
|
||||||
|
);
|
||||||
|
|
||||||
|
state.ui.dropzone.addEventListener("click", () =>
|
||||||
|
state.ui.fileInput.click()
|
||||||
|
);
|
||||||
|
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) uploadStart(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.ui.fileInput.addEventListener("change", () => {
|
||||||
|
if (state.ui.fileInput.files.length > 0)
|
||||||
|
uploadStart(state.ui.fileInput.files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiBuildElements() {
|
||||||
|
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 = uiGetDropzoneText();
|
||||||
|
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 uiCacheElements() {
|
||||||
|
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 uiCreateDeleteLink(file) {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.className = "delete-link";
|
||||||
|
link.href = "#";
|
||||||
|
link.textContent = " [Delete]";
|
||||||
|
link.title = "Delete file";
|
||||||
|
link.addEventListener("click", (e) => fileDeleteClickHandler(e, file));
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiCreateDownloadLink(file) {
|
||||||
|
const size = uiFormatSize(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 uiFormatSize(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 uiFormatSpeed(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 uiGetDropzoneText() {
|
||||||
|
if (window.innerWidth <= 480) {
|
||||||
|
return "Tap to upload files";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Drag & drop files here or click to upload";
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiInitProgress() {
|
||||||
|
state.ui.overallProgressContainer.style.display = "block";
|
||||||
|
state.ui.overallProgress.value = 0;
|
||||||
|
state.ui.overallStatus.textContent = "";
|
||||||
|
state.ui.currentFileName.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiShowError(msg) {
|
||||||
|
uiShowMessage(msg, "error", 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiShowMessage(msg, type, duration = 2000) {
|
||||||
state.ui.dropzone.innerHTML = msg;
|
state.ui.dropzone.innerHTML = msg;
|
||||||
state.ui.dropzone.classList.add(type);
|
state.ui.dropzone.classList.add(type);
|
||||||
|
|
||||||
if (state.errorTimeout) clearTimeout(state.errorTimeout);
|
if (state.errorTimeout) clearTimeout(state.errorTimeout);
|
||||||
|
|
||||||
state.errorTimeout = setTimeout(() => {
|
state.errorTimeout = setTimeout(() => {
|
||||||
state.ui.dropzone.innerHTML = DEFAULT_DROPZONE_TEXT;
|
state.ui.dropzone.innerHTML = uiGetDropzoneText();
|
||||||
state.ui.dropzone.classList.remove(type);
|
state.ui.dropzone.classList.remove(type);
|
||||||
state.errorTimeout = null;
|
state.errorTimeout = null;
|
||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSuccess(msg) {
|
function uiShowSuccess(msg) {
|
||||||
showMessage(msg, "success", 1500);
|
uiShowMessage(msg, "success", 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI() {
|
function uiUpdate() {
|
||||||
if (state.config.Modes.Readonly) {
|
if (state.config.Modes.Readonly) {
|
||||||
state.ui.dropzone.style.display = "none";
|
state.ui.dropzone.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
@@ -349,75 +401,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadFiles(fileListLike) {
|
function uiUpdateProgress(totalUploaded, totalSize, startTime) {
|
||||||
const files = Array.from(fileListLike);
|
|
||||||
if (files.length === 0) return;
|
|
||||||
|
|
||||||
if (!validateFiles(files)) return;
|
|
||||||
|
|
||||||
initUIProgress();
|
|
||||||
|
|
||||||
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
||||||
let uploadedBytes = 0;
|
|
||||||
let currentIndex = 0;
|
|
||||||
const startTime = Date.now();
|
|
||||||
let allSuccessful = true;
|
|
||||||
|
|
||||||
function uploadNext() {
|
|
||||||
if (currentIndex >= files.length) {
|
|
||||||
finishUpload(allSuccessful);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = files[currentIndex];
|
|
||||||
state.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) {
|
|
||||||
updateProgressUI(uploadedBytes + e.loaded, totalSize, startTime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.addEventListener("load", () => {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
uploadedBytes += file.size;
|
|
||||||
} else if (xhr.status === 409) {
|
|
||||||
showError("File already exists: " + file.name);
|
|
||||||
allSuccessful = false;
|
|
||||||
} else {
|
|
||||||
showError("Upload failed: " + file.name);
|
|
||||||
allSuccessful = false;
|
|
||||||
}
|
|
||||||
currentIndex++;
|
|
||||||
uploadNext();
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.addEventListener("error", () => {
|
|
||||||
showError("Network or server error during upload.");
|
|
||||||
allSuccessful = false;
|
|
||||||
currentIndex++;
|
|
||||||
uploadNext();
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.open("POST", state.config.Endpoints.Upload);
|
|
||||||
xhr.send(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFiles();
|
|
||||||
uploadNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgressUI(totalUploaded, totalSize, startTime) {
|
|
||||||
const percent = (totalUploaded / totalSize) * 100;
|
const percent = (totalUploaded / totalSize) * 100;
|
||||||
state.ui.overallProgress.value = percent;
|
state.ui.overallProgress.value = percent;
|
||||||
|
|
||||||
const elapsed = (Date.now() - startTime) / 1000;
|
const elapsed = (Date.now() - startTime) / 1000;
|
||||||
const speed = totalUploaded / elapsed;
|
const speed = totalUploaded / elapsed;
|
||||||
const speedStr = humanReadableSpeed(speed);
|
const speedStr = uiFormatSpeed(speed);
|
||||||
|
|
||||||
const remainingBytes = totalSize - totalUploaded;
|
const remainingBytes = totalSize - totalUploaded;
|
||||||
const etaSec = speed > 0 ? remainingBytes / speed : Infinity;
|
const etaSec = speed > 0 ? remainingBytes / speed : Infinity;
|
||||||
@@ -433,20 +423,82 @@
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFiles(files) {
|
// ===== upload ===========================
|
||||||
for (const f of files) {
|
|
||||||
const safeName = sanitizeFilename(f.name);
|
function uploadFinish(success) {
|
||||||
if (safeName === ".upload") {
|
state.ui.overallProgressContainer.style.display = "none";
|
||||||
showError("Invalid filename: .upload");
|
state.ui.overallProgress.value = 0;
|
||||||
return false;
|
state.ui.overallStatus.textContent = "";
|
||||||
}
|
state.ui.currentFileName.textContent = "";
|
||||||
if (safeName in state.files) {
|
fileListFetch();
|
||||||
showError("File already exists: " + f.name);
|
if (success) {
|
||||||
return false;
|
uiShowSuccess("Upload successful");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", initApp);
|
function uploadStart(fileListLike) {
|
||||||
|
const files = Array.from(fileListLike);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
if (!fileValidateBeforeUpload(files)) return;
|
||||||
|
|
||||||
|
uiInitProgress();
|
||||||
|
|
||||||
|
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
||||||
|
let uploadedBytes = 0;
|
||||||
|
let currentIndex = 0;
|
||||||
|
const startTime = Date.now();
|
||||||
|
let allSuccessful = true;
|
||||||
|
|
||||||
|
function uploadNext() {
|
||||||
|
if (currentIndex >= files.length) {
|
||||||
|
uploadFinish(allSuccessful);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = files[currentIndex];
|
||||||
|
state.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) {
|
||||||
|
uiUpdateProgress(uploadedBytes + e.loaded, totalSize, startTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("load", () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
uploadedBytes += file.size;
|
||||||
|
} else if (xhr.status === 409) {
|
||||||
|
uiShowError("File already exists: " + file.name);
|
||||||
|
allSuccessful = false;
|
||||||
|
} else {
|
||||||
|
uiShowError("Upload failed: " + file.name);
|
||||||
|
allSuccessful = false;
|
||||||
|
}
|
||||||
|
currentIndex++;
|
||||||
|
uploadNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("error", () => {
|
||||||
|
uiShowError("Network or server error during upload.");
|
||||||
|
allSuccessful = false;
|
||||||
|
currentIndex++;
|
||||||
|
uploadNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open("POST", state.config.Endpoints.Upload);
|
||||||
|
xhr.send(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileListFetch();
|
||||||
|
uploadNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== init ============================
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", appInit);
|
||||||
})();
|
})();
|
||||||
|
74
app/http.go
74
app/http.go
@@ -82,32 +82,25 @@ func httpGetFiles(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
|
|||||||
json.NewEncoder(w).Encode([]FileInfo{})
|
json.NewEncoder(w).Encode([]FileInfo{})
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(config.GetPathDataFolder())
|
files, err := filesystem.GetFileListOfDataFolder()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
log.Fatalf("[Error] %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
files := make([]FileInfo, 0, len(entries))
|
fileInfos := make([]FileInfo, 0, len(files))
|
||||||
|
|
||||||
for _, entry := range entries {
|
for filename, sizeInBytes := range files {
|
||||||
if entry.IsDir() {
|
fileInfos = append(
|
||||||
continue
|
fileInfos,
|
||||||
}
|
FileInfo{
|
||||||
|
Name: filename,
|
||||||
info, err := entry.Info()
|
Size: sizeInBytes,
|
||||||
if err != nil {
|
},
|
||||||
continue
|
)
|
||||||
}
|
|
||||||
|
|
||||||
files = append(files, FileInfo{
|
|
||||||
Name: info.Name(),
|
|
||||||
Size: info.Size(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(files)
|
json.NewEncoder(w).Encode(fileInfos)
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpGetFilesDeleteFilename(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
func httpGetFilesDeleteFilename(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
@@ -121,25 +114,10 @@ func httpGetFilesDeleteFilename(w http.ResponseWriter, r *http.Request, ps httpr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(config.GetPathDataFolder())
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files := map[string]int64{}
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
info, err := entry.Info()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
files[info.Name()] = info.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := ps.ByName("filename")
|
filename := ps.ByName("filename")
|
||||||
|
|
||||||
|
files, err := filesystem.GetFileListOfDataFolder()
|
||||||
|
|
||||||
sizeInBytes, fileExists := files[filename]
|
sizeInBytes, fileExists := files[filename]
|
||||||
if !fileExists {
|
if !fileExists {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -147,8 +125,7 @@ func httpGetFilesDeleteFilename(w http.ResponseWriter, r *http.Request, ps httpr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fullPath := filepath.Join(config.GetPathDataFolder(), filename)
|
err = filesystem.DeleteFile(filename)
|
||||||
err = os.Remove(fullPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -167,15 +144,19 @@ func httpGetFilesGetFilename(w http.ResponseWriter, r *http.Request, ps httprout
|
|||||||
}
|
}
|
||||||
|
|
||||||
filename := ps.ByName("filename")
|
filename := ps.ByName("filename")
|
||||||
filePath := filepath.Join(config.GetPathDataFolder(), filename)
|
|
||||||
|
|
||||||
info, err := os.Stat(filePath)
|
files, err := filesystem.GetFileListOfDataFolder()
|
||||||
if err != nil || info.IsDir() {
|
if err != nil {
|
||||||
|
log.Fatalf("[Error] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeInBytes, fileExists := files[filename]
|
||||||
|
if !fileExists {
|
||||||
http.Error(w, "404 File Not Found", http.StatusNotFound)
|
http.Error(w, "404 File Not Found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("| Download | %-21s | %-10s | %s\n", getClientIP(r), filesystem.GetHumanReadableSize(info.Size()), filename)
|
log.Printf("| Download | %-21s | %-10s | %s\n", getClientIP(r), filesystem.GetHumanReadableSize(sizeInBytes), filename)
|
||||||
|
|
||||||
extension := strings.ToLower(filepath.Ext(filename))
|
extension := strings.ToLower(filepath.Ext(filename))
|
||||||
mimeType := mime.TypeByExtension(extension)
|
mimeType := mime.TypeByExtension(extension)
|
||||||
@@ -190,7 +171,7 @@ func httpGetFilesGetFilename(w http.ResponseWriter, r *http.Request, ps httprout
|
|||||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.ServeFile(w, r, filePath)
|
http.ServeFile(w, r, filepath.Join(config.GetPathDataFolder(), filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpGetRoot(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
func httpGetRoot(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
@@ -214,6 +195,11 @@ func httpPostUpload(w http.ResponseWriter, r *http.Request, ps httprouter.Params
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := filesystem.GetFileListOfDataFolder()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[Error] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
reader, err := r.MultipartReader()
|
reader, err := r.MultipartReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not get multipart reader: %v", err), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not get multipart reader: %v", err), http.StatusBadRequest)
|
||||||
|
3
build.sh
3
build.sh
@@ -1,2 +1,3 @@
|
|||||||
|
GOOS=android GOARCH=arm64 go build -o build/ablage-android-arm64 .
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o build/ablage-linux-amd64 .
|
||||||
GOOS=windows GOARCH=amd64 go build -o build/ablage-windows-amd64.exe .
|
GOOS=windows GOARCH=amd64 go build -o build/ablage-windows-amd64.exe .
|
||||||
GOOS=linux GOARCH=amd64 go build -o build/ablage-linux-amd64 .
|
|
@@ -50,13 +50,13 @@ func generateSelfSignedTLSCertificate() error {
|
|||||||
|
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Failed to create new x509 certificate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
key, err := x509.MarshalECPrivateKey(privateKey)
|
key, err := x509.MarshalECPrivateKey(privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Failed to marshal EC private key: %v", err)
|
||||||
}
|
}
|
||||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: key})
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: key})
|
||||||
|
|
||||||
@@ -77,17 +77,17 @@ func loadOrGenerateTLSCertificate() error {
|
|||||||
|
|
||||||
_, err := tls.LoadX509KeyPair(pathTLSCertFile, pathTLSKeyFile)
|
_, err := tls.LoadX509KeyPair(pathTLSCertFile, pathTLSKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error: Failed to load TLS certificate or key: %w", err)
|
return fmt.Errorf("Failed to load TLS certificate or key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
certData, err := os.ReadFile(pathTLSCertFile)
|
certData, err := os.ReadFile(pathTLSCertFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error: Failed to read TLS certificate file: %w", err)
|
return fmt.Errorf("Failed to read TLS certificate file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyData, err := os.ReadFile(pathTLSKeyFile)
|
keyData, err := os.ReadFile(pathTLSKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error: Failed to read TLS key file: %w", err)
|
return fmt.Errorf("Failed to read TLS key file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
selfSignedTLSCertificate = certData
|
selfSignedTLSCertificate = certData
|
||||||
|
@@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultBasicAuthUsername string = "ablage"
|
const DefaultBasicAuthUsername string = "ablage"
|
||||||
@@ -10,26 +9,29 @@ 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.1"
|
const VersionString string = "1.2"
|
||||||
|
|
||||||
var randomBasicAuthPassword string = generateRandomPassword()
|
var randomBasicAuthPassword string = generateRandomPassword()
|
||||||
|
|
||||||
func Init() {
|
func Init() error {
|
||||||
err := gatherDefaultPaths()
|
err := gatherDefaultPaths()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parseFlags()
|
err = parseFlags()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if GetReadonlyMode() && GetSinkholeMode() {
|
if GetReadonlyMode() && GetSinkholeMode() {
|
||||||
fmt.Println("Error: Cannot enable both readonly and sinkhole modes at the same time.")
|
return fmt.Errorf("Cannot enable both readonly and sinkhole modes at the same time.")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadOrGenerateTLSCertificate()
|
err = loadOrGenerateTLSCertificate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
return err
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ var defaultPathUploadFolder string = ""
|
|||||||
func gatherDefaultPaths() error {
|
func gatherDefaultPaths() error {
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[Error] Could not determine binary path: %v", err)
|
return fmt.Errorf("Could not determine binary path: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultPathDataFolder = filepath.Join(filepath.Dir(execPath), DefaultNameDataFolder)
|
defaultPathDataFolder = filepath.Join(filepath.Dir(execPath), DefaultNameDataFolder)
|
||||||
|
@@ -73,7 +73,7 @@ func generateRandomPassword() string {
|
|||||||
return base64.RawURLEncoding.EncodeToString(b)[:LengthOfRandomBasicAuthPassword]
|
return base64.RawURLEncoding.EncodeToString(b)[:LengthOfRandomBasicAuthPassword]
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlags() {
|
func parseFlags() error {
|
||||||
flag.BoolVar(&basicAuthMode, "auth", false, "Enable basic authentication.")
|
flag.BoolVar(&basicAuthMode, "auth", false, "Enable basic authentication.")
|
||||||
flag.BoolVar(&httpMode, "http", false, "Enable http mode. Nothing will be encrypted.")
|
flag.BoolVar(&httpMode, "http", false, "Enable http mode. Nothing will be encrypted.")
|
||||||
flag.BoolVar(&readonlyMode, "readonly", false, "Enable readonly mode. No files can be uploaded or deleted.")
|
flag.BoolVar(&readonlyMode, "readonly", false, "Enable readonly mode. No files can be uploaded or deleted.")
|
||||||
@@ -87,10 +87,25 @@ func parseFlags() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
parseFlagValueBasicAuthPassword()
|
parseFlagValueBasicAuthPassword()
|
||||||
parseFlagValuePortToListenOn()
|
|
||||||
|
err := parseFlagValuePortToListenOn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
parseFlagValuePathDataFolder()
|
parseFlagValuePathDataFolder()
|
||||||
parseFlagValuePathTLSCertFile()
|
|
||||||
parseFlagValuePathTLSKeyFile()
|
err = parseFlagValuePathTLSCertFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = parseFlagValuePathTLSKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlagValueBasicAuthPassword() {
|
func parseFlagValueBasicAuthPassword() {
|
||||||
@@ -106,60 +121,56 @@ func parseFlagValuePathDataFolder() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(pathDataFolder)
|
|
||||||
if err != nil {
|
|
||||||
pathDataFolder = defaultPathDataFolder
|
|
||||||
pathUploadFolder = defaultPathUploadFolder
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
pathDataFolder = defaultPathDataFolder
|
|
||||||
pathUploadFolder = defaultPathUploadFolder
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pathUploadFolder = filepath.Join(pathDataFolder, DefaultNameUploadFolder)
|
pathUploadFolder = filepath.Join(pathDataFolder, DefaultNameUploadFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlagValuePortToListenOn() {
|
func parseFlagValuePortToListenOn() error {
|
||||||
if portToListenOn < 1 || portToListenOn > 65535 {
|
if portToListenOn < 1 || portToListenOn > 65535 {
|
||||||
portToListenOn = DefaultPortToListenOn
|
return fmt.Errorf("The port must be between 1 and 65535 (both ports included).")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlagValuePathTLSCertFile() {
|
func parseFlagValuePathTLSCertFile() error {
|
||||||
if pathTLSCertFile == "" {
|
if pathTLSCertFile == "" {
|
||||||
pathTLSKeyFile = ""
|
if pathTLSKeyFile != "" {
|
||||||
return
|
return fmt.Errorf("Both a certificate and the corresponding key must be provided.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(pathTLSCertFile)
|
info, err := os.Stat(pathTLSCertFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: Failed to read cert: %v\n", err)
|
return fmt.Errorf("Failed to read cert: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
fmt.Fprintf(os.Stderr, "Error: Cert must be a file\n")
|
fmt.Fprintf(os.Stderr, "Error: Cert must be a file\n")
|
||||||
os.Exit(1)
|
return fmt.Errorf("Cert must be a valid file.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlagValuePathTLSKeyFile() {
|
func parseFlagValuePathTLSKeyFile() error {
|
||||||
if pathTLSKeyFile == "" {
|
if pathTLSKeyFile == "" {
|
||||||
pathTLSCertFile = ""
|
if pathTLSCertFile != "" {
|
||||||
return
|
return fmt.Errorf("Both a certificate and the corresponding key must be provided.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(pathTLSKeyFile)
|
info, err := os.Stat(pathTLSKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: Failed to read key: %v\n", err)
|
return fmt.Errorf("Failed to read key: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
fmt.Fprintf(os.Stderr, "Error: Key must be a file\n")
|
return fmt.Errorf("Key must be a valid file.")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package filesystem
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -10,18 +11,55 @@ import (
|
|||||||
"git.0x0001f346.de/andreas/ablage/config"
|
"git.0x0001f346.de/andreas/ablage/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() error {
|
||||||
err := prepareDataFolder()
|
err := createWriteableFolder(config.GetPathDataFolder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("err")
|
return err
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = prepareUploadDir()
|
err = os.RemoveAll(config.GetPathUploadFolder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("err")
|
return fmt.Errorf("Could not delete upload folder '%s': %v", config.GetPathUploadFolder(), err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = createWriteableFolder(config.GetPathUploadFolder())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFile(filename string) error {
|
||||||
|
return os.Remove(filepath.Join(config.GetPathDataFolder(), filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileListOfDataFolder() (map[string]int64, error) {
|
||||||
|
entries, err := os.ReadDir(config.GetPathDataFolder())
|
||||||
|
if err != nil {
|
||||||
|
return map[string]int64{}, fmt.Errorf(
|
||||||
|
"Data folder '%s' became unavailable: %v",
|
||||||
|
config.GetPathDataFolder(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := map[string]int64{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WARN: Could not read info for '%s': %v", entry.Name(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
files[info.Name()] = info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHumanReadableSize(bytes int64) string {
|
func GetHumanReadableSize(bytes int64) string {
|
||||||
@@ -77,60 +115,27 @@ func SanitizeFilename(dirtyFilename string) string {
|
|||||||
return cleanedFilename + extension
|
return cleanedFilename + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDataFolder() error {
|
func createWriteableFolder(path string) error {
|
||||||
info, err := os.Stat(config.GetPathDataFolder())
|
info, err := os.Stat(path)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.Mkdir(config.GetPathDataFolder(), 0755); err != nil {
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
return fmt.Errorf("Error: Could not create folder '%s': %v", config.GetPathDataFolder(), err)
|
return fmt.Errorf("Could not create folder '%s': %v", path, err)
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("Error: Could not access '%s': %v", config.GetPathDataFolder(), err)
|
return fmt.Errorf("Could not access '%s': %v", path, err)
|
||||||
} else if !info.IsDir() {
|
} else if !info.IsDir() {
|
||||||
return fmt.Errorf("Error: '%s' exists but is not a directory", config.GetPathDataFolder())
|
return fmt.Errorf("'%s' exists but is not a directory", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pathTestFile := filepath.Join(config.GetPathDataFolder(), ".write_test")
|
pathTestFile := filepath.Join(path, ".write_test")
|
||||||
err = os.WriteFile(pathTestFile, []byte("test"), 0644)
|
err = os.WriteFile(pathTestFile, []byte("test"), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error: Could not create test file '%s': %v", pathTestFile, err)
|
return fmt.Errorf("Could not create test file '%s': %v", pathTestFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Remove(pathTestFile)
|
err = os.Remove(pathTestFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error: Could not delete test file '%s': %v", pathTestFile, err)
|
return fmt.Errorf("Could not delete test file '%s': %v", pathTestFile, err)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareUploadDir() error {
|
|
||||||
info, err := os.Stat(config.GetPathUploadFolder())
|
|
||||||
if err == nil {
|
|
||||||
if !info.IsDir() {
|
|
||||||
return fmt.Errorf("%s exists, but is not a folder", config.GetPathUploadFolder())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.RemoveAll(config.GetPathUploadFolder())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error: Could not delete upload folder '%s': %v", config.GetPathUploadFolder(), err)
|
|
||||||
}
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("Error: '%s' exists but is somewhat broken", config.GetPathUploadFolder())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(config.GetPathUploadFolder(), 0755); err != nil {
|
|
||||||
return fmt.Errorf("Error: Could not create upload folder '%s': %v", config.GetPathUploadFolder(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pathTestFile := filepath.Join(config.GetPathUploadFolder(), ".write_test")
|
|
||||||
err = os.WriteFile(pathTestFile, []byte("test"), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error: Could not create test file '%s': %v", pathTestFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Remove(pathTestFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error: Could not delete test file '%s': %v", pathTestFile, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
23
main.go
23
main.go
@@ -1,13 +1,30 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"git.0x0001f346.de/andreas/ablage/app"
|
"git.0x0001f346.de/andreas/ablage/app"
|
||||||
"git.0x0001f346.de/andreas/ablage/config"
|
"git.0x0001f346.de/andreas/ablage/config"
|
||||||
"git.0x0001f346.de/andreas/ablage/filesystem"
|
"git.0x0001f346.de/andreas/ablage/filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config.Init()
|
err := config.Init()
|
||||||
filesystem.Init()
|
if err != nil {
|
||||||
app.Init()
|
fmt.Fprintf(os.Stderr, "[Error] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesystem.Init()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.Init()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user