diff --git a/_remove/filebrowser.go b/_remove/filebrowser.go new file mode 100644 index 00000000..58540d8d --- /dev/null +++ b/_remove/filebrowser.go @@ -0,0 +1,185 @@ +package lib + +/* +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "hash" + "io" + "log" + "mime" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/filebrowser/filebrowser/files" + "github.com/filebrowser/filebrowser/rules" + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/users" + "github.com/mholt/caddy" + "github.com/spf13/afero" +) + + + +// FileBrowser represents a File Browser instance which must +// be created through NewFileBrowser. +type FileBrowser struct { + settings *settings.Settings + mux sync.RWMutex +} + +// NewFileBrowser creates a new File Browser instance from a +// storage backend. If that backend doesn't contain settings +// on it (returns ErrNotExist), then we generate a new key +// and base settings. +func NewFileBrowser(backend StorageBackend) (*FileBrowser, error) { + set, err := backend.GetSettings() + + if err == ErrNotExist { + var key []byte + key, err = generateRandomBytes(64) + + if err != nil { + return nil, err + } + + set = &settings.Settings{Key: key} + err = backend.SaveSettings(set) + } + + if err != nil { + return nil, err + } + + return &FileBrowser{ + settings: set, + storage: backend, + }, nil +} + +// RLockSettings locks the settings for reading. +func (f *FileBrowser) RLockSettings() { + f.mux.RLock() +} + +// RUnlockSettings unlocks the settings for reading. +func (f *FileBrowser) RUnlockSettings() { + f.mux.RUnlock() +} + +// CheckRules matches the rules against user rules and global rules. +func (f *FileBrowser) CheckRules(path string, user *users.User) bool { + f.RLockSettings() + val := rules.Check(path, user, f.settings) + f.RUnlockSettings() + return val +} + +// RunHook runs the hooks for the before and after event. +func (f *FileBrowser) RunHook(fn func() error, evt, path, dst string, user *users.User) error { + path = user.FullPath(path) + dst = user.FullPath(dst) + + if val, ok := f.settings.Commands["before_"+evt]; ok { + for _, command := range val { + err := f.exec(command, "before_"+evt, path, dst, user) + if err != nil { + return err + } + } + } + + err := fn() + if err != nil { + return err + } + + if val, ok := f.settings.Commands["after_"+evt]; ok { + for _, command := range val { + err := f.exec(command, "after_"+evt, path, dst, user) + if err != nil { + return err + } + } + } + + return nil +} + +// ParseCommand parses the command taking in account if the current +// instance uses a shell to run the commands or just calls the binary +// directyly. +func (f *FileBrowser) ParseCommand(raw string) ([]string, error) { + f.RLockSettings() + defer f.RUnlockSettings() + + command := []string{} + + if len(f.settings.Shell) == 0 { + cmd, args, err := caddy.SplitCommandAndArgs(raw) + if err != nil { + return nil, err + } + + _, err = exec.LookPath(cmd) + if err != nil { + return nil, err + } + + command = append(command, cmd) + command = append(command, args...) + } else { + command = append(f.settings.Shell, raw) + } + + return command, nil +} + + + + + + + +func (f *FileBrowser) exec(raw, evt, path, dst string, user *users.User) error { + blocking := true + + if strings.HasSuffix(raw, "&") { + blocking = false + raw = strings.TrimSpace(strings.TrimSuffix(raw, "&")) + } + + command, err := f.ParseCommand(raw) + if err != nil { + return err + } + + cmd := exec.Command(command[0], command[1:]...) + cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) + cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) + cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt)) + cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) + cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst)) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if !blocking { + log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " ")) + return cmd.Start() + } + + log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " ")) + return cmd.Run() +} +*/ diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 00000000..be6e768b --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,15 @@ +package auth + +import ( + "net/http" + + "github.com/filebrowser/filebrowser/users" +) + +// Auther is the authentication interface. +type Auther interface { + // Auth is called to authenticate a request. + Auth(*http.Request) (*users.User, error) + // SetStorage attaches the Storage instance. + SetStorage(*users.Storage) +} diff --git a/auth/json.go b/auth/json.go index da91798a..6df0e80e 100644 --- a/auth/json.go +++ b/auth/json.go @@ -4,13 +4,15 @@ import ( "encoding/json" "net/http" "net/url" + "os" "strings" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/users" ) // MethodJSONAuth is used to identify json auth. -const MethodJSONAuth lib.AuthMethod = "json" +const MethodJSONAuth settings.AuthMethod = "json" type jsonCred struct { Password string `json:"password"` @@ -21,20 +23,20 @@ type jsonCred struct { // JSONAuth is a json implementaion of an Auther. type JSONAuth struct { ReCaptcha *ReCaptcha - instance *lib.FileBrowser + storage *users.Storage } // Auth authenticates the user via a json in content body. -func (a *JSONAuth) Auth(r *http.Request) (*lib.User, error) { +func (a *JSONAuth) Auth(r *http.Request) (*users.User, error) { var cred jsonCred if r.Body == nil { - return nil, lib.ErrNoPermission + return nil, os.ErrPermission } err := json.NewDecoder(r.Body).Decode(&cred) if err != nil { - return nil, lib.ErrNoPermission + return nil, os.ErrPermission } // If ReCaptcha is enabled, check the code. @@ -46,21 +48,21 @@ func (a *JSONAuth) Auth(r *http.Request) (*lib.User, error) { } if !ok { - return nil, lib.ErrNoPermission + return nil, os.ErrPermission } } - u, err := a.instance.GetUser(cred.Username) - if err != nil || !lib.CheckPwd(cred.Password, u.Password) { - return nil, lib.ErrNoPermission + u, err := a.storage.Get(cred.Username) + if err != nil || !users.CheckPwd(cred.Password, u.Password) { + return nil, os.ErrPermission } return u, nil } -// SetInstance attaches the instance to the auther. -func (a *JSONAuth) SetInstance(i *lib.FileBrowser) { - a.instance = i +// SetStorage attaches the storage to the auther. +func (a *JSONAuth) SetStorage(s *users.Storage) { + a.storage = s } const reCaptchaAPI = "/recaptcha/api/siteverify" diff --git a/auth/none.go b/auth/none.go index ffe4aeb8..28f8754c 100644 --- a/auth/none.go +++ b/auth/none.go @@ -3,23 +3,24 @@ package auth import ( "net/http" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/users" ) // MethodNoAuth is used to identify no auth. -const MethodNoAuth lib.AuthMethod = "noauth" +const MethodNoAuth settings.AuthMethod = "noauth" // NoAuth is no auth implementation of auther. type NoAuth struct { - instance *lib.FileBrowser + storage *users.Storage } // Auth uses authenticates user 1. -func (a *NoAuth) Auth(r *http.Request) (*lib.User, error) { - return a.instance.GetUser(1) +func (a *NoAuth) Auth(r *http.Request) (*users.User, error) { + return a.storage.Get(1) } -// SetInstance attaches the instance to the auther. -func (a *NoAuth) SetInstance(i *lib.FileBrowser) { - a.instance = i +// SetStorage attaches the storage to the auther. +func (a *NoAuth) SetStorage(s *users.Storage) { + a.storage = s } diff --git a/auth/proxy.go b/auth/proxy.go index 9d6a5876..58f96863 100644 --- a/auth/proxy.go +++ b/auth/proxy.go @@ -2,31 +2,34 @@ package auth import ( "net/http" + "os" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/users" + "github.com/filebrowser/filebrowser/errors" ) // MethodProxyAuth is used to identify no auth. -const MethodProxyAuth lib.AuthMethod = "proxy" +const MethodProxyAuth settings.AuthMethod = "proxy" // ProxyAuth is a proxy implementation of an auther. type ProxyAuth struct { - Header string - instance *lib.FileBrowser + Header string + storage *users.Storage } // Auth authenticates the user via an HTTP header. -func (a *ProxyAuth) Auth(r *http.Request) (*lib.User, error) { +func (a *ProxyAuth) Auth(r *http.Request) (*users.User, error) { username := r.Header.Get(a.Header) - user, err := a.instance.GetUser(username) - if err == lib.ErrNotExist { - return nil, lib.ErrNoPermission + user, err := a.storage.Get(username) + if err == errors.ErrNotExist { + return nil, os.ErrPermission } return user, err } -// SetInstance attaches the instance to the auther. -func (a *ProxyAuth) SetInstance(i *lib.FileBrowser) { - a.instance = i +// SetStorage attaches the storage to the auther. +func (a *ProxyAuth) SetStorage(s *users.Storage) { + a.storage = s } diff --git a/auth/storage.go b/auth/storage.go new file mode 100644 index 00000000..496a1eef --- /dev/null +++ b/auth/storage.go @@ -0,0 +1,39 @@ +package auth + +import ( + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/users" +) + +// StorageBackend is a storage backend for auth storage. +type StorageBackend interface { + Get(settings.AuthMethod) (Auther, error) + Save(Auther) error +} + +// Storage is a auth storage. +type Storage struct { + back StorageBackend + users *users.Storage +} + +// NewStorage creates a auth storage from a backend. +func NewStorage(back StorageBackend, users *users.Storage) *Storage { + return &Storage{back: back, users: users} +} + +// Get wraps a StorageBackend.Get and calls SetStorage on the auther. +func (s *Storage) Get(t settings.AuthMethod) (Auther, error) { + auther, err := s.back.Get(t) + if err != nil { + return nil, err + } + + auther.SetStorage(s.users) + return auther, nil +} + +// Save wraps a StorageBackend.Save. +func (s *Storage) Save(a Auther) error { + return s.back.Save(a) +} diff --git a/cmd/config.go b/cmd/config.go index 6a7e15b0..bb40a026 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -9,7 +9,7 @@ import ( "text/tabwriter" "github.com/filebrowser/filebrowser/auth" - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/cmd/config_init.go b/cmd/config_init.go index 23b95746..df829914 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/asdine/storm" - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/cmd/config_set.go b/cmd/config_set.go index 073f7993..db622993 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -3,7 +3,7 @@ package cmd import ( "strings" - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/cmd/root.go b/cmd/root.go index 2fb42ff2..4338eefe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,7 +12,7 @@ import ( "github.com/asdine/storm" "github.com/filebrowser/filebrowser/auth" - "github.com/filebrowser/filebrowser/lib" + fbhttp "github.com/filebrowser/filebrowser/http" "github.com/spf13/cobra" diff --git a/cmd/users.go b/cmd/users.go index 018a162a..3a86d5e6 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -6,7 +6,7 @@ import ( "os" "text/tabwriter" - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/cmd/users_find.go b/cmd/users_find.go index afd71b78..376d8e03 100644 --- a/cmd/users_find.go +++ b/cmd/users_find.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/cmd/users_new.go b/cmd/users_new.go index e94bc036..e6084bb7 100644 --- a/cmd/users_new.go +++ b/cmd/users_new.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/cmd/users_update.go b/cmd/users_update.go index 9d6193bf..2a988d65 100644 --- a/cmd/users_update.go +++ b/cmd/users_update.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/cmd/utils.go b/cmd/utils.go index 10a3ef33..213d68c0 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -6,7 +6,7 @@ import ( "github.com/asdine/storm" "github.com/filebrowser/filebrowser/bolt" - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/cmd/version.go b/cmd/version.go index 1ba4f81c..fce074ec 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" - "github.com/filebrowser/filebrowser/lib" + "github.com/spf13/cobra" ) diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 00000000..ef178539 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,15 @@ +package errors + +import "errors" + +var ( + ErrEmptyKey = errors.New("empty key") + ErrExist = errors.New("the resource already exists") + ErrNotExist = errors.New("the resource does not exist") + ErrEmptyPassword = errors.New("password is empty") + ErrEmptyUsername = errors.New("username is empty") + ErrScopeIsRelative = errors.New("scope is a relative path") + ErrInvalidDataType = errors.New("invalid data type") + ErrIsDirectory = errors.New("file is directory") + ErrInvalidOption = errors.New("invalid option") +) diff --git a/files/file.go b/files/file.go new file mode 100644 index 00000000..2c932c87 --- /dev/null +++ b/files/file.go @@ -0,0 +1,241 @@ +package files + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "hash" + "io" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/filebrowser/filebrowser/errors" + "github.com/filebrowser/filebrowser/rules" + "github.com/spf13/afero" +) + +// FileInfo describes a file. +type FileInfo struct { + *Listing + Fs afero.Fs `json:"-"` + Path string `json:"path"` + Name string `json:"name"` + Size int64 `json:"size"` + Extension string `json:"extension"` + ModTime time.Time `json:"modified"` + Mode os.FileMode `json:"mode"` + IsDir bool `json:"isDir"` + Type string `json:"type"` + Subtitles []string `json:"subtitles,omitempty"` + Content string `json:"content,omitempty"` + Checksums map[string]string `json:"checksums,omitempty"` +} + +// NewFileInfo creates a File object from a path and a given user. This File +// object will be automatically filled depending on if it is a directory +// or a file. If it's a video file, it will also detect any subtitles. +func NewFileInfo(fs afero.Fs, path string, modify bool, checker rules.Checker) (*FileInfo, error) { + if !checker.Check(path) { + return nil, os.ErrPermission + } + + info, err := fs.Stat(path) + if err != nil { + return nil, err + } + + file := &FileInfo{ + Fs: fs, + Path: path, + Name: info.Name(), + ModTime: info.ModTime(), + Mode: info.Mode(), + IsDir: info.IsDir(), + Size: info.Size(), + Extension: filepath.Ext(info.Name()), + } + + if file.IsDir { + return file, file.readListing(checker) + } + + err = file.detectType(modify) + if err != nil { + return nil, err + } + + return file, err +} + +// Checksum checksums a given File for a given User, using a specific +// algorithm. The checksums data is saved on File object. +func (i *FileInfo) Checksum(algo string) error { + if i.IsDir { + return errors.ErrIsDirectory + } + + if i.Checksums == nil { + i.Checksums = map[string]string{} + } + + reader, err := i.Fs.Open(i.Path) + if err != nil { + return err + } + defer reader.Close() + + var h hash.Hash + + switch algo { + case "md5": + h = md5.New() + case "sha1": + h = sha1.New() + case "sha256": + h = sha256.New() + case "sha512": + h = sha512.New() + default: + return errors.ErrInvalidOption + } + + _, err = io.Copy(h, reader) + if err != nil { + return err + } + + i.Checksums[algo] = hex.EncodeToString(h.Sum(nil)) + return nil +} + +func (i *FileInfo) detectType(modify bool) error { + reader, err := i.Fs.Open(i.Path) + if err != nil { + return err + } + defer reader.Close() + + buffer := make([]byte, 512) + n, err := reader.Read(buffer) + if err != nil && err != io.EOF { + return err + } + + mimetype := mime.TypeByExtension(i.Extension) + if mimetype == "" { + mimetype = http.DetectContentType(buffer[:n]) + } + + switch { + case strings.HasPrefix(mimetype, "video"): + i.Type = "video" + i.detectSubtitles() + return nil + case strings.HasPrefix(mimetype, "audio"): + i.Type = "audio" + return nil + case strings.HasPrefix(mimetype, "image"): + i.Type = "image" + return nil + case isBinary(string(buffer[:n])) || i.Size > 10*1024*1024: // 10 MB + i.Type = "blob" + return nil + default: + i.Type = "text" + afs := &afero.Afero{Fs: i.Fs} + content, err := afs.ReadFile(i.Path) + if err != nil { + return err + } + + if !modify { + i.Type = "textImmutable" + } + + i.Content = string(content) + } + + return nil +} + +func (i *FileInfo) detectSubtitles() { + if i.Type != "video" { + return + } + + i.Subtitles = []string{} + ext := filepath.Ext(i.Path) + + // TODO: detect multiple languages. Base.Lang.vtt + + path := strings.TrimSuffix(i.Path, ext) + ".vtt" + if _, err := i.Fs.Stat(path); err == nil { + i.Subtitles = append(i.Subtitles, path) + } +} + +func (i *FileInfo) readListing(checker rules.Checker) error { + afs := &afero.Afero{Fs: i.Fs} + dir, err := afs.ReadDir(i.Path) + if err != nil { + return err + } + + listing := &Listing{ + Items: []*FileInfo{}, + NumDirs: 0, + NumFiles: 0, + } + + for _, f := range dir { + name := f.Name() + path := path.Join(i.Path, name) + + if !checker.Check(path) { + continue + } + + if strings.HasPrefix(f.Mode().String(), "L") { + // It's a symbolic link. We try to follow it. If it doesn't work, + // we stay with the link information instead if the target's. + info, err := os.Stat(name) + if err == nil { + f = info + } + } + + file := &FileInfo{ + Fs: i.Fs, + Name: name, + Size: f.Size(), + ModTime: f.ModTime(), + Mode: f.Mode(), + IsDir: f.IsDir(), + Extension: filepath.Ext(name), + Path: path, + } + + if file.IsDir { + listing.NumDirs++ + } else { + listing.NumFiles++ + + err := file.detectType(true) + if err != nil { + return err + } + } + + listing.Items = append(listing.Items, file) + } + + i.Listing = listing + return nil +} diff --git a/lib/files.go b/files/listing.go similarity index 73% rename from lib/files.go rename to files/listing.go index 19012087..116b882b 100644 --- a/lib/files.go +++ b/files/listing.go @@ -1,35 +1,17 @@ -package lib +package files import ( - "os" "sort" - "time" "github.com/maruel/natural" ) -// File describes a file. -type File struct { - *Listing - Path string `json:"path"` - Name string `json:"name"` - Size int64 `json:"size"` - Extension string `json:"extension"` - ModTime time.Time `json:"modified"` - Mode os.FileMode `json:"mode"` - IsDir bool `json:"isDir"` - Type string `json:"type"` - Subtitles []string `json:"subtitles,omitempty"` - Content string `json:"content,omitempty"` - Checksums map[string]string `json:"checksums,omitempty"` -} - // Listing is a collection of files. type Listing struct { - Items []*File `json:"items"` - NumDirs int `json:"numDirs"` - NumFiles int `json:"numFiles"` - Sorting Sorting `json:"sorting"` + Items []*FileInfo `json:"items"` + NumDirs int `json:"numDirs"` + NumFiles int `json:"numFiles"` + Sorting Sorting `json:"sorting"` } // ApplySort applies the sort order using .Order and .Sort diff --git a/files/sorting.go b/files/sorting.go new file mode 100644 index 00000000..ecdc3df6 --- /dev/null +++ b/files/sorting.go @@ -0,0 +1,7 @@ +package files + +// Sorting contains a sorting order. +type Sorting struct { + By string `json:"by"` + Asc bool `json:"asc"` +} diff --git a/files/utils.go b/files/utils.go new file mode 100644 index 00000000..c68e9fbc --- /dev/null +++ b/files/utils.go @@ -0,0 +1,12 @@ +package files + +func isBinary(content string) bool { + for _, b := range content { + // 65533 is the unknown char + // 8 and below are control chars (e.g. backspace, null, eof, etc) + if b <= 8 || b == 65533 { + return true + } + } + return false +} diff --git a/http/auth.go b/http/auth.go index a866ccf9..d3792ca5 100644 --- a/http/auth.go +++ b/http/auth.go @@ -4,18 +4,19 @@ import ( "context" "encoding/json" "net/http" + "os" "strings" "time" "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go/request" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/errors" + "github.com/filebrowser/filebrowser/users" ) - func (e *env) loginHandler(w http.ResponseWriter, r *http.Request) { - user, err := e.Auther.Auth(r) - if err == lib.ErrNoPermission { + user, err := e.auther.Auth(r) + if err == os.ErrPermission { httpErr(w, r, http.StatusForbidden, nil) } else if err != nil { httpErr(w, r, http.StatusInternalServerError, err) @@ -30,24 +31,24 @@ type signupBody struct { } func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) { - e.RLockSettings() - defer e.RUnlockSettings() + settings, err := e.Settings.Get() + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } - settings := e.GetSettings() - if !settings.Signup { httpErr(w, r, http.StatusForbidden, nil) return } - if r.Body == nil { httpErr(w, r, http.StatusBadRequest, nil) return } info := &signupBody{} - err := json.NewDecoder(r.Body).Decode(info) + err = json.NewDecoder(r.Body).Decode(info) if err != nil { httpErr(w, r, http.StatusBadRequest, nil) return @@ -58,21 +59,21 @@ func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) { return } - user := &lib.User{ + user := &users.User{ Username: info.Username, } - e.ApplyDefaults(user) + settings.Defaults.Apply(user) - pwd, err := lib.HashPwd(info.Password) + pwd, err := users.HashPwd(info.Password) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) return } user.Password = pwd - err = e.SaveUser(user) - if err == lib.ErrExist { + err = e.Users.Save(user) + if err == errors.ErrExist { httpErr(w, r, http.StatusConflict, nil) return } else if err != nil { @@ -86,8 +87,8 @@ func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) { type userInfo struct { ID uint `json:"id"` Locale string `json:"locale"` - ViewMode lib.ViewMode `json:"viewMode"` - Perm lib.Permissions `json:"perm"` + ViewMode users.ViewMode `json:"viewMode"` + Perm users.Permissions `json:"perm"` Commands []string `json:"commands"` LockPassword bool `json:"lockPassword"` } @@ -119,7 +120,12 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) { func (e *env) auth(next http.HandlerFunc) http.HandlerFunc { keyFunc := func(token *jwt.Token) (interface{}, error) { - return e.GetSettings().Key, nil + settings, err := e.Settings.Get() + if err != nil { + return nil, err + } + + return settings.Key, nil } nextWithUser := func(w http.ResponseWriter, r *http.Request, id uint) { @@ -154,7 +160,7 @@ func (e *env) renew(w http.ResponseWriter, r *http.Request) { e.printToken(w, r, user) } -func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *lib.User) { +func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *users.User) { claims := &authToken{ User: userInfo{ ID: user.ID, @@ -171,7 +177,14 @@ func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *lib.User) } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString(e.GetSettings().Key) + + settings, err := e.Settings.Get() + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } + + signed, err := token.SignedString(settings.Key) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) diff --git a/http/http.go b/http/http.go index bed3014e..f1936ffb 100644 --- a/http/http.go +++ b/http/http.go @@ -7,7 +7,10 @@ import ( "strconv" "time" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/auth" + "github.com/filebrowser/filebrowser/storage" + "github.com/filebrowser/filebrowser/errors" + "github.com/filebrowser/filebrowser/users" "github.com/gorilla/mux" "github.com/gorilla/websocket" ) @@ -24,22 +27,19 @@ type modifyRequest struct { } type env struct { - *lib.FileBrowser - Auther lib.Auther + *storage.Storage + auther auth.Auther } // NewHandler builds an HTTP handler on the top of a File Browser instance. -func NewHandler(fb *lib.FileBrowser) (http.Handler, error) { - authMethod := fb.GetSettings().AuthMethod +func NewHandler(storage *storage.Storage) (http.Handler, error) { + /* authMethod := fb.GetSettings().AuthMethod auther, err := fb.GetAuther(authMethod) if err != nil { return nil, err - } + } */ - e := &env{ - FileBrowser: fb, - Auther: auther, - } + e := &env{} r := mux.NewRouter() @@ -109,10 +109,10 @@ func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) { } } -func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) { +func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) { id := r.Context().Value(keyUserID).(uint) - user, err := e.GetUser(id) - if err == lib.ErrNotExist { + user, err := e.Users.Get(id) + if err == errors.ErrNotExist { httpErr(w, r, http.StatusForbidden, nil) return nil, false } @@ -125,7 +125,7 @@ func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) return user, true } -func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) { +func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) { user, ok := e.getUser(w, r) if !ok { return nil, false diff --git a/http/raw.go b/http/raw.go index ac128f84..f96b1cf2 100644 --- a/http/raw.go +++ b/http/raw.go @@ -7,14 +7,15 @@ import ( "path/filepath" "strings" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/files" + "github.com/filebrowser/filebrowser/users" "github.com/hacdias/fileutils" "github.com/mholt/archiver" ) const apiRawPrefix = "/api/raw" -func parseQueryFiles(r *http.Request, f *lib.File, u *lib.User) ([]string, error) { +func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) { files := []string{} names := strings.Split(r.URL.Query().Get("files"), ",") @@ -67,6 +68,8 @@ func (e *env) rawHandler(w http.ResponseWriter, r *http.Request) { return } + files.NewFileInfo(user.Fs, path, user.Perm.Modify) + file, err := e.NewFile(path, user) if err != nil { httpErr(w, r, httpFsErr(err), err) @@ -141,7 +144,7 @@ func (e *env) rawHandler(w http.ResponseWriter, r *http.Request) { } } -func fileHandler(w http.ResponseWriter, r *http.Request, file *lib.File, user *lib.User) { +func fileHandler(w http.ResponseWriter, r *http.Request, file *files.File, user *users.User) { fd, err := user.Fs.Open(file.Path) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) diff --git a/http/resource.go b/http/resource.go index a9f146ab..a34f6cf4 100644 --- a/http/resource.go +++ b/http/resource.go @@ -10,7 +10,8 @@ import ( "strings" "github.com/filebrowser/filebrowser/fileutils" - "github.com/filebrowser/filebrowser/lib" + + "github.com/filebrowser/filebrowser/users" ) const apiResourcePrefix = "/api/resources" @@ -30,7 +31,7 @@ func httpFsErr(err error) int { } } -func (e *env) getResourceData(w http.ResponseWriter, r *http.Request, prefix string) (string, *lib.User, bool) { +func (e *env) getResourceData(w http.ResponseWriter, r *http.Request, prefix string) (string, *users.User, bool) { user, ok := e.getUser(w, r) if !ok { return "", nil, ok @@ -65,7 +66,7 @@ func (e *env) resourceGetHandler(w http.ResponseWriter, r *http.Request) { } if checksum := r.URL.Query().Get("checksum"); checksum != "" { - err = e.Checksum(file,user, checksum) + err = e.Checksum(file, user, checksum) if err == lib.ErrInvalidOption { httpErr(w, r, http.StatusBadRequest, nil) return diff --git a/http/settings.go b/http/settings.go index 063b01bb..05bed412 100644 --- a/http/settings.go +++ b/http/settings.go @@ -4,17 +4,17 @@ import ( "encoding/json" "net/http" - "github.com/filebrowser/filebrowser/lib" - "github.com/jinzhu/copier" + "github.com/filebrowser/filebrowser/rules" + "github.com/filebrowser/filebrowser/settings" ) type settingsData struct { - Signup bool `json:"signup"` - Defaults lib.UserDefaults `json:"defaults"` - Rules []lib.Rule `json:"rules"` - Branding lib.Branding `json:"branding"` - Shell []string `json:"shell"` - Commands map[string][]string `json:"commands"` + Signup bool `json:"signup"` + Defaults settings.UserDefaults `json:"defaults"` + Rules []rules.Rule `json:"rules"` + Branding settings.Branding `json:"branding"` + Shell []string `json:"shell"` + Commands map[string][]string `json:"commands"` } func (e *env) settingsGetHandler(w http.ResponseWriter, r *http.Request) { @@ -23,16 +23,19 @@ func (e *env) settingsGetHandler(w http.ResponseWriter, r *http.Request) { return } - e.RLockSettings() - defer e.RUnlockSettings() + settings, err := e.Settings.Get() + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } data := &settingsData{ - Signup: e.GetSettings().Signup, - Defaults: e.GetSettings().Defaults, - Rules: e.GetSettings().Rules, - Branding: e.GetSettings().Branding, - Shell: e.GetSettings().Shell, - Commands: e.GetSettings().Commands, + Signup: settings.Signup, + Defaults: settings.Defaults, + Rules: settings.Rules, + Branding: settings.Branding, + Shell: settings.Shell, + Commands: settings.Commands, } renderJSON(w, r, data) @@ -51,10 +54,11 @@ func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { return } - e.RLockSettings() - settings := &lib.Settings{} - err = copier.Copy(settings, e.GetSettings()) - e.RUnlockSettings() + settings, err := e.Settings.Get() + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } if err != nil { httpErr(w, r, http.StatusInternalServerError, err) @@ -68,7 +72,7 @@ func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { settings.Shell = req.Shell settings.Commands = req.Commands - err = e.SaveSettings(settings) + err = e.Settings.Save(settings) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) } diff --git a/http/share.go b/http/share.go index 5c2bdf30..aaee18c7 100644 --- a/http/share.go +++ b/http/share.go @@ -8,7 +8,8 @@ import ( "strings" "time" - "github.com/filebrowser/filebrowser/lib" + "github.com/filebrowser/filebrowser/errors" + "github.com/filebrowser/filebrowser/share" ) const apiSharePrefix = "/api/share" @@ -33,9 +34,9 @@ func (e *env) shareGetHandler(w http.ResponseWriter, r *http.Request) { return } - s, err := e.GetLinksByPath(path) - if err == lib.ErrNotExist { - renderJSON(w, r, []*lib.ShareLink{}) + s, err := e.Share.Gets(path) + if err == errors.ErrNotExist { + renderJSON(w, r, []*share.Link{}) return } @@ -46,7 +47,7 @@ func (e *env) shareGetHandler(w http.ResponseWriter, r *http.Request) { for i, link := range s { if link.Expires && link.ExpireDate.Before(time.Now()) { - e.DeleteLink(link.Hash) + e.Share.Delete(link.Hash) s = append(s[:i], s[i+1:]...) } } @@ -72,7 +73,7 @@ func (e *env) shareDeleteHandler(w http.ResponseWriter, r *http.Request) { return } - err := e.DeleteLink(hash) + err := e.Share.Delete(hash) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) return diff --git a/http/static.go b/http/static.go index cf7f7478..10649085 100644 --- a/http/static.go +++ b/http/static.go @@ -11,7 +11,6 @@ import ( "github.com/GeertJohan/go.rice" "github.com/filebrowser/filebrowser/auth" - "github.com/filebrowser/filebrowser/lib" ) func (e *env) getStaticData() map[string]interface{} { diff --git a/http/users.go b/http/users.go index 0cb2731b..aafcd899 100644 --- a/http/users.go +++ b/http/users.go @@ -7,7 +7,8 @@ import ( "strconv" "strings" - "github.com/filebrowser/filebrowser/lib" + + "github.com/filebrowser/filebrowser/users" "github.com/gorilla/mux" ) @@ -22,7 +23,7 @@ func getUserID(r *http.Request) (uint, error) { type modifyUserRequest struct { modifyRequest - Data *lib.User `json:"data"` + Data *users.User `json:"data"` } func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, bool) { @@ -74,7 +75,7 @@ func (e *env) usersGetHandler(w http.ResponseWriter, r *http.Request) { renderJSON(w, r, users) } -func (e *env) userSelfOrAdmin(w http.ResponseWriter, r *http.Request) (*lib.User, uint, bool) { +func (e *env) userSelfOrAdmin(w http.ResponseWriter, r *http.Request) (*users.User, uint, bool) { user, ok := e.getUser(w, r) if !ok { return nil, 0, false @@ -133,7 +134,7 @@ func (e *env) userDeleteHandler(w http.ResponseWriter, r *http.Request) { } func (e *env) userPostHandler(w http.ResponseWriter, r *http.Request) { - _, ok := e.getAdminUser(w,r) + _, ok := e.getAdminUser(w, r) if !ok { return } @@ -197,7 +198,7 @@ func (e *env) userPutHandler(w http.ResponseWriter, r *http.Request) { if req.Data.Password != "" { req.Data.Password, err = lib.HashPwd(req.Data.Password) } else { - var suser *lib.User + var suser *users.User suser, err = e.GetUser(modifiedID) req.Data.Password = suser.Password } diff --git a/lib/auth.go b/lib/auth.go deleted file mode 100644 index 2404742e..00000000 --- a/lib/auth.go +++ /dev/null @@ -1,14 +0,0 @@ -package lib - -import "net/http" - -// AuthMethod describes an authentication method. -type AuthMethod string - -// Auther is the authentication interface. -type Auther interface { - // Auth is called to authenticate a request. - Auth(*http.Request) (*User, error) - // SetInstance attaches the File Browser instance. - SetInstance(*FileBrowser) -} diff --git a/lib/errors.go b/lib/errors.go deleted file mode 100644 index c44507f4..00000000 --- a/lib/errors.go +++ /dev/null @@ -1,17 +0,0 @@ -package lib - -import "errors" - -var ( - ErrExist = errors.New("the resource already exists") - ErrNotExist = errors.New("the resource does not exist") - ErrIsDirectory = errors.New("file is directory") - ErrEmptyPassword = errors.New("password is empty") - ErrEmptyUsername = errors.New("username is empty") - ErrInvalidOption = errors.New("invalid option") - ErrPathIsRel = errors.New("path is relative") - ErrNoPermission = errors.New("permission denied") - ErrInvalidAuthMethod = errors.New("invalid auth method") - ErrEmptyKey = errors.New("empty key") - ErrInvalidDataType = errors.New("invalid data type") -) diff --git a/lib/filebrowser.go b/lib/filebrowser.go deleted file mode 100644 index a9f7e69e..00000000 --- a/lib/filebrowser.go +++ /dev/null @@ -1,404 +0,0 @@ -package lib - -import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/hex" - "fmt" - "hash" - "io" - "log" - "mime" - "net/http" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "sync" - - "github.com/mholt/caddy" - "github.com/spf13/afero" -) - -var defaultEvents = []string{ - "save", - "copy", - "rename", - "upload", - "delete", -} - -// FileBrowser represents a File Browser instance which must -// be created through NewFileBrowser. -type FileBrowser struct { - settings *Settings - storage StorageBackend - mux sync.RWMutex -} - -// NewFileBrowser creates a new File Browser instance from a -// storage backend. If that backend doesn't contain settings -// on it (returns ErrNotExist), then we generate a new key -// and base settings. -func NewFileBrowser(backend StorageBackend) (*FileBrowser, error) { - settings, err := backend.GetSettings() - - if err == ErrNotExist { - var key []byte - key, err = generateRandomBytes(64) - - if err != nil { - return nil, err - } - - settings = &Settings{Key: key} - err = backend.SaveSettings(settings) - } - - if err != nil { - return nil, err - } - - return &FileBrowser{ - settings: settings, - storage: backend, - }, nil -} - -// RLockSettings locks the settings for reading. -func (f *FileBrowser) RLockSettings() { - f.mux.RLock() -} - -// RUnlockSettings unlocks the settings for reading. -func (f *FileBrowser) RUnlockSettings() { - f.mux.RUnlock() -} - -// RulesCheck matches a path against the user rules and the -// global rules. Returns true if allowed, false if not. -func (f *FileBrowser) RulesCheck(u *User, path string) bool { - for _, rule := range u.Rules { - if rule.Matches(path) { - return rule.Allow - } - } - - f.mux.RLock() - defer f.mux.RUnlock() - - for _, rule := range f.settings.Rules { - if rule.Matches(path) { - return rule.Allow - } - } - - return true -} - -// RunHook runs the hooks for the before and after event. -func (f *FileBrowser) RunHook(fn func() error, evt, path, dst string, user *User) error { - path = user.FullPath(path) - dst = user.FullPath(dst) - - if val, ok := f.settings.Commands["before_"+evt]; ok { - for _, command := range val { - err := f.exec(command, "before_"+evt, path, dst, user) - if err != nil { - return err - } - } - } - - err := fn() - if err != nil { - return err - } - - if val, ok := f.settings.Commands["after_"+evt]; ok { - for _, command := range val { - err := f.exec(command, "after_"+evt, path, dst, user) - if err != nil { - return err - } - } - } - - return nil -} - -// ParseCommand parses the command taking in account if the current -// instance uses a shell to run the commands or just calls the binary -// directyly. -func (f *FileBrowser) ParseCommand(raw string) ([]string, error) { - f.RLockSettings() - defer f.RUnlockSettings() - - command := []string{} - - if len(f.settings.Shell) == 0 { - cmd, args, err := caddy.SplitCommandAndArgs(raw) - if err != nil { - return nil, err - } - - _, err = exec.LookPath(cmd) - if err != nil { - return nil, err - } - - command = append(command, cmd) - command = append(command, args...) - } else { - command = append(f.settings.Shell, raw) - } - - return command, nil -} - -// ApplyDefaults applies the default options to a user. -func (f *FileBrowser) ApplyDefaults(u *User) { - f.RLockSettings() - u.Scope = f.settings.Defaults.Scope - u.Locale = f.settings.Defaults.Locale - u.ViewMode = f.settings.Defaults.ViewMode - u.Perm = f.settings.Defaults.Perm - u.Sorting = f.settings.Defaults.Sorting - u.Commands = f.settings.Defaults.Commands - f.RUnlockSettings() -} - -// NewFile creates a File object from a path and a given user. This File -// object will be automatically filled depending on if it is a directory -// or a file. If it's a video file, it will also detect any subtitles. -func (f *FileBrowser) NewFile(path string, user *User) (*File, error) { - if !f.RulesCheck(user, path) { - return nil, os.ErrPermission - } - - info, err := user.Fs.Stat(path) - if err != nil { - return nil, err - } - - file := &File{ - Path: path, - Name: info.Name(), - ModTime: info.ModTime(), - Mode: info.Mode(), - IsDir: info.IsDir(), - Size: info.Size(), - Extension: filepath.Ext(info.Name()), - } - - if file.IsDir { - return file, f.readListing(file, user) - } - - err = f.detectType(file, user) - if err != nil { - return nil, err - } - - if file.Type == "video" { - f.detectSubtitles(file, user) - } - - return file, err -} - -// Checksum checksums a given File for a given User, using a specific -// algorithm. The checksums data is saved on File object. -func (f *FileBrowser) Checksum(file *File, user *User, algo string) error { - if file.IsDir { - return ErrIsDirectory - } - - if file.Checksums == nil { - file.Checksums = map[string]string{} - } - - i, err := user.Fs.Open(file.Path) - if err != nil { - return err - } - defer i.Close() - - var h hash.Hash - - switch algo { - case "md5": - h = md5.New() - case "sha1": - h = sha1.New() - case "sha256": - h = sha256.New() - case "sha512": - h = sha512.New() - default: - return ErrInvalidOption - } - - _, err = io.Copy(h, i) - if err != nil { - return err - } - - file.Checksums[algo] = hex.EncodeToString(h.Sum(nil)) - return nil -} - -func (f *FileBrowser) readListing(file *File, user *User) error { - afs := &afero.Afero{Fs: user.Fs} - files, err := afs.ReadDir(file.Path) - if err != nil { - return err - } - - listing := &Listing{ - Items: []*File{}, - NumDirs: 0, - NumFiles: 0, - } - - for _, i := range files { - name := i.Name() - path := path.Join(file.Path, name) - - if !f.RulesCheck(user, path) { - continue - } - - if strings.HasPrefix(i.Mode().String(), "L") { - // It's a symbolic link. We try to follow it. If it doesn't work, - // we stay with the link information instead if the target's. - info, err := os.Stat(name) - if err == nil { - i = info - } - } - - file := &File{ - Name: name, - Size: i.Size(), - ModTime: i.ModTime(), - Mode: i.Mode(), - IsDir: i.IsDir(), - Extension: filepath.Ext(name), - Path: path, - } - - if file.IsDir { - listing.NumDirs++ - } else { - listing.NumFiles++ - - err := f.detectType(file, user) - if err != nil { - return err - } - } - - listing.Items = append(listing.Items, file) - } - - file.Listing = listing - return nil -} - -func (f *FileBrowser) detectType(file *File, user *User) error { - i, err := user.Fs.Open(file.Path) - if err != nil { - return err - } - defer i.Close() - - buffer := make([]byte, 512) - n, err := i.Read(buffer) - if err != nil && err != io.EOF { - return err - } - - mimetype := mime.TypeByExtension(file.Extension) - if mimetype == "" { - mimetype = http.DetectContentType(buffer[:n]) - } - - switch { - case strings.HasPrefix(mimetype, "video"): - file.Type = "video" - return nil - case strings.HasPrefix(mimetype, "audio"): - file.Type = "audio" - return nil - case strings.HasPrefix(mimetype, "image"): - file.Type = "image" - return nil - case isBinary(string(buffer[:n])) || file.Size > 10*1024*1024: // 10 MB - file.Type = "blob" - return nil - default: - file.Type = "text" - afs := &afero.Afero{Fs: user.Fs} - content, err := afs.ReadFile(file.Path) - if err != nil { - return err - } - file.Content = string(content) - } - - if !user.Perm.Modify && file.Type == "text" { - file.Type = "textImmutable" - } - - return nil -} - -func (f *FileBrowser) detectSubtitles(file *File, user *User) { - file.Subtitles = []string{} - ext := filepath.Ext(file.Path) - base := strings.TrimSuffix(file.Path, ext) - - // TODO: detect multiple languages. Like base.lang.vtt - - path := base + ".vtt" - if _, err := user.Fs.Stat(path); err == nil { - file.Subtitles = append(file.Subtitles, path) - } -} - -func (f *FileBrowser) exec(raw, evt, path, dst string, user *User) error { - blocking := true - - if strings.HasSuffix(raw, "&") { - blocking = false - raw = strings.TrimSpace(strings.TrimSuffix(raw, "&")) - } - - command, err := f.ParseCommand(raw) - if err != nil { - return err - } - - cmd := exec.Command(command[0], command[1:]...) - cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) - cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) - cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt)) - cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) - cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst)) - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if !blocking { - log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " ")) - return cmd.Start() - } - - log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " ")) - return cmd.Run() -} diff --git a/lib/settings.go b/lib/settings.go deleted file mode 100644 index 01eb3bd0..00000000 --- a/lib/settings.go +++ /dev/null @@ -1,38 +0,0 @@ -package lib - -// Settings contain the main settings of the application. -type Settings struct { - Key []byte `json:"key"` - BaseURL string `json:"baseURL"` - Signup bool `json:"signup"` - Defaults UserDefaults `json:"defaults"` - AuthMethod AuthMethod `json:"authMethod"` - Branding Branding `json:"branding"` - Commands map[string][]string `json:"commands"` - Shell []string `json:"shell"` - Rules []Rule `json:"rules"` // TODO: use this add to cli -} - -// Sorting contains a sorting order. -type Sorting struct { - By string `json:"by"` - Asc bool `json:"asc"` -} - -// Branding contains the branding settings of the app. -type Branding struct { - Name string `json:"name"` - DisableExternal bool `json:"disableExternal"` - Files string `json:"files"` -} - -// UserDefaults is a type that holds the default values -// for some fields on User. -type UserDefaults struct { - Scope string `json:"scope"` - Locale string `json:"locale"` - ViewMode ViewMode `json:"viewMode"` - Sorting Sorting `json:"sorting"` - Perm Permissions `json:"perm"` - Commands []string `json:"commands"` -} diff --git a/lib/storage.go b/lib/storage.go deleted file mode 100644 index b3d563d6..00000000 --- a/lib/storage.go +++ /dev/null @@ -1,199 +0,0 @@ -package lib - -import ( - "strings" -) - -// StorageBackend is the interface used to persist data. -type StorageBackend interface { - GetUserByID(uint) (*User, error) - GetUserByUsername(string) (*User, error) - GetUsers() ([]*User, error) - SaveUser(u *User) error - UpdateUser(u *User, fields ...string) error - DeleteUserByID(uint) error - DeleteUserByUsername(string) error - GetSettings() (*Settings, error) - SaveSettings(*Settings) error - GetAuther(AuthMethod) (Auther, error) - SaveAuther(Auther) error - GetLinkByHash(hash string) (*ShareLink, error) - GetLinkPermanent(path string) (*ShareLink, error) - GetLinksByPath(path string) ([]*ShareLink, error) - SaveLink(s *ShareLink) error - DeleteLink(hash string) error -} - -// GetUser allows you to get a user by its name or username. The provided -// id must be a string for username lookup or a uint for id lookup. If id -// is neither, a ErrInvalidDataType will be returned. -func (f *FileBrowser) GetUser(id interface{}) (*User, error) { - var ( - user *User - err error - ) - - switch id.(type) { - case string: - user, err = f.storage.GetUserByUsername(id.(string)) - case uint: - user, err = f.storage.GetUserByID(id.(uint)) - default: - return nil, ErrInvalidDataType - } - - if err != nil { - return nil, err - } - - user.clean() - return user, err -} - -// GetUsers gets a list of all users. -func (f *FileBrowser) GetUsers() ([]*User, error) { - users, err := f.storage.GetUsers() - if err != nil { - return nil, err - } - - for _, user := range users { - user.clean() - } - - return users, err -} - -// UpdateUser updates a user in the database. -func (f *FileBrowser) UpdateUser(user *User, fields ...string) error { - err := user.clean(fields...) - if err != nil { - return err - } - - return f.storage.UpdateUser(user, fields...) -} - -// SaveUser saves the user in a storage. -func (f *FileBrowser) SaveUser(user *User) error { - if err := user.clean(); err != nil { - return err - } - - return f.storage.SaveUser(user) -} - -// DeleteUser allows you to delete a user by its name or username. The provided -// id must be a string for username lookup or a uint for id lookup. If id -// is neither, a ErrInvalidDataType will be returned. -func (f *FileBrowser) DeleteUser(id interface{}) (err error) { - switch id.(type) { - case string: - err = f.storage.DeleteUserByUsername(id.(string)) - case uint: - err = f.storage.DeleteUserByID(id.(uint)) - default: - err = ErrInvalidDataType - } - - return -} - -// GetSettings returns the settings for the current instance. -func (f *FileBrowser) GetSettings() *Settings { - return f.settings -} - -// SaveSettings saves the settings for the current instance. -func (f *FileBrowser) SaveSettings(s *Settings) error { - s.BaseURL = strings.TrimSuffix(s.BaseURL, "/") - - if len(s.Key) == 0 { - return ErrEmptyKey - } - - if s.Defaults.Locale == "" { - s.Defaults.Locale = "en" - } - - if s.Defaults.Commands == nil { - s.Defaults.Commands = []string{} - } - - if s.Defaults.ViewMode == "" { - s.Defaults.ViewMode = MosaicViewMode - } - - if s.Rules == nil { - s.Rules = []Rule{} - } - - if s.Shell == nil { - s.Shell = []string{} - } - - if s.Commands == nil { - s.Commands = map[string][]string{} - } - - for _, event := range defaultEvents { - if _, ok := s.Commands["before_"+event]; !ok { - s.Commands["before_"+event] = []string{} - } - - if _, ok := s.Commands["after_"+event]; !ok { - s.Commands["after_"+event] = []string{} - } - } - - err := f.storage.SaveSettings(s) - if err != nil { - return err - } - - f.mux.Lock() - f.settings = s - f.mux.Unlock() - return nil -} - -// GetAuther wraps a StorageBackend.GetAuther and calls SetInstance on the auther. -func (f *FileBrowser) GetAuther(t AuthMethod) (Auther, error) { - auther, err := f.storage.GetAuther(t) - if err != nil { - return nil, err - } - - auther.SetInstance(f) - return auther, nil -} - -// SaveAuther wraps a StorageBackend.SaveAuther. -func (f *FileBrowser) SaveAuther(a Auther) error { - return f.storage.SaveAuther(a) -} - -// GetLinkByHash wraps a StorageBackend.GetLinkByHash. -func (f *FileBrowser) GetLinkByHash(hash string) (*ShareLink, error) { - return f.storage.GetLinkByHash(hash) -} - -// GetLinkPermanent wraps a StorageBackend.GetLinkPermanent -func (f *FileBrowser) GetLinkPermanent(path string) (*ShareLink, error) { - return f.storage.GetLinkPermanent(path) -} - -// GetLinksByPath wraps a StorageBackend.GetLinksByPath -func (f *FileBrowser) GetLinksByPath(path string) ([]*ShareLink, error) { - return f.storage.GetLinksByPath(path) -} - -// SaveLink wraps a StorageBackend.SaveLink -func (f *FileBrowser) SaveLink(s *ShareLink) error { - return f.storage.SaveLink(s) -} - -// DeleteLink wraps a StorageBackend.DeleteLink -func (f *FileBrowser) DeleteLink(hash string) error { - return f.storage.DeleteLink(hash) -} diff --git a/lib/utils.go b/lib/utils.go deleted file mode 100644 index ee828dfb..00000000 --- a/lib/utils.go +++ /dev/null @@ -1,37 +0,0 @@ -package lib - -import ( - "crypto/rand" - - "golang.org/x/crypto/bcrypt" -) - -func generateRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - // Note that err == nil only if we read len(b) bytes. - return b, err -} - -func isBinary(content string) bool { - for _, b := range content { - // 65533 is the unknown char - // 8 and below are control chars (e.g. backspace, null, eof, etc) - if b <= 8 || b == 65533 { - return true - } - } - return false -} - -// HashPwd hashes a password. -func HashPwd(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(bytes), err -} - -// CheckPwd checks if a password is correct. -func CheckPwd(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} diff --git a/lib/rule.go b/rules/rules.go similarity index 88% rename from lib/rule.go rename to rules/rules.go index 1acc5034..548332a4 100644 --- a/lib/rule.go +++ b/rules/rules.go @@ -1,10 +1,15 @@ -package lib +package rules import ( "regexp" "strings" ) +// Checker is a Rules checker. +type Checker interface { + Check(path string) bool +} + // Rule is a allow/disallow rule. type Rule struct { Regex bool `json:"regex"` diff --git a/settings/branding.go b/settings/branding.go new file mode 100644 index 00000000..8a835b1d --- /dev/null +++ b/settings/branding.go @@ -0,0 +1,8 @@ +package settings + +// Branding contains the branding settings of the app. +type Branding struct { + Name string `json:"name"` + DisableExternal bool `json:"disableExternal"` + Files string `json:"files"` +} diff --git a/settings/defaults.go b/settings/defaults.go new file mode 100644 index 00000000..0b6bc03d --- /dev/null +++ b/settings/defaults.go @@ -0,0 +1,27 @@ +package settings + +import ( + "github.com/filebrowser/filebrowser/files" + "github.com/filebrowser/filebrowser/users" +) + +// UserDefaults is a type that holds the default values +// for some fields on User. +type UserDefaults struct { + Scope string `json:"scope"` + Locale string `json:"locale"` + ViewMode users.ViewMode `json:"viewMode"` + Sorting files.Sorting `json:"sorting"` + Perm users.Permissions `json:"perm"` + Commands []string `json:"commands"` +} + +// Apply applies the default options to a user. +func (d *UserDefaults) Apply(u *users.User) { + u.Scope = d.Scope + u.Locale = d.Locale + u.ViewMode = d.ViewMode + u.Perm = d.Perm + u.Sorting = d.Sorting + u.Commands = d.Commands +} diff --git a/settings/settings.go b/settings/settings.go new file mode 100644 index 00000000..9318027d --- /dev/null +++ b/settings/settings.go @@ -0,0 +1,24 @@ +package settings + +import "github.com/filebrowser/filebrowser/rules" + +// AuthMethod describes an authentication method. +type AuthMethod string + +// Settings contain the main settings of the application. +type Settings struct { + Key []byte `json:"key"` + BaseURL string `json:"baseURL"` + Signup bool `json:"signup"` + Defaults UserDefaults `json:"defaults"` + AuthMethod AuthMethod `json:"authMethod"` + Branding Branding `json:"branding"` + Commands map[string][]string `json:"commands"` + Shell []string `json:"shell"` + Rules []rules.Rule `json:"rules"` // TODO: use this add to cli +} + +// GetRules implements rules.Provider. +func (s *Settings) GetRules() []rules.Rule { + return s.Rules +} diff --git a/settings/storage.go b/settings/storage.go new file mode 100644 index 00000000..98b0eaeb --- /dev/null +++ b/settings/storage.go @@ -0,0 +1,88 @@ +package settings + +import ( + "strings" + + "github.com/filebrowser/filebrowser/errors" + "github.com/filebrowser/filebrowser/rules" + "github.com/filebrowser/filebrowser/users" +) + +// StorageBackend is a settings storage backend. +type StorageBackend interface { + Get() (*Settings, error) + Save(*Settings) error +} + +// Storage is a settings storage. +type Storage struct { + back StorageBackend +} + +// NewStorage creates a settings storage from a backend. +func NewStorage(back StorageBackend) *Storage { + return &Storage{back: back} +} + +// Get returns the settings for the current instance. +func (s *Storage) Get() (*Settings, error) { + return s.back.Get() +} + +var defaultEvents = []string{ + "save", + "copy", + "rename", + "upload", + "delete", +} + +// Save saves the settings for the current instance. +func (s *Storage) Save(set *Settings) error { + set.BaseURL = strings.TrimSuffix(set.BaseURL, "/") + + if len(set.Key) == 0 { + return errors.ErrEmptyKey + } + + if set.Defaults.Locale == "" { + set.Defaults.Locale = "en" + } + + if set.Defaults.Commands == nil { + set.Defaults.Commands = []string{} + } + + if set.Defaults.ViewMode == "" { + set.Defaults.ViewMode = users.MosaicViewMode + } + + if set.Rules == nil { + set.Rules = []rules.Rule{} + } + + if set.Shell == nil { + set.Shell = []string{} + } + + if set.Commands == nil { + set.Commands = map[string][]string{} + } + + for _, event := range defaultEvents { + if _, ok := set.Commands["before_"+event]; !ok { + set.Commands["before_"+event] = []string{} + } + + if _, ok := set.Commands["after_"+event]; !ok { + set.Commands["after_"+event] = []string{} + } + } + + err := s.back.Save(set) + if err != nil { + return err + } + + return nil +} diff --git a/lib/share.go b/share/share.go similarity index 66% rename from lib/share.go rename to share/share.go index 0f9f7167..7e4add87 100644 --- a/lib/share.go +++ b/share/share.go @@ -1,9 +1,9 @@ -package lib +package share import "time" -// ShareLink is the information needed to build a shareable link. -type ShareLink struct { +// Link is the information needed to build a shareable link. +type Link struct { Hash string `json:"hash" storm:"id,index"` Path string `json:"path" storm:"index"` Expires bool `json:"expires"` diff --git a/share/storage.go b/share/storage.go new file mode 100644 index 00000000..50a97ccc --- /dev/null +++ b/share/storage.go @@ -0,0 +1,45 @@ +package share + +// StorageBackend is the interface to implement for a share storage. +type StorageBackend interface { + GetByHash(hash string) (*Link, error) + GetPermanent(path string) (*Link, error) + Gets(path string) ([]*Link, error) + Save(s *Link) error + Delete(hash string) error +} + +// Storage is a storage. +type Storage struct { + back StorageBackend +} + +// NewStorage creates a share links storage from a backend. +func NewStorage(back StorageBackend) *Storage { + return &Storage{back: back} +} + +// GetByHash wraps a StorageBackend.GetByHash. +func (s *Storage) GetByHash(hash string) (*Link, error) { + return s.back.GetByHash(hash) +} + +// GetPermanent wraps a StorageBackend.GetPermanent +func (s *Storage) GetPermanent(path string) (*Link, error) { + return s.back.GetPermanent(path) +} + +// Gets wraps a StorageBackend.Gets +func (s *Storage) Gets(path string) ([]*Link, error) { + return s.back.Gets(path) +} + +// Save wraps a StorageBackend.Save +func (s *Storage) Save(l *Link) error { + return s.back.Save(l) +} + +// Delete wraps a StorageBackend.Delete +func (s *Storage) Delete(hash string) error { + return s.back.Delete(hash) +} diff --git a/bolt/bolt.go b/storage/bolt/bolt.go similarity index 71% rename from bolt/bolt.go rename to storage/bolt/bolt.go index 70c8e369..824de7f9 100644 --- a/bolt/bolt.go +++ b/storage/bolt/bolt.go @@ -2,7 +2,7 @@ package bolt import "github.com/asdine/storm" -// Backend implements lib.StorageBackend +// Backend implements storage.Backend // using Bolt DB. type Backend struct { DB *storm.DB diff --git a/bolt/config.go b/storage/bolt/config.go similarity index 83% rename from bolt/config.go rename to storage/bolt/config.go index a00ed897..ed2cfa1d 100644 --- a/bolt/config.go +++ b/storage/bolt/config.go @@ -3,7 +3,7 @@ package bolt import ( "github.com/asdine/storm" "github.com/filebrowser/filebrowser/auth" - "github.com/filebrowser/filebrowser/lib" + ) func (b Backend) get(name string, to interface{}) error { @@ -19,12 +19,12 @@ func (b Backend) save(name string, from interface{}) error { return b.DB.Set("config", name, from) } -func (b Backend) GetSettings() (*lib.Settings, error) { - settings := &lib.Settings{} +func (b Backend) GetSettings() (*settings.Settings, error) { + settings := &settings.Settings{} return settings, b.get("settings", settings) } -func (b Backend) SaveSettings(s *lib.Settings) error { +func (b Backend) SaveSettings(s *settings.Settings) error { return b.save("settings", s) } diff --git a/bolt/share.go b/storage/bolt/share.go similarity index 95% rename from bolt/share.go rename to storage/bolt/share.go index ef1c6303..374a60c0 100644 --- a/bolt/share.go +++ b/storage/bolt/share.go @@ -3,7 +3,7 @@ package bolt import ( "github.com/asdine/storm" "github.com/asdine/storm/q" - "github.com/filebrowser/filebrowser/lib" + ) func (s Backend) GetLinkByHash(hash string) (*lib.ShareLink, error) { diff --git a/bolt/users.go b/storage/bolt/users.go similarity index 97% rename from bolt/users.go rename to storage/bolt/users.go index 085ad2d2..b599a132 100644 --- a/bolt/users.go +++ b/storage/bolt/users.go @@ -4,7 +4,7 @@ import ( "reflect" "github.com/asdine/storm" - "github.com/filebrowser/filebrowser/lib" + ) func (st Backend) GetUserByID(id uint) (*lib.User, error) { diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 00000000..6924d1f7 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,17 @@ +package storage + +import ( + "github.com/filebrowser/filebrowser/auth" + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/share" + "github.com/filebrowser/filebrowser/users" +) + +// Storage is a storage powered by a Backend whih makes the neccessary +// verifications when fetching and saving data to ensure consistency. +type Storage struct { + Users *users.Storage + Share *share.Storage + Auth *auth.Storage + Settings *settings.Storage +} diff --git a/storage/utils.go b/storage/utils.go new file mode 100644 index 00000000..8f27b354 --- /dev/null +++ b/storage/utils.go @@ -0,0 +1,10 @@ +package storage + +import "crypto/rand" + +func generateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + return b, err +} diff --git a/users/password.go b/users/password.go new file mode 100644 index 00000000..d7ef250a --- /dev/null +++ b/users/password.go @@ -0,0 +1,17 @@ +package users + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashPwd hashes a password. +func HashPwd(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPwd checks if a password is correct. +func CheckPwd(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/users/permissions.go b/users/permissions.go new file mode 100644 index 00000000..29e0a5b9 --- /dev/null +++ b/users/permissions.go @@ -0,0 +1,13 @@ +package users + +// Permissions describe a user's permissions. +type Permissions struct { + Admin bool `json:"admin"` + Execute bool `json:"execute"` + Create bool `json:"create"` + Rename bool `json:"rename"` + Modify bool `json:"modify"` + Delete bool `json:"delete"` + Share bool `json:"share"` + Download bool `json:"download"` +} diff --git a/users/storage.go b/users/storage.go new file mode 100644 index 00000000..7cc03b3b --- /dev/null +++ b/users/storage.go @@ -0,0 +1,101 @@ +package users + +import ( + "github.com/filebrowser/filebrowser/errors" +) + +// StorageBackend is the interface to implement for a users storage. +type StorageBackend interface { + GetByID(uint) (*User, error) + GetByUsername(string) (*User, error) + Gets() ([]*User, error) + Save(u *User) error + Update(u *User, fields ...string) error + DeleteByID(uint) error + DeleteByUsername(string) error +} + +// Storage is a users storage. +type Storage struct { + back StorageBackend +} + +// NewStorage creates a users storage from a backend. +func NewStorage(back StorageBackend) *Storage { + return &Storage{back: back} +} + +// Get allows you to get a user by its name or username. The provided +// id must be a string for username lookup or a uint for id lookup. If id +// is neither, a ErrInvalidDataType will be returned. +func (s *Storage) Get(id interface{}) (*User, error) { + var ( + user *User + err error + ) + + switch id.(type) { + case string: + user, err = s.back.GetByUsername(id.(string)) + case uint: + user, err = s.back.GetByID(id.(uint)) + default: + return nil, errors.ErrInvalidDataType + } + + if err != nil { + return nil, err + } + + user.Clean() + return user, err +} + +// Gets gets a list of all users. +func (s *Storage) Gets() ([]*User, error) { + users, err := s.back.Gets() + if err != nil { + return nil, err + } + + for _, user := range users { + user.Clean() + } + + return users, err +} + +// Update updates a user in the database. +func (s *Storage) Update(user *User, fields ...string) error { + err := user.Clean(fields...) + if err != nil { + return err + } + + return s.back.Update(user, fields...) +} + +// Save saves the user in a storage. +func (s *Storage) Save(user *User) error { + if err := user.Clean(); err != nil { + return err + } + + return s.back.Save(user) +} + +// Delete allows you to delete a user by its name or username. The provided +// id must be a string for username lookup or a uint for id lookup. If id +// is neither, a ErrInvalidDataType will be returned. +func (s *Storage) Delete(id interface{}) (err error) { + switch id.(type) { + case string: + err = s.back.DeleteByUsername(id.(string)) + case uint: + err = s.back.DeleteByID(id.(uint)) + default: + err = errors.ErrInvalidDataType + } + + return +} diff --git a/lib/user.go b/users/users.go similarity index 57% rename from lib/user.go rename to users/users.go index 9973fee2..0527009b 100644 --- a/lib/user.go +++ b/users/users.go @@ -1,9 +1,12 @@ -package lib +package users import ( + "github.com/filebrowser/filebrowser/errors" "path/filepath" "regexp" + "github.com/filebrowser/filebrowser/files" + "github.com/filebrowser/filebrowser/rules" "github.com/spf13/afero" ) @@ -15,32 +18,25 @@ const ( MosaicViewMode ViewMode = "mosaic" ) -// Permissions describe a user's permissions. -type Permissions struct { - Admin bool `json:"admin"` - Execute bool `json:"execute"` - Create bool `json:"create"` - Rename bool `json:"rename"` - Modify bool `json:"modify"` - Delete bool `json:"delete"` - Share bool `json:"share"` - Download bool `json:"download"` -} - // User describes a user. type User struct { - ID uint `storm:"id,increment" json:"id"` - Username string `storm:"unique" json:"username"` - Password string `json:"password"` - Scope string `json:"scope"` - Locale string `json:"locale"` - LockPassword bool `json:"lockPassword"` - ViewMode ViewMode `json:"viewMode"` - Perm Permissions `json:"perm"` - Commands []string `json:"commands"` - Sorting Sorting `json:"sorting"` - Fs afero.Fs `json:"-"` - Rules []Rule `json:"rules"` + ID uint `storm:"id,increment" json:"id"` + Username string `storm:"unique" json:"username"` + Password string `json:"password"` + Scope string `json:"scope"` + Locale string `json:"locale"` + LockPassword bool `json:"lockPassword"` + ViewMode ViewMode `json:"viewMode"` + Perm Permissions `json:"perm"` + Commands []string `json:"commands"` + Sorting files.Sorting `json:"sorting"` + Fs afero.Fs `json:"-"` + Rules []rules.Rule `json:"rules"` +} + +// GetRules implements rules.Provider. +func (u *User) GetRules() []rules.Rule { + return u.Rules } var checkableFields = []string{ @@ -53,7 +49,9 @@ var checkableFields = []string{ "Rules", } -func (u *User) clean(fields ...string) error { +// Clean cleans up a user and verifies if all its fields +// are alright to be saved. +func (u *User) Clean(fields ...string) error { if len(fields) == 0 { fields = checkableFields } @@ -62,15 +60,15 @@ func (u *User) clean(fields ...string) error { switch field { case "Username": if u.Username == "" { - return ErrEmptyUsername + return errors.ErrEmptyUsername } case "Password": if u.Password == "" { - return ErrEmptyPassword + return errors.ErrEmptyPassword } case "Scope": if !filepath.IsAbs(u.Scope) { - return ErrPathIsRel + return errors.ErrScopeIsRelative } case "ViewMode": if u.ViewMode == "" { @@ -86,7 +84,7 @@ func (u *User) clean(fields ...string) error { } case "Rules": if u.Rules == nil { - u.Rules = []Rule{} + u.Rules = []rules.Rule{} } } } diff --git a/lib/version.go b/version/version.go similarity index 84% rename from lib/version.go rename to version/version.go index 95359653..e9ae924e 100644 --- a/lib/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ -package lib +package version const ( // Version is the current File Browser version.