feat: global rules

License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>
This commit is contained in:
Henrique Dias 2019-01-03 23:29:33 +00:00
parent 01a7056419
commit 6811f205b4
36 changed files with 731 additions and 946 deletions

View File

@ -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"

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
},

View File

@ -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 == "" {

View File

@ -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")

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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")

View File

@ -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})

View File

@ -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")

View File

@ -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")

View File

@ -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
}

1
go.sum
View File

@ -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=

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}

385
types/filebrowser.go Normal file
View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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"`

View File

@ -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)
}

View File

@ -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
}

37
types/utils.go Normal file
View File

@ -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
}