feat: implement legacy config auth storage and core api
This commit is contained in:
@@ -1,14 +1,62 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chfs-ng/internal/api"
|
"flag"
|
||||||
"chfs-ng/internal/storage"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"chfs-ng/internal/api"
|
||||||
|
"chfs-ng/internal/auth"
|
||||||
|
"chfs-ng/internal/config"
|
||||||
|
"chfs-ng/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
s := &api.Server{Store: storage.Local{Root: "/data"}}
|
file := flag.String("file", "", "configuration file")
|
||||||
log.Println("chfs-ng development server listening on :80")
|
pathFlag := flag.String("path", "/data", "shared path")
|
||||||
log.Fatal(http.ListenAndServe(":80", s.Handler()))
|
port := flag.Int("port", 80, "listen port")
|
||||||
|
flag.Parse()
|
||||||
|
cfg := config.Default()
|
||||||
|
if *file != "" {
|
||||||
|
var err error
|
||||||
|
cfg, err = config.Load(*file)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *pathFlag != "/data" || *file == "" {
|
||||||
|
if *file == "" {
|
||||||
|
cfg.Path = []string{*pathFlag}
|
||||||
|
cfg.Port = *port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root := cfg.Path[0]
|
||||||
|
if e := os.MkdirAll(root, 0755); e != nil {
|
||||||
|
log.Fatal(e)
|
||||||
|
}
|
||||||
|
mgr, _ := auth.ParseRules(cfg.Rules)
|
||||||
|
srv := &api.Server{Store: storage.Local{Root: root}, Auth: mgr, Sessions: map[string]*auth.Account{}}
|
||||||
|
addr := ":" + itoa(cfg.Port)
|
||||||
|
if cfg.Port == 0 {
|
||||||
|
addr = ":" + itoa(*port)
|
||||||
|
}
|
||||||
|
log.Printf("chfs-ng listening on %s root=%s", addr, filepath.Clean(root))
|
||||||
|
if cfg.SSLCert != "" && cfg.SSLKey != "" {
|
||||||
|
log.Fatal(http.ListenAndServeTLS(addr, cfg.SSLCert, cfg.SSLKey, srv.Handler()))
|
||||||
|
}
|
||||||
|
log.Fatal(http.ListenAndServe(addr, srv.Handler()))
|
||||||
|
}
|
||||||
|
func itoa(n int) string {
|
||||||
|
const d = "0123456789"
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
b := make([]byte, 0, 6)
|
||||||
|
for n > 0 {
|
||||||
|
b = append([]byte{d[n%10]}, b...)
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chfs-ng/internal/storage"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chfs-ng/internal/auth"
|
||||||
|
"chfs-ng/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct{ Store storage.Storage }
|
type Server struct {
|
||||||
|
Store storage.Storage
|
||||||
|
Auth *auth.Manager
|
||||||
|
Sessions map[string]*auth.Account
|
||||||
|
}
|
||||||
|
type fileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Modified string `json:"modified"`
|
||||||
|
Dir bool `json:"dir"`
|
||||||
|
Mask string `json:"mask"`
|
||||||
|
GuestMask string `json:"guestMask"`
|
||||||
|
HasSubDir bool `json:"hasSubDir"`
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Handler() http.Handler {
|
func (s *Server) Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -15,12 +37,192 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.HandleFunc("/chfs/rmfiles", s.remove)
|
mux.HandleFunc("/chfs/rmfiles", s.remove)
|
||||||
mux.HandleFunc("/chfs/exist", s.exist)
|
mux.HandleFunc("/chfs/exist", s.exist)
|
||||||
mux.HandleFunc("/chfs/session", s.session)
|
mux.HandleFunc("/chfs/session", s.session)
|
||||||
|
mux.HandleFunc("/chfs/newtext", s.newtext)
|
||||||
|
mux.HandleFunc("/chfs/updatetext", s.updatetext)
|
||||||
|
mux.HandleFunc("/chfs/upload", s.upload)
|
||||||
|
mux.HandleFunc("/chfs/upload2", s.upload)
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
func notImpl(w http.ResponseWriter) { http.Error(w, "not implemented", http.StatusNotImplemented) }
|
func account(r *http.Request, s *Server) *auth.Account {
|
||||||
func (s *Server) files(w http.ResponseWriter, r *http.Request) { notImpl(w) }
|
if c, e := r.Cookie("chfs_token"); e == nil {
|
||||||
func (s *Server) newdir(w http.ResponseWriter, r *http.Request) { notImpl(w) }
|
if a := s.Sessions[c.Value]; a != nil {
|
||||||
func (s *Server) rename(w http.ResponseWriter, r *http.Request) { notImpl(w) }
|
return a
|
||||||
func (s *Server) remove(w http.ResponseWriter, r *http.Request) { notImpl(w) }
|
}
|
||||||
func (s *Server) exist(w http.ResponseWriter, r *http.Request) { notImpl(w) }
|
}
|
||||||
func (s *Server) session(w http.ResponseWriter, r *http.Request) { notImpl(w) }
|
return s.Auth.Guest
|
||||||
|
}
|
||||||
|
func writeJSON(w http.ResponseWriter, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
func qpath(r *http.Request) string {
|
||||||
|
return strings.Trim(strings.ReplaceAll(r.URL.Query().Get("filepath"), "\\", "/"), "/")
|
||||||
|
}
|
||||||
|
func permString(p auth.PermMask) string {
|
||||||
|
if p.HasDelete() {
|
||||||
|
return "d"
|
||||||
|
}
|
||||||
|
if p.HasWrite() {
|
||||||
|
return "w"
|
||||||
|
}
|
||||||
|
if p.HasRead() {
|
||||||
|
return "r"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (s *Server) files(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := account(r, s)
|
||||||
|
p := qpath(r)
|
||||||
|
m := a.MaskFor(p)
|
||||||
|
if !m.HasRead() {
|
||||||
|
http.Error(w, "forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, e := s.Store.List(p)
|
||||||
|
if e != nil {
|
||||||
|
http.Error(w, e.Error(), 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]fileInfo, 0, len(list))
|
||||||
|
for _, fi := range list {
|
||||||
|
fp := path.Join(p, fi.Name())
|
||||||
|
sub := false
|
||||||
|
if fi.IsDir() {
|
||||||
|
if xs, e := s.Store.List(fp); e == nil {
|
||||||
|
sub = len(xs) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, fileInfo{Name: fi.Name(), Path: fp, Size: fi.Size(), Modified: fi.ModTime().Format(time.RFC3339), Dir: fi.IsDir(), Mask: permString(a.MaskFor(fp)), HasSubDir: sub})
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"files": out, "currentMask": permString(m), "virtualRoot": false})
|
||||||
|
}
|
||||||
|
func (s *Server) newdir(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := account(r, s)
|
||||||
|
p := qpath(r)
|
||||||
|
if !a.MaskFor(p).HasWrite() {
|
||||||
|
http.Error(w, "forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := r.FormValue("name")
|
||||||
|
if name == "" {
|
||||||
|
name = r.FormValue("new")
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
http.Error(w, "name required", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e := s.Store.MkdirAll(path.Join(p, name)); e != nil {
|
||||||
|
http.Error(w, e.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
func (s *Server) rename(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := account(r, s)
|
||||||
|
old := strings.Trim(r.FormValue("old"), "/")
|
||||||
|
neu := strings.Trim(r.FormValue("new"), "/")
|
||||||
|
if !a.MaskFor(old).HasWrite() {
|
||||||
|
http.Error(w, "forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e := s.Store.Rename(old, neu); e != nil {
|
||||||
|
http.Error(w, e.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := account(r, s)
|
||||||
|
_ = r.ParseForm()
|
||||||
|
items := r.Form["path"]
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = r.Form["paths"]
|
||||||
|
}
|
||||||
|
for _, p := range items {
|
||||||
|
if !a.MaskFor(p).HasDelete() {
|
||||||
|
http.Error(w, "forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e := s.Store.Remove(p); e != nil {
|
||||||
|
http.Error(w, e.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
|
||||||
|
p := qpath(r)
|
||||||
|
_, e := s.Store.Stat(p)
|
||||||
|
writeJSON(w, map[string]any{"exist": e == nil})
|
||||||
|
}
|
||||||
|
func (s *Server) session(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
a := account(r, s)
|
||||||
|
writeJSON(w, map[string]any{"login": a != nil, "user": a.Username})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, pwd := r.FormValue("user"), r.FormValue("pwd")
|
||||||
|
a := s.Auth.Authenticate(user, pwd)
|
||||||
|
if a == nil {
|
||||||
|
http.Error(w, "invalid account", 401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := auth.NewToken()
|
||||||
|
s.Sessions[token] = a
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode})
|
||||||
|
writeJSON(w, map[string]any{"ok": true, "user": a.Username})
|
||||||
|
}
|
||||||
|
func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := account(r, s)
|
||||||
|
p := strings.Trim(r.FormValue("path"), "/")
|
||||||
|
if !a.MaskFor(p).HasWrite() {
|
||||||
|
http.Error(w, "forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, e := s.Store.Create(p)
|
||||||
|
if e != nil {
|
||||||
|
http.Error(w, e.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
io.WriteString(f, r.FormValue("text"))
|
||||||
|
writeJSON(w, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
func (s *Server) updatetext(w http.ResponseWriter, r *http.Request) { s.newtext(w, r) }
|
||||||
|
func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := account(r, s)
|
||||||
|
dir := strings.Trim(r.FormValue("path"), "/")
|
||||||
|
if !a.MaskFor(dir).HasWrite() {
|
||||||
|
http.Error(w, "forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e := r.ParseMultipartForm(64 << 20); e != nil {
|
||||||
|
http.Error(w, e.Error(), 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var saved []string
|
||||||
|
for _, hs := range r.MultipartForm.File {
|
||||||
|
for _, h := range hs {
|
||||||
|
if e := savePart(s.Store, dir, h); e != nil {
|
||||||
|
http.Error(w, e.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saved = append(saved, h.Filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"ok": true, "files": saved})
|
||||||
|
}
|
||||||
|
func savePart(st storage.Storage, dir string, h *multipart.FileHeader) error {
|
||||||
|
in, e := h.Open()
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
f, e := st.Create(path.Join(dir, h.Filename))
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, e = io.Copy(f, in)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type PermMask uint8
|
type PermMask uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -12,8 +19,116 @@ const (
|
|||||||
func (p PermMask) HasRead() bool { return p&Read != 0 }
|
func (p PermMask) HasRead() bool { return p&Read != 0 }
|
||||||
func (p PermMask) HasWrite() bool { return p&Write != 0 }
|
func (p PermMask) HasWrite() bool { return p&Write != 0 }
|
||||||
func (p PermMask) HasDelete() bool { return p&Delete != 0 }
|
func (p PermMask) HasDelete() bool { return p&Delete != 0 }
|
||||||
|
func ParseMask(s string) PermMask {
|
||||||
|
var p PermMask
|
||||||
|
for _, r := range strings.ToLower(s) {
|
||||||
|
switch r {
|
||||||
|
case 'r':
|
||||||
|
p |= Read
|
||||||
|
case 'w':
|
||||||
|
p |= Read | Write
|
||||||
|
case 'd':
|
||||||
|
p |= Read | Write | Delete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
type PathRule struct {
|
||||||
|
Dir string
|
||||||
|
Mask PermMask
|
||||||
|
}
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Username, Password string
|
Username, Password string
|
||||||
Mask PermMask
|
Mask PermMask
|
||||||
|
Rules []PathRule
|
||||||
|
}
|
||||||
|
type Manager struct {
|
||||||
|
Accounts map[string]*Account
|
||||||
|
Guest *Account
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRules(values []string) (*Manager, error) {
|
||||||
|
m := &Manager{Accounts: map[string]*Account{}}
|
||||||
|
for _, value := range values {
|
||||||
|
for _, item := range strings.Split(value, "|") {
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Split(item, ":")
|
||||||
|
if len(fields) < 3 || len(fields)%2 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a := &Account{Username: fields[0], Password: fields[1], Mask: ParseMask(fields[2])}
|
||||||
|
for i := 3; i+1 < len(fields); i += 2 {
|
||||||
|
a.Rules = append(a.Rules, PathRule{Dir: strings.Trim(fields[i], "/"), Mask: ParseMask(fields[i+1])})
|
||||||
|
}
|
||||||
|
if a.Username == "" {
|
||||||
|
m.Guest = a
|
||||||
|
} else {
|
||||||
|
m.Accounts[a.Username] = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Guest == nil {
|
||||||
|
m.Guest = &Account{Mask: Read | Write}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Authenticate(user, pwd string) *Account {
|
||||||
|
if a, ok := m.Accounts[user]; ok && a.Password == pwd {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
if user == "" && pwd == "" {
|
||||||
|
return m.Guest
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (a *Account) MaskFor(path string) PermMask {
|
||||||
|
path = strings.Trim(path, "/")
|
||||||
|
best := -1
|
||||||
|
mask := a.Mask
|
||||||
|
for _, r := range a.Rules {
|
||||||
|
d := strings.Trim(r.Dir, "/")
|
||||||
|
if matchPath(d, path) && len(d) > best {
|
||||||
|
best = len(d)
|
||||||
|
mask = r.Mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
func matchPath(pattern, path string) bool {
|
||||||
|
if pattern == "" {
|
||||||
|
return path == ""
|
||||||
|
}
|
||||||
|
pp := strings.Split(pattern, "/")
|
||||||
|
ps := strings.Split(path, "/")
|
||||||
|
if len(ps) < len(pp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, p := range pp {
|
||||||
|
if p == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if p == "?" {
|
||||||
|
if ps[i] == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p != ps[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToken() string {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, e := rand.Read(b); e != nil {
|
||||||
|
h := sha256.Sum256([]byte("fallback"))
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseRules(t *testing.T) {
|
||||||
|
m, err := ParseRules([]string{"::r|admin:secret:rw:private:d"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !m.Guest.Mask.HasRead() || m.Guest.Mask.HasWrite() {
|
||||||
|
t.Fatal("guest mask")
|
||||||
|
}
|
||||||
|
a := m.Accounts["admin"]
|
||||||
|
if a == nil || !a.Mask.HasWrite() || a.Mask.HasDelete() {
|
||||||
|
t.Fatal("admin mask")
|
||||||
|
}
|
||||||
|
if got := a.MaskFor("private/x"); !got.HasDelete() {
|
||||||
|
t.Fatalf("subrule=%v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Path []string
|
||||||
|
Port int
|
||||||
|
Allow string
|
||||||
|
Rules []string
|
||||||
|
Log string
|
||||||
|
FileRemove int
|
||||||
|
ImagePreview bool
|
||||||
|
FolderDownload string
|
||||||
|
SessionTimeout int
|
||||||
|
HTMLTitle string
|
||||||
|
HTMLNotice string
|
||||||
|
SSLCert string
|
||||||
|
SSLKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Config {
|
||||||
|
return Config{Path: []string{"."}, Port: 80, FileRemove: 1, ImagePreview: true, FolderDownload: "enable", SessionTimeout: 30}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
c := Default()
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
for s.Scan() {
|
||||||
|
line := strings.TrimSpace(s.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i := strings.IndexByte(line, '=')
|
||||||
|
if i < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, val := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
|
||||||
|
val = strings.Trim(val, "\\\"")
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "path":
|
||||||
|
c.Path = splitPath(val)
|
||||||
|
case "port":
|
||||||
|
if n, e := strconv.Atoi(val); e == nil && n > 0 && n < 65536 {
|
||||||
|
c.Port = n
|
||||||
|
}
|
||||||
|
case "allow":
|
||||||
|
c.Allow = val
|
||||||
|
case "rule":
|
||||||
|
if val != "" {
|
||||||
|
c.Rules = append(c.Rules, val)
|
||||||
|
}
|
||||||
|
case "log":
|
||||||
|
c.Log = val
|
||||||
|
case "file.remove":
|
||||||
|
if n, e := strconv.Atoi(val); e == nil {
|
||||||
|
c.FileRemove = n
|
||||||
|
}
|
||||||
|
case "image.preview":
|
||||||
|
c.ImagePreview = parseBool(val, c.ImagePreview)
|
||||||
|
case "folder.download":
|
||||||
|
c.FolderDownload = strings.ToLower(val)
|
||||||
|
case "folder.leaf.download":
|
||||||
|
if parseBool(val, false) {
|
||||||
|
c.FolderDownload = "leaf"
|
||||||
|
}
|
||||||
|
case "session.timeout":
|
||||||
|
if n, e := strconv.Atoi(val); e == nil && n > 0 {
|
||||||
|
c.SessionTimeout = n
|
||||||
|
}
|
||||||
|
case "html.title":
|
||||||
|
c.HTMLTitle = val
|
||||||
|
case "html.notice":
|
||||||
|
c.HTMLNotice = val
|
||||||
|
case "ssl.cert":
|
||||||
|
c.SSLCert = val
|
||||||
|
case "ssl.key":
|
||||||
|
c.SSLKey = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPath(v string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, p := range strings.Split(v, "|") {
|
||||||
|
p = strings.TrimSpace(strings.Trim(p, "\\\""))
|
||||||
|
if p != "" {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return []string{"."}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBool(v string, d bool) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||||
|
case "true", "1", "yes", "on":
|
||||||
|
return true
|
||||||
|
case "false", "0", "no", "off":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
if len(c.Path) == 0 {
|
||||||
|
return fmt.Errorf("path is empty")
|
||||||
|
}
|
||||||
|
if c.Port < 1 || c.Port > 65535 {
|
||||||
|
return fmt.Errorf("invalid port")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
@@ -17,24 +20,82 @@ type Storage interface {
|
|||||||
|
|
||||||
type Local struct{ Root string }
|
type Local struct{ Root string }
|
||||||
|
|
||||||
func (s Local) real(path string) string { return filepath.Join(s.Root, filepath.Clean("/"+path)) }
|
func (s Local) real(path string) (string, error) {
|
||||||
func (s Local) Stat(path string) (os.FileInfo, error) { return os.Stat(s.real(path)) }
|
root, err := filepath.Abs(s.Root)
|
||||||
func (s Local) Open(path string) (*os.File, error) { return os.Open(s.real(path)) }
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
p := strings.TrimPrefix(filepath.Clean("/"+path), string(filepath.Separator))
|
||||||
|
full := filepath.Join(root, p)
|
||||||
|
rel, err := filepath.Rel(root, full)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||||
|
return "", errors.New("path escapes storage root")
|
||||||
|
}
|
||||||
|
return full, nil
|
||||||
|
}
|
||||||
|
func (s Local) Stat(path string) (os.FileInfo, error) {
|
||||||
|
p, e := s.real(path)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return os.Stat(p)
|
||||||
|
}
|
||||||
|
func (s Local) Open(path string) (*os.File, error) {
|
||||||
|
p, e := s.real(path)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return os.Open(p)
|
||||||
|
}
|
||||||
func (s Local) Create(path string) (*os.File, error) {
|
func (s Local) Create(path string) (*os.File, error) {
|
||||||
p := s.real(path)
|
p, e := s.real(path)
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
if e != nil {
|
||||||
return nil, err
|
return nil, e
|
||||||
|
}
|
||||||
|
if e = os.MkdirAll(filepath.Dir(p), 0755); e != nil {
|
||||||
|
return nil, e
|
||||||
}
|
}
|
||||||
return os.Create(p)
|
return os.Create(p)
|
||||||
}
|
}
|
||||||
func (s Local) Remove(path string) error { return os.RemoveAll(s.real(path)) }
|
func (s Local) Remove(path string) error {
|
||||||
func (s Local) Rename(a, b string) error { return os.Rename(s.real(a), s.real(b)) }
|
p, e := s.real(path)
|
||||||
func (s Local) MkdirAll(path string) error { return os.MkdirAll(s.real(path), 0755) }
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return os.RemoveAll(p)
|
||||||
|
}
|
||||||
|
func (s Local) Rename(a, b string) error {
|
||||||
|
pa, e := s.real(a)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
pb, e := s.real(b)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return os.Rename(pa, pb)
|
||||||
|
}
|
||||||
|
func (s Local) MkdirAll(path string) error {
|
||||||
|
p, e := s.real(path)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return os.MkdirAll(p, 0755)
|
||||||
|
}
|
||||||
func (s Local) List(path string) ([]os.FileInfo, error) {
|
func (s Local) List(path string) ([]os.FileInfo, error) {
|
||||||
f, err := os.Open(s.real(path))
|
p, e := s.real(path)
|
||||||
if err != nil {
|
if e != nil {
|
||||||
return nil, err
|
return nil, e
|
||||||
|
}
|
||||||
|
f, e := os.Open(p)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
return f.Readdir(-1)
|
return f.Readdir(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ fs.FS
|
||||||
|
|||||||
Reference in New Issue
Block a user