feat: global rules
License: MIT Signed-off-by: Henrique Dias <hacdias@gmail.com>
This commit is contained in:
parent
01a7056419
commit
6811f205b4
10
auth/json.go
10
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"
|
||||
|
||||
10
auth/none.go
10
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
|
||||
}
|
||||
|
||||
@ -12,13 +12,13 @@ const MethodProxyAuth types.AuthMethod = "proxy"
|
||||
// ProxyAuth is a proxy implementation of an auther.
|
||||
type ProxyAuth struct {
|
||||
Header string
|
||||
store *types.Storage
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
},
|
||||
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
46
cmd/root.go
46
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,12 +88,16 @@ 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{
|
||||
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{
|
||||
@ -112,9 +110,14 @@ func quickSetup(cmd *cobra.Command) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
13
cmd/utils.go
13
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
|
||||
}
|
||||
|
||||
1
go.sum
1
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=
|
||||
|
||||
18
http/auth.go
18
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)
|
||||
|
||||
@ -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 {
|
||||
*types.FileBrowser
|
||||
Auther types.Auther
|
||||
Settings *types.Settings
|
||||
Store *types.Storage
|
||||
mux sync.RWMutex // settings mutex for Settings changes.
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
385
types/filebrowser.go
Normal 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()
|
||||
}
|
||||
285
types/files.go
285
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()
|
||||
// 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:
|
||||
return ErrInvalidOption
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
|
||||
_, 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
} 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:
|
||||
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)
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
222
types/fs.go
222
types/fs.go
@ -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)
|
||||
}
|
||||
107
types/listing.go
107
types/listing.go
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
107
types/runner.go
107
types/runner.go
@ -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()
|
||||
}
|
||||
@ -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"`
|
||||
|
||||
115
types/storage.go
115
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)
|
||||
}
|
||||
|
||||
@ -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
37
types/utils.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user