Simple file upload and browsing
This commit is contained in:
240
lts.go
240
lts.go
@ -1,10 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
//go:embed tmpls/*
|
||||
@ -13,14 +23,232 @@ var tmpls embed.FS
|
||||
//go:embed etc/*
|
||||
var etc embed.FS
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(template.ParseFS(tmpls, "tmpls/*.html"))
|
||||
if err := tmpl.Execute(w, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
func createFile(storePath, name string) (*os.File, error) {
|
||||
if _, err := os.Stat(storePath); os.IsNotExist(err) {
|
||||
os.Mkdir(storePath, 0755)
|
||||
}
|
||||
|
||||
uuidBytes, err := NewV4()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uuid := uuidBytes.String()
|
||||
|
||||
dst, err := os.Create(filepath.Join(storePath, fmt.Sprintf("%s-%s", uuid, name)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
type AllowedUser struct {
|
||||
Name string
|
||||
Password string
|
||||
}
|
||||
|
||||
var allowedUsers []AllowedUser
|
||||
var allowedUsersMutex sync.Mutex
|
||||
|
||||
func basicAuth(handler http.HandlerFunc, realm string) http.HandlerFunc {
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
allowedUsersMutex.Lock()
|
||||
for _, allowedUser := range allowedUsers {
|
||||
if subtle.ConstantTimeCompare([]byte(user), []byte(allowedUser.Name)) == 1 {
|
||||
if subtle.ConstantTimeCompare([]byte(pass), []byte(allowedUser.Password)) == 1 {
|
||||
goto auth_ok
|
||||
} else {
|
||||
goto auth_fail
|
||||
}
|
||||
}
|
||||
}
|
||||
goto auth_fail
|
||||
|
||||
auth_ok:
|
||||
allowedUsersMutex.Unlock()
|
||||
handler(w, r)
|
||||
return
|
||||
|
||||
auth_fail:
|
||||
allowedUsersMutex.Unlock()
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var storePath string
|
||||
|
||||
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
|
||||
func byteCountSI(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
limit := int64(10737418240) // 10GiB
|
||||
r.ParseMultipartForm(limit)
|
||||
|
||||
file, handler, err := r.FormFile("myfile")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "Could not get the file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fmt.Fprintf(w, "Uploaded file: %s\n", handler.Filename)
|
||||
fmt.Fprintf(w, "File size: %s\n", byteCountSI(handler.Size))
|
||||
|
||||
dst, err := createFile(storePath, handler.Filename)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := dst.ReadFrom(file); err != nil {
|
||||
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
home_tmpl, _ := tmpls.ReadFile("tmpls/home.html")
|
||||
tmpl := template.Must(template.New("home").Parse(string(home_tmpl)))
|
||||
if err := tmpl.Execute(w, nil); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
type BrowseRecord struct {
|
||||
FileName string
|
||||
ReadableName string
|
||||
Perm string
|
||||
Modtime string
|
||||
}
|
||||
|
||||
func handleBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
browse_tmpl, _ := tmpls.ReadFile("tmpls/browse.html")
|
||||
tmpl := template.Must(template.New("browse").Parse(string(browse_tmpl)))
|
||||
|
||||
storeEntries, err := os.ReadDir(storePath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
var records []BrowseRecord
|
||||
|
||||
for _, e := range storeEntries {
|
||||
filename := e.Name()
|
||||
readable_name := filename[37:] // 32 hex + 4 `-` + 1 `-`
|
||||
info, _ := e.Info()
|
||||
|
||||
modtime := fmt.Sprintf("%d/%s/%d %02d:%02d:%02d",
|
||||
info.ModTime().Day(), info.ModTime().Month().String(), info.ModTime().Year(),
|
||||
info.ModTime().Hour(), info.ModTime().Minute(), info.ModTime().Second(),
|
||||
)
|
||||
|
||||
records = append(records, BrowseRecord{
|
||||
FileName: filename,
|
||||
ReadableName: readable_name,
|
||||
Perm: info.Mode().Perm().String(),
|
||||
Modtime: modtime,
|
||||
})
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, records); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func reloadAllowedUsers() {
|
||||
log.Println("loading ALLOWED_USERS.txt")
|
||||
allowedUsersTxt, err := ioutil.ReadFile("./ALLOWED_USERS.txt")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(allowedUsersTxt)))
|
||||
allowedUsersMutex.Lock()
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.Fields(line)
|
||||
allowedUsers = append(allowedUsers, AllowedUser{ Name: parts[0], Password: parts[1] })
|
||||
}
|
||||
allowedUsersMutex.Unlock()
|
||||
}
|
||||
|
||||
func watchAllowedUsers() chan bool {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Has(fsnotify.Chmod | fsnotify.Rename) {
|
||||
reloadAllowedUsers()
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println(err)
|
||||
case <-done:
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = watcher.Add("./ALLOWED_USERS.txt")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return done
|
||||
}
|
||||
|
||||
func main() {
|
||||
storePathValue, ok := os.LookupEnv("LTS_STORE_PATH")
|
||||
if !ok {
|
||||
log.Fatal("LTS_STORE_PATH not set")
|
||||
}
|
||||
storePath = storePathValue
|
||||
|
||||
reloadAllowedUsers()
|
||||
doneWatching := watchAllowedUsers()
|
||||
|
||||
http.HandleFunc("/", handleHome)
|
||||
http.HandleFunc("/browse", handleBrowse)
|
||||
http.HandleFunc("/upload", basicAuth(handleUpload, "Enter username and password"))
|
||||
http.Handle("/etc/", http.FileServerFS(etc))
|
||||
http.Handle("/store/", http.StripPrefix("/store/", http.FileServer(http.Dir(storePath))))
|
||||
|
||||
http.ListenAndServe(":9090", nil)
|
||||
|
||||
doneWatching<-true
|
||||
}
|
||||
|
Reference in New Issue
Block a user