Files
ablage/app/http.go
2025-08-26 21:54:30 +02:00

279 lines
7.3 KiB
Go

package app
import (
"encoding/json"
"fmt"
"io"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"git.0x0001f346.de/andreas/ablage/config"
"git.0x0001f346.de/andreas/ablage/filesystem"
"github.com/julienschmidt/httprouter"
)
const httpPathRoot string = "/"
const httpPathConfig string = "/config/"
const httpPathFaviconICO string = "/favicon.ico"
const httpPathFaviconSVG string = "/favicon.svg"
const httpPathFiles string = "/files/"
const httpPathFilesDeleteFilename string = "/files/delete/:filename"
const httpPathFilesGetFilename string = "/files/get/:filename"
const httpPathScriptJS string = "/script.js"
const httpPathStyleCSS string = "/style.css"
const httpPathUpload string = "/upload/"
func httpGetConfig(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
type Endpoints struct {
Files string `json:"Files"`
FilesDelete string `json:"FilesDelete"`
FilesGet string `json:"FilesGet"`
Upload string `json:"Upload"`
}
type Modes struct {
Readonly bool `json:"Readonly"`
Sinkhole bool `json:"Sinkhole"`
}
type Config struct {
Endpoints Endpoints `json:"Endpoints"`
Modes Modes `json:"Modes"`
}
var config Config = Config{
Endpoints: Endpoints{
Files: httpPathFiles,
FilesDelete: httpPathFilesDeleteFilename,
FilesGet: httpPathFilesGetFilename,
Upload: httpPathUpload,
},
Modes: Modes{
Readonly: config.GetReadonlyMode(),
Sinkhole: config.GetSinkholeMode(),
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(config)
}
func httpGetFaviconICO(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.Redirect(w, r, "/favicon.svg", http.StatusSeeOther)
}
func httpGetFaviconSVG(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("Content-Type", "image/svg+xml")
w.Write(assetFaviconSVG)
}
func httpGetFiles(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
type FileInfo struct {
Name string `json:"Name"`
Size int64 `json:"Size"`
}
if config.GetSinkholeMode() {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]FileInfo{})
}
entries, err := os.ReadDir(config.GetPathDataFolder())
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
files := make([]FileInfo, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
files = append(files, FileInfo{
Name: info.Name(),
Size: info.Size(),
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(files)
}
func httpGetFilesDeleteFilename(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if config.GetReadonlyMode() {
http.Error(w, "403 Forbidden", http.StatusForbidden)
return
}
if config.GetSinkholeMode() {
http.Error(w, "404 File Not Found", http.StatusNotFound)
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")
sizeInBytes, fileExists := files[filename]
if !fileExists {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
return
}
fullPath := filepath.Join(config.GetPathDataFolder(), filename)
err = os.Remove(fullPath)
if err != nil {
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
log.Printf("| Delete | %-21s | %-10s | %s\n", getClientIP(r), filesystem.GetHumanReadableSize(sizeInBytes), filename)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}
func httpGetFilesGetFilename(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if config.GetSinkholeMode() {
http.Error(w, "404 File Not Found", http.StatusNotFound)
return
}
filename := ps.ByName("filename")
filePath := filepath.Join(config.GetPathDataFolder(), filename)
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
http.Error(w, "404 File Not Found", http.StatusNotFound)
return
}
log.Printf("| Download | %-21s | %-10s | %s\n", getClientIP(r), filesystem.GetHumanReadableSize(info.Size()), filename)
extension := strings.ToLower(filepath.Ext(filename))
mimeType := mime.TypeByExtension(extension)
if mimeType == "" {
mimeType = "application/octet-stream"
}
w.Header().Set("Content-Type", mimeType)
if isBrowserDisplayableFileType(extension) {
w.Header().Set("Content-Disposition", "inline; filename=\""+filename+"\"")
} else {
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
}
http.ServeFile(w, r, filePath)
}
func httpGetRoot(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(assetIndexHTML)
}
func httpGetScriptJS(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("Content-Type", "text/javascript")
w.Write(assetScriptJS)
}
func httpGetStyleCSS(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("Content-Type", "text/css")
w.Write(assetStyleCSS)
}
func httpPostUpload(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if config.GetReadonlyMode() {
http.Error(w, "403 Forbidden", http.StatusForbidden)
return
}
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, fmt.Sprintf("Could not get multipart reader: %v", err), http.StatusBadRequest)
return
}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
http.Error(w, fmt.Sprintf("Error reading part: %v", err), http.StatusInternalServerError)
return
}
defer part.Close()
if part.FileName() == "" {
continue
}
safeFilename := filesystem.SanitizeFilename(part.FileName())
pathToFileInDataFolder := filepath.Join(config.GetPathDataFolder(), safeFilename)
pathToFileInUploadFolder := filepath.Join(config.GetPathUploadFolder(), safeFilename)
if _, err = os.Stat(pathToFileInDataFolder); err == nil {
http.Error(w, "File already exists", http.StatusConflict)
return
}
if _, err = os.Stat(pathToFileInUploadFolder); err == nil {
http.Error(w, "File already exists", http.StatusConflict)
return
}
uploadFile, err := os.Create(pathToFileInUploadFolder)
if err != nil {
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
defer uploadFile.Close()
bytesWritten, err := io.Copy(uploadFile, part)
if err != nil {
_ = os.Remove(pathToFileInUploadFolder)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
if err = os.Rename(pathToFileInUploadFolder, pathToFileInDataFolder); err != nil {
_ = os.Remove(pathToFileInDataFolder)
_ = os.Remove(pathToFileInUploadFolder)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
log.Printf("| Upload | %-21s | %-10s | %s\n",
getClientIP(r), filesystem.GetHumanReadableSize(bytesWritten), safeFilename)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}