diff --git a/auth/json.go b/auth/json.go index d47dafd4..481b3889 100644 --- a/auth/json.go +++ b/auth/json.go @@ -21,7 +21,7 @@ type jsonCred struct { // JSONAuth is a json implementaion of an Auther. type JSONAuth struct { ReCaptcha *ReCaptcha - store *types.Storage + instance *types.FileBrowser } // Auth authenticates the user via a json in content body. @@ -50,7 +50,7 @@ func (a *JSONAuth) Auth(r *http.Request) (*types.User, error) { } } - u, err := a.store.GetUser(cred.Username) + u, err := a.instance.GetUser(cred.Username) if err != nil || !types.CheckPwd(cred.Password, u.Password) { return nil, types.ErrNoPermission } @@ -58,9 +58,9 @@ func (a *JSONAuth) Auth(r *http.Request) (*types.User, error) { return u, nil } -// SetStorage attaches the storage information to the auther. -func (a *JSONAuth) SetStorage(s *types.Storage) { - a.store = s +// SetInstance attaches the instance to the auther. +func (a *JSONAuth) SetInstance(i *types.FileBrowser) { + a.instance = i } const reCaptchaAPI = "/recaptcha/api/siteverify" diff --git a/auth/none.go b/auth/none.go index 1bf12e4e..b542f807 100644 --- a/auth/none.go +++ b/auth/none.go @@ -11,15 +11,15 @@ const MethodNoAuth types.AuthMethod = "noauth" // NoAuth is no auth implementation of auther. type NoAuth struct { - store *types.Storage + instance *types.FileBrowser } // Auth uses authenticates user 1. func (a *NoAuth) Auth(r *http.Request) (*types.User, error) { - return a.store.GetUser(1) + return a.instance.GetUser(1) } -// SetStorage attaches the storage information to the auther. -func (a *NoAuth) SetStorage(s *types.Storage) { - a.store = s +// SetInstance attaches the instance to the auther. +func (a *NoAuth) SetInstance(i *types.FileBrowser) { + a.instance = i } diff --git a/auth/proxy.go b/auth/proxy.go index 6f3dabea..0a913b80 100644 --- a/auth/proxy.go +++ b/auth/proxy.go @@ -11,14 +11,14 @@ const MethodProxyAuth types.AuthMethod = "proxy" // ProxyAuth is a proxy implementation of an auther. type ProxyAuth struct { - Header string - store *types.Storage + Header string + instance *types.FileBrowser } // Auth authenticates the user via an HTTP header. func (a *ProxyAuth) Auth(r *http.Request) (*types.User, error) { username := r.Header.Get(a.Header) - user, err := a.store.GetUser(username) + user, err := a.instance.GetUser(username) if err == types.ErrNotExist { return nil, types.ErrNoPermission } @@ -26,7 +26,7 @@ func (a *ProxyAuth) Auth(r *http.Request) (*types.User, error) { return user, err } -// SetStorage attaches the storage information to the auther. -func (a *ProxyAuth) SetStorage(s *types.Storage) { - a.store = s +// SetInstance attaches the instance to the auther. +func (a *ProxyAuth) SetInstance(i *types.FileBrowser) { + a.instance = i } diff --git a/cmd/cmds_add.go b/cmd/cmds_add.go index 13518eae..19b44f67 100644 --- a/cmd/cmds_add.go +++ b/cmd/cmds_add.go @@ -20,15 +20,14 @@ var cmdsAddCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) - s, err := st.GetSettings() - checkErr(err) + st := getFileBrowser(db) + s := st.GetSettings() evt := mustGetString(cmd, "event") command := mustGetString(cmd, "command") s.Commands[evt] = append(s.Commands[evt], command) - err = st.SaveSettings(s) + err := st.SaveSettings(s) checkErr(err) printEvents(s.Commands) }, diff --git a/cmd/cmds_ls.go b/cmd/cmds_ls.go index d8199410..e46a1aa8 100644 --- a/cmd/cmds_ls.go +++ b/cmd/cmds_ls.go @@ -17,9 +17,8 @@ var cmdsLsCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) - s, err := st.GetSettings() - checkErr(err) + st := getFileBrowser(db) + s := st.GetSettings() evt := mustGetString(cmd, "event") if evt == "" { diff --git a/cmd/cmds_rm.go b/cmd/cmds_rm.go index d6dc5046..efe1f6d4 100644 --- a/cmd/cmds_rm.go +++ b/cmd/cmds_rm.go @@ -20,9 +20,8 @@ var cmdsRmCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) - s, err := st.GetSettings() - checkErr(err) + st := getFileBrowser(db) + s := st.GetSettings() evt := mustGetString(cmd, "event") i, err := cmd.Flags().GetUint("index") diff --git a/cmd/config_cat.go b/cmd/config_cat.go index 0deebfcd..1c59fc34 100644 --- a/cmd/config_cat.go +++ b/cmd/config_cat.go @@ -16,9 +16,8 @@ var configCatCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) - s, err := st.GetSettings() - checkErr(err) + st := getFileBrowser(db) + s := st.GetSettings() auther, err := st.GetAuther(s.AuthMethod) checkErr(err) printSettings(s, auther) diff --git a/cmd/config_init.go b/cmd/config_init.go index 6fc305cf..8d8b4f65 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -36,25 +36,27 @@ override the options.`, getUserDefaults(cmd, &defaults, true) authMethod, auther := getAuthentication(cmd) - settings := &types.Settings{ - Key: generateRandomBytes(64), // 256 bits - BaseURL: mustGetString(cmd, "baseURL"), - Signup: mustGetBool(cmd, "signup"), - Shell: strings.Split(strings.TrimSpace(mustGetString(cmd, "shell")), " "), - Defaults: defaults, - AuthMethod: authMethod, - Branding: types.Branding{ - Name: mustGetString(cmd, "branding.name"), - DisableExternal: mustGetBool(cmd, "branding.disableExternal"), - Files: mustGetString(cmd, "branding.files"), - }, - } - db, err := storm.Open(databasePath) checkErr(err) defer db.Close() + st := getFileBrowser(db) + settings := st.GetSettings() - saveConfig(db, settings, auther) + settings.BaseURL = mustGetString(cmd, "baseURL") + settings.Signup = mustGetBool(cmd, "signup") + settings.Shell = strings.Split(strings.TrimSpace(mustGetString(cmd, "shell")), " ") + settings.Defaults = defaults + settings.AuthMethod = authMethod + settings.Branding = types.Branding{ + Name: mustGetString(cmd, "branding.name"), + DisableExternal: mustGetBool(cmd, "branding.disableExternal"), + Files: mustGetString(cmd, "branding.files"), + } + + err = st.SaveSettings(settings) + checkErr(err) + err = st.SaveAuther(auther) + checkErr(err) fmt.Printf(` Congratulations! You've set up your database to use with File Browser. @@ -64,11 +66,3 @@ need to call the main command to boot up the server. printSettings(settings, auther) }, } - -func saveConfig(db *storm.DB, s *types.Settings, a types.Auther) { - st := getStore(db) - err := st.SaveSettings(s) - checkErr(err) - err = st.SaveAuther(a) - checkErr(err) -} diff --git a/cmd/config_set.go b/cmd/config_set.go index 737ac25c..13301590 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -23,9 +23,8 @@ you want to change.`, db := getDB() defer db.Close() - st := getStore(db) - s, err := st.GetSettings() - checkErr(err) + st := getFileBrowser(db) + s := st.GetSettings() auth := false cmd.Flags().Visit(func(flag *pflag.Flag) { @@ -50,6 +49,7 @@ you want to change.`, getUserDefaults(cmd, &s.Defaults, false) var auther types.Auther + var err error if auth { s.AuthMethod, auther = getAuthentication(cmd) err = st.SaveAuther(auther) diff --git a/cmd/root.go b/cmd/root.go index b74447de..dbb44b32 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,16 +56,10 @@ listening on loalhost on a random port. Use the flags to change it.`, var err error db := getDB() defer db.Close() - - env := &fhttp.Env{ - Store: getStore(db), - } - - env.Settings, err = env.Store.GetSettings() + fb := getFileBrowser(db) + env := &fhttp.Env{FileBrowser: fb} + env.Auther, err = env.GetAuther(env.GetSettings().AuthMethod) checkErr(err) - env.Auther, err = env.Store.GetAuther(env.Settings.AuthMethod) - checkErr(err) - startServer(cmd, env) }, } @@ -94,27 +88,36 @@ func quickSetup(cmd *cobra.Command) { panic(errors.New("scope flag must be set for quick setup")) } - settings := &types.Settings{ - Key: generateRandomBytes(64), - BaseURL: "", - Signup: false, - AuthMethod: auth.MethodJSONAuth, - Defaults: types.UserDefaults{ - Scope: scope, - Locale: "en", - Perm: types.Permissions{ - Admin: false, - Execute: true, - Create: true, - Rename: true, - Modify: true, - Delete: true, - Share: true, - Download: true, - }, + db, err := storm.Open(databasePath) + checkErr(err) + defer db.Close() + fb := getFileBrowser(db) + + settings := fb.GetSettings() + settings.BaseURL = "" + settings.Signup = false + settings.AuthMethod = auth.MethodJSONAuth + settings.Defaults = types.UserDefaults{ + Scope: scope, + Locale: "en", + Perm: types.Permissions{ + Admin: false, + Execute: true, + Create: true, + Rename: true, + Modify: true, + Delete: true, + Share: true, + Download: true, }, } + err = fb.SaveSettings(settings) + checkErr(err) + + err = fb.SaveAuther(&auth.JSONAuth{}) + checkErr(err) + password, err := types.HashPwd("admin") checkErr(err) @@ -124,17 +127,10 @@ func quickSetup(cmd *cobra.Command) { LockPassword: false, } - user.ApplyDefaults(settings.Defaults) + fb.ApplyDefaults(user) user.Perm.Admin = true - db, err := storm.Open(databasePath) - checkErr(err) - defer db.Close() - - saveConfig(db, settings, &auth.JSONAuth{}) - - st := getStore(db) - err = st.SaveUser(user) + err = fb.SaveUser(user) checkErr(err) } diff --git a/cmd/users_find.go b/cmd/users_find.go index 07597354..9a2de82f 100644 --- a/cmd/users_find.go +++ b/cmd/users_find.go @@ -30,7 +30,7 @@ var usersLsCmd = &cobra.Command{ var findUsers = func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) + st := getFileBrowser(db) username, _ := cmd.Flags().GetString("username") id, _ := cmd.Flags().GetUint("id") diff --git a/cmd/users_new.go b/cmd/users_new.go index b6ff5898..1950bab3 100644 --- a/cmd/users_new.go +++ b/cmd/users_new.go @@ -23,14 +23,13 @@ var usersNewCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) + st := getFileBrowser(db) - settings, err := st.GetSettings() - checkErr(err) + settings := st.GetSettings() getUserDefaults(cmd, &settings.Defaults, false) password, _ := cmd.Flags().GetString("password") - password, err = types.HashPwd(password) + password, err := types.HashPwd(password) checkErr(err) user := &types.User{ @@ -39,8 +38,7 @@ var usersNewCmd = &cobra.Command{ LockPassword: mustGetBool(cmd, "lockPassword"), } - user.ApplyDefaults(settings.Defaults) - + st.ApplyDefaults(user) err = st.SaveUser(user) checkErr(err) printUsers([]*types.User{user}) diff --git a/cmd/users_rm.go b/cmd/users_rm.go index ef454ba1..346c3223 100644 --- a/cmd/users_rm.go +++ b/cmd/users_rm.go @@ -20,7 +20,7 @@ var usersRmCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) + st := getFileBrowser(db) username, _ := cmd.Flags().GetString("username") id, _ := cmd.Flags().GetUint("id") diff --git a/cmd/users_update.go b/cmd/users_update.go index cebcc787..f20e96f1 100644 --- a/cmd/users_update.go +++ b/cmd/users_update.go @@ -23,7 +23,7 @@ options you want to change.`, Run: func(cmd *cobra.Command, args []string) { db := getDB() defer db.Close() - st := getStore(db) + st := getFileBrowser(db) id, _ := cmd.Flags().GetUint("id") username := mustGetString(cmd, "username") diff --git a/cmd/utils.go b/cmd/utils.go index 23f515c3..73bac359 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -1,7 +1,6 @@ package cmd import ( - "crypto/rand" "errors" "os" @@ -39,14 +38,8 @@ func getDB() *storm.DB { return db } -func getStore(db *storm.DB) *types.Storage { - return types.NewStorage(&bolt.Backend{DB: db}) -} - -func generateRandomBytes(n int) []byte { - b := make([]byte, n) - _, err := rand.Read(b) - // Note that err == nil only if we read len(b) bytes. +func getFileBrowser(db *storm.DB) *types.FileBrowser { + fb, err := types.NewFileBrowser(&bolt.Backend{DB: db}) checkErr(err) - return b + return fb } diff --git a/go.sum b/go.sum index c5dcc82e..17517a3e 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/DataDog/zstd v1.3.4 h1:LAGHkXuvC6yky+C2CUG2tD7w8QlrUwpue8XwIh0X4AY= github.com/DataDog/zstd v1.3.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da h1:UVU3a9pRUyLdnBtn60WjRl0s4SEyJc2ChCY56OAR6wI= github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw= +github.com/NaturalNode/natural v1.0.9 h1:ry8vPQSJc+SvewawAOTzjeZNbYD7AQzmNddCuwqJk4c= github.com/Sereal/Sereal v0.0.0-20180905114147-563b78806e28 h1:KjLSBawWQq6I0p9VRX8RtHIuttTYvUCGfMgNoBBFxYs= github.com/Sereal/Sereal v0.0.0-20180905114147-563b78806e28/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= diff --git a/http/auth.go b/http/auth.go index 8a569e5a..4f790dca 100644 --- a/http/auth.go +++ b/http/auth.go @@ -12,6 +12,7 @@ import ( "github.com/filebrowser/filebrowser/types" ) + func (e *Env) loginHandler(w http.ResponseWriter, r *http.Request) { user, err := e.Auther.Auth(r) if err == types.ErrNoPermission { @@ -29,14 +30,17 @@ type signupBody struct { } func (e *Env) signupHandler(w http.ResponseWriter, r *http.Request) { - e.mux.RLock() - defer e.mux.RUnlock() + e.RLockSettings() + defer e.RUnlockSettings() - if !e.Settings.Signup { + settings := e.GetSettings() + + if !settings.Signup { httpErr(w, r, http.StatusForbidden, nil) return } + if r.Body == nil { httpErr(w, r, http.StatusBadRequest, nil) return @@ -58,7 +62,7 @@ func (e *Env) signupHandler(w http.ResponseWriter, r *http.Request) { Username: info.Username, } - user.ApplyDefaults(e.Settings.Defaults) + e.ApplyDefaults(user) pwd, err := types.HashPwd(info.Password) if err != nil { @@ -67,7 +71,7 @@ func (e *Env) signupHandler(w http.ResponseWriter, r *http.Request) { } user.Password = pwd - err = e.Store.SaveUser(user) + err = e.SaveUser(user) if err == types.ErrExist { httpErr(w, r, http.StatusConflict, nil) return @@ -115,7 +119,7 @@ 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.Settings.Key, nil + return e.GetSettings().Key, nil } nextWithUser := func(w http.ResponseWriter, r *http.Request, id uint) { @@ -167,7 +171,7 @@ func (e *Env) printToken(w http.ResponseWriter, r *http.Request, user *types.Use } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString(e.Settings.Key) + signed, err := token.SignedString(e.GetSettings().Key) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) diff --git a/http/http.go b/http/http.go index 1be6ee33..3091b6fa 100644 --- a/http/http.go +++ b/http/http.go @@ -5,7 +5,6 @@ import ( "log" "net/http" "strconv" - "sync" "time" "github.com/filebrowser/filebrowser/types" @@ -15,7 +14,6 @@ import ( type key int - const ( keyUserID key = iota ) @@ -27,10 +25,8 @@ type modifyRequest struct { // Env contains the required info for FB to run. type Env struct { - Auther types.Auther - Settings *types.Settings - Store *types.Storage - mux sync.RWMutex // settings mutex for Settings changes. + *types.FileBrowser + Auther types.Auther } // Handler ... @@ -105,7 +101,7 @@ func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) { func (e *Env) getUser(w http.ResponseWriter, r *http.Request) (*types.User, bool) { id := r.Context().Value(keyUserID).(uint) - user, err := e.Store.GetUser(id) + user, err := e.GetUser(id) if err == types.ErrNotExist { httpErr(w, r, http.StatusForbidden, nil) return nil, false diff --git a/http/raw.go b/http/raw.go index ec1b6f71..c1e10478 100644 --- a/http/raw.go +++ b/http/raw.go @@ -67,7 +67,7 @@ func (e *Env) rawHandler(w http.ResponseWriter, r *http.Request) { return } - file, err := types.NewFile(user, path) + file, err := e.NewFile(path, user) if err != nil { httpErr(w, r, httpFsErr(err), err) return diff --git a/http/resource.go b/http/resource.go index e2959ee9..67d6c0e8 100644 --- a/http/resource.go +++ b/http/resource.go @@ -42,11 +42,6 @@ func (e *Env) getResourceData(w http.ResponseWriter, r *http.Request, prefix str path = "/" } - /* TODO if !user.IsAllowed(path) { - httpErr(w, r, http.StatusForbidden, nil) - return "", nil, false - } */ - return path, user, true } @@ -56,7 +51,7 @@ func (e *Env) resourceGetHandler(w http.ResponseWriter, r *http.Request) { return } - file, err := types.NewFile(user, path) + file, err := e.NewFile(path, user) if err != nil { httpErr(w, r, httpFsErr(err), err) return @@ -69,16 +64,13 @@ func (e *Env) resourceGetHandler(w http.ResponseWriter, r *http.Request) { return } - if file.Type == "video" { - file.DetectSubtitles() - } - if !user.Perm.Modify && file.Type == "text" { + // TODO: move to detet file type file.Type = "textImmutable" } if checksum := r.URL.Query().Get("checksum"); checksum != "" { - err = file.Checksum(checksum) + err = e.Checksum(file,user, checksum) if err == types.ErrInvalidOption { httpErr(w, r, http.StatusBadRequest, nil) return @@ -105,7 +97,7 @@ func (e *Env) resourceDeleteHandler(w http.ResponseWriter, r *http.Request) { return } - err := e.Settings.Run(func() error { + err := e.RunHook(func() error { return user.Fs.RemoveAll(path) }, "delete", path, "", user) @@ -156,7 +148,7 @@ func (e *Env) resourcePostPutHandler(w http.ResponseWriter, r *http.Request) { } } - err := e.Settings.Run(func() error { + err := e.RunHook(func() error { file, err := user.Fs.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) if err != nil { return err @@ -222,7 +214,7 @@ func (e *Env) resourcePatchHandler(w http.ResponseWriter, r *http.Request) { } } - err = e.Settings.Run(func() error { + err = e.RunHook(func() error { if action == "copy" { return fileutils.Copy(user.Fs, src, dst) } diff --git a/http/settings.go b/http/settings.go index dcef1721..3d9ca103 100644 --- a/http/settings.go +++ b/http/settings.go @@ -23,16 +23,16 @@ func (e *Env) settingsGetHandler(w http.ResponseWriter, r *http.Request) { return } - e.mux.RLock() - defer e.mux.RUnlock() + e.RLockSettings() + defer e.RUnlockSettings() data := &settingsData{ - Signup: e.Settings.Signup, - Defaults: e.Settings.Defaults, - Rules: e.Settings.Rules, - Branding: e.Settings.Branding, - Shell: e.Settings.Shell, - Commands: e.Settings.Commands, + Signup: e.GetSettings().Signup, + Defaults: e.GetSettings().Defaults, + Rules: e.GetSettings().Rules, + Branding: e.GetSettings().Branding, + Shell: e.GetSettings().Shell, + Commands: e.GetSettings().Commands, } renderJSON(w, r, data) @@ -51,11 +51,11 @@ func (e *Env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { return } - e.mux.Lock() - defer e.mux.Unlock() - + e.RLockSettings() settings := &types.Settings{} - err = copier.Copy(settings, e.Settings) + err = copier.Copy(settings, e.GetSettings()) + e.RUnlockSettings() + if err != nil { httpErr(w, r, http.StatusInternalServerError, err) return @@ -68,12 +68,8 @@ func (e *Env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { settings.Shell = req.Shell settings.Commands = req.Commands - err = e.Store.SaveSettings(settings) + err = e.SaveSettings(settings) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) - return } - - // TODO: do not replace settings, but change fields - e.Settings = settings } diff --git a/http/share.go b/http/share.go index 5df58dad..2ed08d8e 100644 --- a/http/share.go +++ b/http/share.go @@ -33,7 +33,7 @@ func (e *Env) shareGetHandler(w http.ResponseWriter, r *http.Request) { return } - s, err := e.Store.GetLinksByPath(path) + s, err := e.GetLinksByPath(path) if err == types.ErrNotExist { renderJSON(w, r, []*types.ShareLink{}) return @@ -46,7 +46,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.Store.DeleteLink(link.Hash) + e.DeleteLink(link.Hash) s = append(s[:i], s[i+1:]...) } } @@ -72,7 +72,7 @@ func (e *Env) shareDeleteHandler(w http.ResponseWriter, r *http.Request) { return } - err := e.Store.DeleteLink(hash) + err := e.DeleteLink(hash) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) return @@ -91,9 +91,9 @@ func (e *Env) sharePostHandler(w http.ResponseWriter, r *http.Request) { if expire == "" { var err error - s, err = e.Store.GetLinkPermanent(path) + s, err = e.GetLinkPermanent(path) if err == nil { - w.Write([]byte(e.Settings.BaseURL + "/share/" + s.Hash)) + w.Write([]byte(e.GetSettings().BaseURL + "/share/" + s.Hash)) return } } @@ -135,7 +135,7 @@ func (e *Env) sharePostHandler(w http.ResponseWriter, r *http.Request) { s.ExpireDate = time.Now().Add(add) } - if err := e.Store.SaveLink(s); err != nil { + if err := e.SaveLink(s); err != nil { httpErr(w, r, http.StatusInternalServerError, err) return } diff --git a/http/static.go b/http/static.go index 3fcadb0a..52dbe370 100644 --- a/http/static.go +++ b/http/static.go @@ -15,25 +15,27 @@ import ( ) func (e *Env) getStaticData() map[string]interface{} { - e.mux.RLock() - defer e.mux.RUnlock() + e.RLockSettings() + defer e.RUnlockSettings() - staticURL := strings.TrimPrefix(e.Settings.BaseURL+"/static", "/") + settings := e.GetSettings() + + staticURL := strings.TrimPrefix(settings.BaseURL+"/static", "/") data := map[string]interface{}{ - "Name": e.Settings.Branding.Name, - "DisableExternal": e.Settings.Branding.DisableExternal, - "BaseURL": e.Settings.BaseURL, + "Name": settings.Branding.Name, + "DisableExternal": settings.Branding.DisableExternal, + "BaseURL": settings.BaseURL, "Version": types.Version, "StaticURL": staticURL, - "Signup": e.Settings.Signup, - "NoAuth": e.Settings.AuthMethod == auth.MethodNoAuth, + "Signup": settings.Signup, + "NoAuth": settings.AuthMethod == auth.MethodNoAuth, "CSS": false, "ReCaptcha": false, } - if e.Settings.Branding.Files != "" { - path := filepath.Join(e.Settings.Branding.Files, "custom.css") + if settings.Branding.Files != "" { + path := filepath.Join(settings.Branding.Files, "custom.css") _, err := os.Stat(path) if err != nil && !os.IsNotExist(err) { @@ -45,7 +47,7 @@ func (e *Env) getStaticData() map[string]interface{} { } } - if e.Settings.AuthMethod == auth.MethodJSONAuth { + if settings.AuthMethod == auth.MethodJSONAuth { auther := e.Auther.(*auth.JSONAuth) if auther.ReCaptcha != nil { @@ -88,18 +90,18 @@ func (e *Env) getStaticHandlers() (http.Handler, http.Handler) { }) static := http.StripPrefix("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - e.mux.RLock() - defer e.mux.RUnlock() + e.RLockSettings() + defer e.RUnlockSettings() - if e.Settings.Branding.Files != "" { + if e.GetSettings().Branding.Files != "" { if strings.HasPrefix(r.URL.Path, "img/") { - path := filepath.Join(e.Settings.Branding.Files, r.URL.Path) + path := filepath.Join(e.GetSettings().Branding.Files, r.URL.Path) if _, err := os.Stat(path); err == nil { http.ServeFile(w, r, path) return } - } else if r.URL.Path == "custom.css" && e.Settings.Branding.Files != "" { - http.ServeFile(w, r, filepath.Join(e.Settings.Branding.Files, "custom.css")) + } else if r.URL.Path == "custom.css" && e.GetSettings().Branding.Files != "" { + http.ServeFile(w, r, filepath.Join(e.GetSettings().Branding.Files, "custom.css")) return } } diff --git a/http/users.go b/http/users.go index 54499707..be25eb71 100644 --- a/http/users.go +++ b/http/users.go @@ -57,7 +57,7 @@ func (e *Env) usersGetHandler(w http.ResponseWriter, r *http.Request) { return } - users, err := e.Store.GetUsers() + users, err := e.GetUsers() if err != nil { httpErr(w, r, http.StatusInternalServerError, err) return @@ -100,7 +100,7 @@ func (e *Env) userGetHandler(w http.ResponseWriter, r *http.Request) { return } - u, err := e.Store.GetUser(id) + u, err := e.GetUser(id) if err == types.ErrNotExist { httpErr(w, r, http.StatusNotFound, nil) return @@ -121,7 +121,7 @@ func (e *Env) userDeleteHandler(w http.ResponseWriter, r *http.Request) { return } - err := e.Store.DeleteUser(id) + err := e.DeleteUser(id) if err == types.ErrNotExist { httpErr(w, r, http.StatusNotFound, nil) return @@ -160,7 +160,7 @@ func (e *Env) userPostHandler(w http.ResponseWriter, r *http.Request) { return } - err = e.Store.SaveUser(req.Data) + err = e.SaveUser(req.Data) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) return @@ -198,7 +198,7 @@ func (e *Env) userPutHandler(w http.ResponseWriter, r *http.Request) { req.Data.Password, err = types.HashPwd(req.Data.Password) } else { var suser *types.User - suser, err = e.Store.GetUser(modifiedID) + suser, err = e.GetUser(modifiedID) req.Data.Password = suser.Password } @@ -232,7 +232,7 @@ func (e *Env) userPutHandler(w http.ResponseWriter, r *http.Request) { req.Which[k] = strings.Title(v) } - err = e.Store.UpdateUser(req.Data, req.Which...) + err = e.UpdateUser(req.Data, req.Which...) if err != nil { httpErr(w, r, http.StatusInternalServerError, err) } diff --git a/http/websockets.go b/http/websockets.go index fbe2552a..cfd2fd4b 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -60,10 +60,7 @@ func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { return } - e.mux.RLock() - command, err := e.Settings.ParseCommand(raw) - e.mux.RUnlock() - + command, err := e.ParseCommand(raw) if err != nil { err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())) if err != nil { diff --git a/types/auth.go b/types/auth.go index 34c40559..00c2ed1e 100644 --- a/types/auth.go +++ b/types/auth.go @@ -2,10 +2,13 @@ package types 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) - // SetStorage gives the Auther the storage. - SetStorage(*Storage) + // SetInstance attaches the File Browser instance. + SetInstance(*FileBrowser) } diff --git a/types/filebrowser.go b/types/filebrowser.go new file mode 100644 index 00000000..384abcef --- /dev/null +++ b/types/filebrowser.go @@ -0,0 +1,385 @@ +package types + +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", +} + +type FileBrowser struct { + settings *Settings + storage StorageBackend + mux sync.RWMutex +} + +func (f *FileBrowser) RLockSettings() { + f.mux.RLock() +} + +func (f *FileBrowser) RUnlockSettings() { + f.mux.RUnlock() +} + +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 +} + +// 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 +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 defaults to a user. +func (f *FileBrowser) ApplyDefaults(u *User) { + f.mux.RLock() + 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.mux.RUnlock() +} + +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 +} + +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) + } + + 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/types/files.go b/types/files.go index e4c47668..da003d00 100644 --- a/types/files.go +++ b/types/files.go @@ -1,27 +1,16 @@ package types import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/hex" - "hash" - "io" - "mime" - "net/http" "os" - "path/filepath" - "strings" + "sort" "time" - "github.com/spf13/afero" + "github.com/maruel/natural" ) // File describes a file. type File struct { *Listing - user *User Path string `json:"path"` Name string `json:"name"` Size int64 `json:"size"` @@ -35,200 +24,102 @@ type File struct { Checksums map[string]string `json:"checksums,omitempty"` } -// NewFile generates a new file info from a user and a path. -func NewFile(u *User, path string) (*File, error) { - f := &File{ - Path: path, - } - - i, err := u.Fs.Stat(path) - if err != nil { - return f, err - } - - f.user = u - f.Name = i.Name() - f.ModTime = i.ModTime() - f.Mode = i.Mode() - f.IsDir = i.IsDir() - f.Size = i.Size() - f.Extension = filepath.Ext(f.Name) - - if f.IsDir { - err = f.getDirInfo() - } else { - err = f.detectFileType() - } - - return f, err +// 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"` } -// Checksum retrieves the checksum of a file. -func (f *File) Checksum(algo string) error { - if f.IsDir { - return ErrIsDirectory - } - - if f.Checksums == nil { - f.Checksums = map[string]string{} - } - - i, err := f.user.Fs.Open(f.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 - } - - f.Checksums[algo] = hex.EncodeToString(h.Sum(nil)) - return nil -} - -func (f *File) getDirInfo() error { - afs := &afero.Afero{Fs: f.user.Fs} - files, err := afs.ReadDir(f.Path) - if err != nil { - return err - } - - f.Listing = &Listing{ - Items: []*File{}, - NumDirs: 0, - NumFiles: 0, - } - - for _, i := range files { - name := i.Name() - path := filepath.Join(f.Path, name) - - 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 - } +// ApplySort applies the sort order using .Order and .Sort +func (l Listing) ApplySort() { + // Check '.Order' to know how to sort + if !l.Sorting.Asc { + switch l.Sorting.By { + case "name": + sort.Sort(sort.Reverse(byName(l))) + case "size": + sort.Sort(sort.Reverse(bySize(l))) + case "modified": + sort.Sort(sort.Reverse(byModified(l))) + default: + // If not one of the above, do nothing + return } - - file := &File{ - user: f.user, - Name: name, - Size: i.Size(), - ModTime: i.ModTime(), - Mode: i.Mode(), - IsDir: i.IsDir(), - Extension: filepath.Ext(name), - Path: path, - } - - if file.IsDir { - f.Listing.NumDirs++ - } else { - f.Listing.NumFiles++ - - err := file.detectFileType() - if err != nil { - return err - } - } - - f.Listing.Items = append(f.Listing.Items, file) - } - - return nil -} - -func (f *File) detectFileType() error { - i, err := f.user.Fs.Open(f.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(f.Extension) - if mimetype == "" { - mimetype = http.DetectContentType(buffer[:n]) - } - - switch { - case strings.HasPrefix(mimetype, "video"): - f.Type = "video" - return nil - case strings.HasPrefix(mimetype, "audio"): - f.Type = "audio" - return nil - case strings.HasPrefix(mimetype, "image"): - f.Type = "image" - return nil - case isBinary(string(buffer[:n])) || f.Size > 10*1024*1024: // 10 MB - f.Type = "blob" - return nil - default: - f.Type = "text" - afs := &afero.Afero{Fs: f.user.Fs} - content, err := afs.ReadFile(f.Path) - if err != nil { - return err - } - f.Content = string(content) - } - - return nil -} - -var ( - subtitleExts = []string{ - ".vtt", - } -) - -// DetectSubtitles fills the subtitles field if the file -// is a movie. -func (f *File) DetectSubtitles() { - f.Subtitles = []string{} - ext := filepath.Ext(f.Path) - base := strings.TrimSuffix(f.Path, ext) - - for _, ext := range subtitleExts { - path := base + ext - if _, err := f.user.Fs.Stat(path); err == nil { - f.Subtitles = append(f.Subtitles, path) + } else { // If we had more Orderings we could add them here + switch l.Sorting.By { + case "name": + sort.Sort(byName(l)) + case "size": + sort.Sort(bySize(l)) + case "modified": + sort.Sort(byModified(l)) + default: + sort.Sort(byName(l)) + return } } } -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 +// Implement sorting for Listing +type byName Listing +type bySize Listing +type byModified Listing + +// By Name +func (l byName) Len() int { + return len(l.Items) +} + +func (l byName) Swap(i, j int) { + l.Items[i], l.Items[j] = l.Items[j], l.Items[i] +} + +// Treat upper and lower case equally +func (l byName) Less(i, j int) bool { + if l.Items[i].IsDir && !l.Items[j].IsDir { + return true + } + + if !l.Items[i].IsDir && l.Items[j].IsDir { + return false + } + + return natural.Less(l.Items[i].Name, l.Items[j].Name) +} + +// By Size +func (l bySize) Len() int { + return len(l.Items) +} + +func (l bySize) Swap(i, j int) { + l.Items[i], l.Items[j] = l.Items[j], l.Items[i] +} + +const directoryOffset = -1 << 31 // = math.MinInt32 +func (l bySize) Less(i, j int) bool { + iSize, jSize := l.Items[i].Size, l.Items[j].Size + if l.Items[i].IsDir { + iSize = directoryOffset + iSize + } + if l.Items[j].IsDir { + jSize = directoryOffset + jSize + } + return iSize < jSize +} + +// By Modified +func (l byModified) Len() int { + return len(l.Items) +} + +func (l byModified) Swap(i, j int) { + l.Items[i], l.Items[j] = l.Items[j], l.Items[i] +} + +func (l byModified) Less(i, j int) bool { + iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime + return iModified.Sub(jModified) < 0 } diff --git a/types/fs.go b/types/fs.go deleted file mode 100644 index accb4c48..00000000 --- a/types/fs.go +++ /dev/null @@ -1,222 +0,0 @@ -package types - -import ( - "os" - "path" - "syscall" - "time" - - "github.com/spf13/afero" -) - -type userFs struct { - source afero.Fs - user *User - settings *Settings -} - -type userFile struct { - f afero.File - path string - fs *userFs -} - -func (u *userFs) isAllowed(name string) bool { - if !isAllowed(name, u.user.Rules) { - return false - } - - return isAllowed(name, u.settings.Rules) -} - -func (u *userFs) FullPath(path string) string { - return afero.FullBaseFsPath(u.source.(*afero.BasePathFs), path) -} - -func (u *userFs) Chtimes(name string, a, m time.Time) error { - if !u.isAllowed(name) { - return syscall.ENOENT - } - - return u.source.Chtimes(name, a, m) -} - -func (u *userFs) Chmod(name string, mode os.FileMode) error { - if !u.isAllowed(name) { - return syscall.ENOENT - } - - return u.source.Chmod(name, mode) -} - -func (u *userFs) Name() string { - return "userFs" -} - -func (u *userFs) Stat(name string) (os.FileInfo, error) { - if !u.isAllowed(name) { - return nil, syscall.ENOENT - } - - return u.source.Stat(name) -} - -func (u *userFs) Rename(oldname, newname string) error { - if !u.user.Perm.Rename { - return os.ErrPermission - } - - if !u.isAllowed(oldname) || !u.isAllowed(newname) { - return syscall.ENOENT - } - - return u.source.Rename(oldname, newname) -} - -func (u *userFs) RemoveAll(name string) error { - if !u.user.Perm.Delete { - return os.ErrPermission - } - - if !u.isAllowed(name) { - return syscall.ENOENT - } - - return u.source.RemoveAll(name) -} - -func (u *userFs) Remove(name string) error { - if !u.user.Perm.Delete { - return os.ErrPermission - } - - if !u.isAllowed(name) { - return syscall.ENOENT - } - - return u.source.Remove(name) -} - -func (u *userFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { - if !u.isAllowed(name) { - return nil, syscall.ENOENT - } - - return u.source.OpenFile(name, flag, perm) -} - -func (u *userFs) Open(name string) (afero.File, error) { - if !u.isAllowed(name) { - return nil, syscall.ENOENT - } - - f, err := u.source.Open(name) - return &userFile{fs: u, path: name, f: f}, err -} - -func (u *userFs) Mkdir(name string, perm os.FileMode) error { - if !u.user.Perm.Create { - return os.ErrPermission - } - - if !u.isAllowed(name) { - return syscall.ENOENT - } - - return u.source.Mkdir(name, perm) -} - -func (u *userFs) MkdirAll(name string, perm os.FileMode) error { - if !u.user.Perm.Create { - return os.ErrPermission - } - - if !u.isAllowed(name) { - return syscall.ENOENT - } - - return u.source.MkdirAll(name, perm) -} - -func (u *userFs) Create(name string) (afero.File, error) { - if !u.user.Perm.Create { - return nil, os.ErrPermission - } - - if !u.isAllowed(name) { - return nil, syscall.ENOENT - } - - return u.source.Create(name) -} - -func (f *userFile) Close() error { - return f.f.Close() -} - -func (f *userFile) Read(s []byte) (int, error) { - return f.f.Read(s) -} - -func (f *userFile) ReadAt(s []byte, o int64) (int, error) { - return f.f.ReadAt(s, o) -} - -func (f *userFile) Seek(o int64, w int) (int64, error) { - return f.f.Seek(o, w) -} - -func (f *userFile) Write(s []byte) (int, error) { - return f.f.Write(s) -} - -func (f *userFile) WriteAt(s []byte, o int64) (int, error) { - return f.f.WriteAt(s, o) -} - -func (f *userFile) Name() string { - return f.f.Name() -} - -func (f *userFile) Readdir(c int) (fi []os.FileInfo, err error) { - var rfi []os.FileInfo - rfi, err = f.f.Readdir(c) - if err != nil { - return nil, err - } - for _, i := range rfi { - if f.fs.isAllowed(path.Join(f.path, i.Name())) { - fi = append(fi, i) - } - } - return fi, nil -} - -func (f *userFile) Readdirnames(c int) (n []string, err error) { - fi, err := f.Readdir(c) - if err != nil { - return nil, err - } - for _, s := range fi { - if f.fs.isAllowed(s.Name()) { - n = append(n, s.Name()) - } - } - return n, nil -} - -func (f *userFile) Stat() (os.FileInfo, error) { - return f.f.Stat() -} - -func (f *userFile) Sync() error { - return f.f.Sync() -} - -func (f *userFile) Truncate(s int64) error { - return f.f.Truncate(s) -} - -func (f *userFile) WriteString(s string) (int, error) { - return f.f.WriteString(s) -} diff --git a/types/listing.go b/types/listing.go deleted file mode 100644 index ecfc8c90..00000000 --- a/types/listing.go +++ /dev/null @@ -1,107 +0,0 @@ -package types - -import ( - "sort" - - "github.com/maruel/natural" -) - -// 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"` -} - -// ApplySort applies the sort order using .Order and .Sort -func (l Listing) ApplySort() { - // Check '.Order' to know how to sort - if !l.Sorting.Asc { - switch l.Sorting.By { - case "name": - sort.Sort(sort.Reverse(byName(l))) - case "size": - sort.Sort(sort.Reverse(bySize(l))) - case "modified": - sort.Sort(sort.Reverse(byModified(l))) - default: - // If not one of the above, do nothing - return - } - } else { // If we had more Orderings we could add them here - switch l.Sorting.By { - case "name": - sort.Sort(byName(l)) - case "size": - sort.Sort(bySize(l)) - case "modified": - sort.Sort(byModified(l)) - default: - sort.Sort(byName(l)) - return - } - } -} - -// Implement sorting for Listing -type byName Listing -type bySize Listing -type byModified Listing - -// By Name -func (l byName) Len() int { - return len(l.Items) -} - -func (l byName) Swap(i, j int) { - l.Items[i], l.Items[j] = l.Items[j], l.Items[i] -} - -// Treat upper and lower case equally -func (l byName) Less(i, j int) bool { - if l.Items[i].IsDir && !l.Items[j].IsDir { - return true - } - - if !l.Items[i].IsDir && l.Items[j].IsDir { - return false - } - - return natural.Less(l.Items[i].Name, l.Items[j].Name) -} - -// By Size -func (l bySize) Len() int { - return len(l.Items) -} - -func (l bySize) Swap(i, j int) { - l.Items[i], l.Items[j] = l.Items[j], l.Items[i] -} - -const directoryOffset = -1 << 31 // = math.MinInt32 -func (l bySize) Less(i, j int) bool { - iSize, jSize := l.Items[i].Size, l.Items[j].Size - if l.Items[i].IsDir { - iSize = directoryOffset + iSize - } - if l.Items[j].IsDir { - jSize = directoryOffset + jSize - } - return iSize < jSize -} - -// By Modified -func (l byModified) Len() int { - return len(l.Items) -} - -func (l byModified) Swap(i, j int) { - l.Items[i], l.Items[j] = l.Items[j], l.Items[i] -} - -func (l byModified) Less(i, j int) bool { - iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime - return iModified.Sub(jModified) < 0 -} diff --git a/types/rule.go b/types/rule.go index 3de537ca..c4c36462 100644 --- a/types/rule.go +++ b/types/rule.go @@ -13,6 +13,15 @@ type Rule struct { Regexp *Regexp `json:"regexp"` } +// Matches matches a path against a rule. +func (r *Rule) Matches(path string) bool { + if r.Regex { + return r.Regexp.MatchString(path) + } + + return strings.HasPrefix(path, r.Path) +} + // Regexp is a wrapper to the native regexp type where we // save the raw expression. type Regexp struct { @@ -28,17 +37,3 @@ func (r *Regexp) MatchString(s string) bool { return r.regexp.MatchString(s) } - -func isAllowed(path string, rules []Rule) bool { - for _, rule := range rules { - if rule.Regex { - if rule.Regexp.MatchString(path) { - return rule.Allow - } - } else if strings.HasPrefix(path, rule.Path) { - return rule.Allow - } - } - - return true -} diff --git a/types/runner.go b/types/runner.go deleted file mode 100644 index 478da61d..00000000 --- a/types/runner.go +++ /dev/null @@ -1,107 +0,0 @@ -package types - -import ( - "fmt" - "log" - "os" - "os/exec" - "strings" - - "github.com/mholt/caddy" -) - -var defaultEvents = []string{ - "save", - "copy", - "rename", - "upload", - "delete", -} - -// Run runs the hooks for the before and after event. -func (s *Settings) Run(fn func() error, evt, path, dst string, user *User) error { - path = user.FullPath(path) - dst = user.FullPath(dst) - - if val, ok := s.Commands["before_"+evt]; ok { - for _, command := range val { - err := s.exec(command, "before_"+evt, path, dst, user) - if err != nil { - return err - } - } - } - - err := fn() - if err != nil { - return err - } - - if val, ok := s.Commands["after_"+evt]; ok { - for _, command := range val { - err := s.exec(command, "after_"+evt, path, dst, user) - if err != nil { - return err - } - } - } - - return nil -} - -// ParseCommand parses the command taking in account -func (s *Settings) ParseCommand(raw string) ([]string, error) { - command := []string{} - - if len(s.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(s.Shell, raw) - } - - return command, nil -} - -func (s *Settings) 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 := s.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/types/settings.go b/types/settings.go index b382fcd4..067fdfa3 100644 --- a/types/settings.go +++ b/types/settings.go @@ -1,8 +1,5 @@ package types -// AuthMethod is an authentication method. -type AuthMethod string - // Settings contain the main settings of the application. type Settings struct { Key []byte `json:"key"` diff --git a/types/storage.go b/types/storage.go index 40e42c4b..06c5dc03 100644 --- a/types/storage.go +++ b/types/storage.go @@ -24,21 +24,10 @@ type StorageBackend interface { DeleteLink(hash string) error } -// Storage implements Storage interface and verifies -// the data before getting in and out the database. -type Storage struct { - src StorageBackend -} - -// NewStorage creates a Storage from a StorageBackend. -func NewStorage(src StorageBackend) *Storage { - return &Storage{src: src} -} - // 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 (v *Storage) GetUser(id interface{}) (*User, error) { +func (f *FileBrowser) GetUser(id interface{}) (*User, error) { var ( user *User err error @@ -46,9 +35,9 @@ func (v *Storage) GetUser(id interface{}) (*User, error) { switch id.(type) { case string: - user, err = v.src.GetUserByUsername(id.(string)) + user, err = f.storage.GetUserByUsername(id.(string)) case uint: - user, err = v.src.GetUserByID(id.(uint)) + user, err = f.storage.GetUserByID(id.(uint)) default: return nil, ErrInvalidDataType } @@ -57,72 +46,52 @@ func (v *Storage) GetUser(id interface{}) (*User, error) { return nil, err } - settings, err := v.GetSettings() - if err != nil { - return nil, err - } - - user.clean(settings) + user.clean() return user, err } // GetUsers gets a list of all users. -func (v *Storage) GetUsers() ([]*User, error) { - users, err := v.src.GetUsers() - if err != nil { - return nil, err - } - - settings, err := v.GetSettings() +func (f *FileBrowser) GetUsers() ([]*User, error) { + users, err := f.storage.GetUsers() if err != nil { return nil, err } for _, user := range users { - user.clean(settings) + user.clean() } return users, err } // UpdateUser updates a user in the database. -func (v *Storage) UpdateUser(user *User, fields ...string) error { - settings, err := v.GetSettings() +func (f *FileBrowser) UpdateUser(user *User, fields ...string) error { + err := user.clean(fields...) if err != nil { return err } - err = user.clean(settings, fields...) - if err != nil { - return err - } - - return v.src.UpdateUser(user, fields...) + return f.storage.UpdateUser(user, fields...) } // SaveUser saves the user in a storage. -func (v *Storage) SaveUser(user *User) error { - settings, err := v.GetSettings() - if err != nil { +func (f *FileBrowser) SaveUser(user *User) error { + if err := user.clean(); err != nil { return err } - if err := user.clean(settings); err != nil { - return err - } - - return v.src.SaveUser(user) + 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 (v *Storage) DeleteUser(id interface{}) (err error) { +func (f *FileBrowser) DeleteUser(id interface{}) (err error) { switch id.(type) { case string: - err = v.src.DeleteUserByUsername(id.(string)) + err = f.storage.DeleteUserByUsername(id.(string)) case uint: - err = v.src.DeleteUserByID(id.(uint)) + err = f.storage.DeleteUserByID(id.(uint)) default: err = ErrInvalidDataType } @@ -130,13 +99,11 @@ func (v *Storage) DeleteUser(id interface{}) (err error) { return } -// GetSettings wraps a ConfigStore.GetSettings -func (v *Storage) GetSettings() (*Settings, error) { - return v.src.GetSettings() +func (f *FileBrowser) GetSettings() *Settings { + return f.settings } -// SaveSettings wraps a ConfigStore.SaveSettings -func (v *Storage) SaveSettings(s *Settings) error { +func (f *FileBrowser) SaveSettings(s *Settings) error { s.BaseURL = strings.TrimSuffix(s.BaseURL, "/") if len(s.Key) == 0 { @@ -177,46 +144,54 @@ func (v *Storage) SaveSettings(s *Settings) error { } } - return v.src.SaveSettings(s) + err := f.storage.SaveSettings(s) + if err != nil { + return err + } + + f.mux.Lock() + f.settings = s + f.mux.Unlock() + return nil } -// GetAuther wraps a ConfigStore.GetAuther -func (v *Storage) GetAuther(t AuthMethod) (Auther, error) { - auther, err := v.src.GetAuther(t) +// GetAuther wraps a Storage.GetAuther +func (f *FileBrowser) GetAuther(t AuthMethod) (Auther, error) { + auther, err := f.storage.GetAuther(t) if err != nil { return nil, err } - auther.SetStorage(v) + auther.SetInstance(f) return auther, nil } -// SaveAuther wraps a ConfigStore.SaveAuther -func (v *Storage) SaveAuther(a Auther) error { - return v.src.SaveAuther(a) +// SaveAuther wraps a Storage.SaveAuther +func (f *FileBrowser) SaveAuther(a Auther) error { + return f.storage.SaveAuther(a) } // GetLinkByHash wraps a Storage.GetLinkByHash. -func (v *Storage) GetLinkByHash(hash string) (*ShareLink, error) { - return v.src.GetLinkByHash(hash) +func (f *FileBrowser) GetLinkByHash(hash string) (*ShareLink, error) { + return f.storage.GetLinkByHash(hash) } // GetLinkPermanent wraps a Storage.GetLinkPermanent -func (v *Storage) GetLinkPermanent(path string) (*ShareLink, error) { - return v.src.GetLinkPermanent(path) +func (f *FileBrowser) GetLinkPermanent(path string) (*ShareLink, error) { + return f.storage.GetLinkPermanent(path) } // GetLinksByPath wraps a Storage.GetLinksByPath -func (v *Storage) GetLinksByPath(path string) ([]*ShareLink, error) { - return v.src.GetLinksByPath(path) +func (f *FileBrowser) GetLinksByPath(path string) ([]*ShareLink, error) { + return f.storage.GetLinksByPath(path) } // SaveLink wraps a Storage.SaveLink -func (v *Storage) SaveLink(s *ShareLink) error { - return v.src.SaveLink(s) +func (f *FileBrowser) SaveLink(s *ShareLink) error { + return f.storage.SaveLink(s) } // DeleteLink wraps a Storage.DeleteLink -func (v *Storage) DeleteLink(hash string) error { - return v.src.DeleteLink(hash) +func (f *FileBrowser) DeleteLink(hash string) error { + return f.storage.DeleteLink(hash) } diff --git a/types/user.go b/types/user.go index 7c11297d..4934f259 100644 --- a/types/user.go +++ b/types/user.go @@ -5,7 +5,6 @@ import ( "regexp" "github.com/spf13/afero" - "golang.org/x/crypto/bcrypt" ) // ViewMode describes a view mode. @@ -54,7 +53,7 @@ var checkableFields = []string{ "Rules", } -func (u *User) clean(settings *Settings, fields ...string) error { +func (u *User) clean(fields ...string) error { if len(fields) == 0 { fields = checkableFields } @@ -93,11 +92,7 @@ func (u *User) clean(settings *Settings, fields ...string) error { } if u.Fs == nil { - u.Fs = &userFs{ - user: u, - settings: settings, - source: afero.NewBasePathFs(afero.NewOsFs(), u.Scope), - } + u.Fs = afero.NewBasePathFs(afero.NewOsFs(), u.Scope) } return nil @@ -105,7 +100,7 @@ func (u *User) clean(settings *Settings, fields ...string) error { // FullPath gets the full path for a user's relative path. func (u *User) FullPath(path string) string { - return u.Fs.(*userFs).FullPath(path) + return afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path) } // CanExecute checks if an user can execute a specific command. @@ -122,25 +117,3 @@ func (u *User) CanExecute(command string) bool { return false } - -// ApplyDefaults applies defaults to a user. -func (u *User) ApplyDefaults(defaults UserDefaults) { - u.Scope = defaults.Scope - u.Locale = defaults.Locale - u.ViewMode = defaults.ViewMode - u.Perm = defaults.Perm - u.Sorting = defaults.Sorting - u.Commands = defaults.Commands -} - -// 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/types/utils.go b/types/utils.go new file mode 100644 index 00000000..14b48afd --- /dev/null +++ b/types/utils.go @@ -0,0 +1,37 @@ +package types + +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 +}