10 Commits
v1.1 ... main

10 changed files with 503 additions and 431 deletions

View File

@@ -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 {

View File

@@ -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);
})(); })();

View File

@@ -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)

View File

@@ -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 .

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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
View File

@@ -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)
}
} }