2023-10-13 09:43:27 +00:00
|
|
|
package fstore
|
|
|
|
|
|
|
|
import (
|
|
|
|
"embed"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-10-16 08:50:19 +00:00
|
|
|
|
|
|
|
"git.crumpington.com/public/jldb/fstore/pages"
|
2023-10-13 09:43:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
//go:embed static/*
|
|
|
|
var staticFS embed.FS
|
|
|
|
|
|
|
|
type browser struct {
|
|
|
|
store *Store
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Store) ServeBrowser(listenAddr string) error {
|
|
|
|
b := &browser{s}
|
|
|
|
http.HandleFunc("/", b.handle)
|
|
|
|
http.Handle("/static/", http.FileServer(http.FS(staticFS)))
|
|
|
|
return http.ListenAndServe(listenAddr, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *browser) handle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
b.handleGet(w, r)
|
|
|
|
case http.MethodPost:
|
|
|
|
b.handlePOST(w, r)
|
|
|
|
default:
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *browser) handleGet(w http.ResponseWriter, r *http.Request) {
|
|
|
|
path := cleanPath(r.URL.Path)
|
|
|
|
if err := validatePath(path); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
info, err := b.store.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
pages.Page{Path: path}.Render(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !info.IsDir() {
|
|
|
|
b.store.Serve(w, r, path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dirs, files, err := b.store.List(path)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
pages.Page{
|
|
|
|
Path: path,
|
|
|
|
Dirs: dirs,
|
|
|
|
Files: files,
|
|
|
|
}.Render(w)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle actions:
|
|
|
|
// - upload (multipart),
|
|
|
|
// - delete
|
|
|
|
func (b *browser) handlePOST(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if err := r.ParseMultipartForm(1024 * 1024); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch r.Form.Get("Action") {
|
|
|
|
case "Upload":
|
|
|
|
b.handlePOSTUpload(w, r)
|
|
|
|
case "Delete":
|
|
|
|
b.handlePOSTDelete(w, r)
|
|
|
|
default:
|
|
|
|
http.Error(w, "unknown action", http.StatusBadRequest)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *browser) handlePOSTUpload(w http.ResponseWriter, r *http.Request) {
|
|
|
|
file, handler, err := r.FormFile("File")
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
relativePath := handler.Filename
|
|
|
|
if p := r.Form.Get("Path"); p != "" {
|
|
|
|
relativePath = p
|
|
|
|
}
|
|
|
|
fullPath := filepath.Join(r.URL.Path, relativePath)
|
|
|
|
|
|
|
|
tmpPath := b.store.GetTempFilePath()
|
|
|
|
defer os.RemoveAll(tmpPath)
|
|
|
|
|
|
|
|
tmpFile, err := os.Create(tmpPath)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer tmpFile.Close()
|
|
|
|
|
|
|
|
if _, err := io.Copy(tmpFile, file); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := b.store.Store(tmpPath, fullPath); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(w, r, filepath.Dir(fullPath), http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *browser) handlePOSTDelete(w http.ResponseWriter, r *http.Request) {
|
|
|
|
path := r.Form.Get("Path")
|
|
|
|
if err := b.store.Remove(path); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(w, r, filepath.Dir(path), http.StatusSeeOther)
|
|
|
|
}
|