License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>
This commit is contained in:
Henrique Dias 2019-01-04 12:30:52 +00:00
parent 27e12c687b
commit 18ad9d78b3
53 changed files with 1057 additions and 889 deletions

185
_remove/filebrowser.go Normal file
View File

@ -0,0 +1,185 @@
package lib
/*
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"log"
"mime"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"github.com/filebrowser/filebrowser/files"
"github.com/filebrowser/filebrowser/rules"
"github.com/filebrowser/filebrowser/settings"
"github.com/filebrowser/filebrowser/users"
"github.com/mholt/caddy"
"github.com/spf13/afero"
)
// FileBrowser represents a File Browser instance which must
// be created through NewFileBrowser.
type FileBrowser struct {
settings *settings.Settings
mux sync.RWMutex
}
// NewFileBrowser creates a new File Browser instance from a
// storage backend. If that backend doesn't contain settings
// on it (returns ErrNotExist), then we generate a new key
// and base settings.
func NewFileBrowser(backend StorageBackend) (*FileBrowser, error) {
set, err := backend.GetSettings()
if err == ErrNotExist {
var key []byte
key, err = generateRandomBytes(64)
if err != nil {
return nil, err
}
set = &settings.Settings{Key: key}
err = backend.SaveSettings(set)
}
if err != nil {
return nil, err
}
return &FileBrowser{
settings: set,
storage: backend,
}, nil
}
// RLockSettings locks the settings for reading.
func (f *FileBrowser) RLockSettings() {
f.mux.RLock()
}
// RUnlockSettings unlocks the settings for reading.
func (f *FileBrowser) RUnlockSettings() {
f.mux.RUnlock()
}
// CheckRules matches the rules against user rules and global rules.
func (f *FileBrowser) CheckRules(path string, user *users.User) bool {
f.RLockSettings()
val := rules.Check(path, user, f.settings)
f.RUnlockSettings()
return val
}
// RunHook runs the hooks for the before and after event.
func (f *FileBrowser) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
path = user.FullPath(path)
dst = user.FullPath(dst)
if val, ok := f.settings.Commands["before_"+evt]; ok {
for _, command := range val {
err := f.exec(command, "before_"+evt, path, dst, user)
if err != nil {
return err
}
}
}
err := fn()
if err != nil {
return err
}
if val, ok := f.settings.Commands["after_"+evt]; ok {
for _, command := range val {
err := f.exec(command, "after_"+evt, path, dst, user)
if err != nil {
return err
}
}
}
return nil
}
// ParseCommand parses the command taking in account if the current
// instance uses a shell to run the commands or just calls the binary
// directyly.
func (f *FileBrowser) ParseCommand(raw string) ([]string, error) {
f.RLockSettings()
defer f.RUnlockSettings()
command := []string{}
if len(f.settings.Shell) == 0 {
cmd, args, err := caddy.SplitCommandAndArgs(raw)
if err != nil {
return nil, err
}
_, err = exec.LookPath(cmd)
if err != nil {
return nil, err
}
command = append(command, cmd)
command = append(command, args...)
} else {
command = append(f.settings.Shell, raw)
}
return command, nil
}
func (f *FileBrowser) exec(raw, evt, path, dst string, user *users.User) error {
blocking := true
if strings.HasSuffix(raw, "&") {
blocking = false
raw = strings.TrimSpace(strings.TrimSuffix(raw, "&"))
}
command, err := f.ParseCommand(raw)
if err != nil {
return err
}
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope))
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if !blocking {
log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " "))
return cmd.Start()
}
log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " "))
return cmd.Run()
}
*/

15
auth/auth.go Normal file
View File

@ -0,0 +1,15 @@
package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/users"
)
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
Auth(*http.Request) (*users.User, error)
// SetStorage attaches the Storage instance.
SetStorage(*users.Storage)
}

View File

@ -4,13 +4,15 @@ import (
"encoding/json"
"net/http"
"net/url"
"os"
"strings"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/settings"
"github.com/filebrowser/filebrowser/users"
)
// MethodJSONAuth is used to identify json auth.
const MethodJSONAuth lib.AuthMethod = "json"
const MethodJSONAuth settings.AuthMethod = "json"
type jsonCred struct {
Password string `json:"password"`
@ -21,20 +23,20 @@ type jsonCred struct {
// JSONAuth is a json implementaion of an Auther.
type JSONAuth struct {
ReCaptcha *ReCaptcha
instance *lib.FileBrowser
storage *users.Storage
}
// Auth authenticates the user via a json in content body.
func (a *JSONAuth) Auth(r *http.Request) (*lib.User, error) {
func (a *JSONAuth) Auth(r *http.Request) (*users.User, error) {
var cred jsonCred
if r.Body == nil {
return nil, lib.ErrNoPermission
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, lib.ErrNoPermission
return nil, os.ErrPermission
}
// If ReCaptcha is enabled, check the code.
@ -46,21 +48,21 @@ func (a *JSONAuth) Auth(r *http.Request) (*lib.User, error) {
}
if !ok {
return nil, lib.ErrNoPermission
return nil, os.ErrPermission
}
}
u, err := a.instance.GetUser(cred.Username)
if err != nil || !lib.CheckPwd(cred.Password, u.Password) {
return nil, lib.ErrNoPermission
u, err := a.storage.Get(cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
return nil, os.ErrPermission
}
return u, nil
}
// SetInstance attaches the instance to the auther.
func (a *JSONAuth) SetInstance(i *lib.FileBrowser) {
a.instance = i
// SetStorage attaches the storage to the auther.
func (a *JSONAuth) SetStorage(s *users.Storage) {
a.storage = s
}
const reCaptchaAPI = "/recaptcha/api/siteverify"

View File

@ -3,23 +3,24 @@ package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/settings"
"github.com/filebrowser/filebrowser/users"
)
// MethodNoAuth is used to identify no auth.
const MethodNoAuth lib.AuthMethod = "noauth"
const MethodNoAuth settings.AuthMethod = "noauth"
// NoAuth is no auth implementation of auther.
type NoAuth struct {
instance *lib.FileBrowser
storage *users.Storage
}
// Auth uses authenticates user 1.
func (a *NoAuth) Auth(r *http.Request) (*lib.User, error) {
return a.instance.GetUser(1)
func (a *NoAuth) Auth(r *http.Request) (*users.User, error) {
return a.storage.Get(1)
}
// SetInstance attaches the instance to the auther.
func (a *NoAuth) SetInstance(i *lib.FileBrowser) {
a.instance = i
// SetStorage attaches the storage to the auther.
func (a *NoAuth) SetStorage(s *users.Storage) {
a.storage = s
}

View File

@ -2,31 +2,34 @@ package auth
import (
"net/http"
"os"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/settings"
"github.com/filebrowser/filebrowser/users"
"github.com/filebrowser/filebrowser/errors"
)
// MethodProxyAuth is used to identify no auth.
const MethodProxyAuth lib.AuthMethod = "proxy"
const MethodProxyAuth settings.AuthMethod = "proxy"
// ProxyAuth is a proxy implementation of an auther.
type ProxyAuth struct {
Header string
instance *lib.FileBrowser
storage *users.Storage
}
// Auth authenticates the user via an HTTP header.
func (a *ProxyAuth) Auth(r *http.Request) (*lib.User, error) {
func (a *ProxyAuth) Auth(r *http.Request) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := a.instance.GetUser(username)
if err == lib.ErrNotExist {
return nil, lib.ErrNoPermission
user, err := a.storage.Get(username)
if err == errors.ErrNotExist {
return nil, os.ErrPermission
}
return user, err
}
// SetInstance attaches the instance to the auther.
func (a *ProxyAuth) SetInstance(i *lib.FileBrowser) {
a.instance = i
// SetStorage attaches the storage to the auther.
func (a *ProxyAuth) SetStorage(s *users.Storage) {
a.storage = s
}

39
auth/storage.go Normal file
View File

@ -0,0 +1,39 @@
package auth
import (
"github.com/filebrowser/filebrowser/settings"
"github.com/filebrowser/filebrowser/users"
)
// StorageBackend is a storage backend for auth storage.
type StorageBackend interface {
Get(settings.AuthMethod) (Auther, error)
Save(Auther) error
}
// Storage is a auth storage.
type Storage struct {
back StorageBackend
users *users.Storage
}
// NewStorage creates a auth storage from a backend.
func NewStorage(back StorageBackend, users *users.Storage) *Storage {
return &Storage{back: back, users: users}
}
// Get wraps a StorageBackend.Get and calls SetStorage on the auther.
func (s *Storage) Get(t settings.AuthMethod) (Auther, error) {
auther, err := s.back.Get(t)
if err != nil {
return nil, err
}
auther.SetStorage(s.users)
return auther, nil
}
// Save wraps a StorageBackend.Save.
func (s *Storage) Save(a Auther) error {
return s.back.Save(a)
}

View File

@ -9,7 +9,7 @@ import (
"text/tabwriter"
"github.com/filebrowser/filebrowser/auth"
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

View File

@ -7,7 +7,7 @@ import (
"strings"
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

View File

@ -3,7 +3,7 @@ package cmd
import (
"strings"
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

View File

@ -12,7 +12,7 @@ import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/auth"
"github.com/filebrowser/filebrowser/lib"
fbhttp "github.com/filebrowser/filebrowser/http"
"github.com/spf13/cobra"

View File

@ -6,7 +6,7 @@ import (
"os"
"text/tabwriter"
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

View File

@ -1,7 +1,7 @@
package cmd
import (
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

View File

@ -1,7 +1,7 @@
package cmd
import (
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

View File

@ -1,7 +1,7 @@
package cmd
import (
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

View File

@ -6,7 +6,7 @@ import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/bolt"
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

View File

@ -3,7 +3,7 @@ package cmd
import (
"fmt"
"github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
)

15
errors/errors.go Normal file
View File

@ -0,0 +1,15 @@
package errors
import "errors"
var (
ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option")
)

241
files/file.go Normal file
View File

@ -0,0 +1,241 @@
package files
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/filebrowser/filebrowser/errors"
"github.com/filebrowser/filebrowser/rules"
"github.com/spf13/afero"
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(fs afero.Fs, path string, modify bool, checker rules.Checker) (*FileInfo, error) {
if !checker.Check(path) {
return nil, os.ErrPermission
}
info, err := fs.Stat(path)
if err != nil {
return nil, err
}
file := &FileInfo{
Fs: fs,
Path: path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
}
if file.IsDir {
return file, file.readListing(checker)
}
err = file.detectType(modify)
if err != nil {
return nil, err
}
return file, err
}
// Checksum checksums a given File for a given User, using a specific
// algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error {
if i.IsDir {
return errors.ErrIsDirectory
}
if i.Checksums == nil {
i.Checksums = map[string]string{}
}
reader, err := i.Fs.Open(i.Path)
if err != nil {
return err
}
defer reader.Close()
var h hash.Hash
switch algo {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return errors.ErrInvalidOption
}
_, err = io.Copy(h, reader)
if err != nil {
return err
}
i.Checksums[algo] = hex.EncodeToString(h.Sum(nil))
return nil
}
func (i *FileInfo) detectType(modify bool) error {
reader, err := i.Fs.Open(i.Path)
if err != nil {
return err
}
defer reader.Close()
buffer := make([]byte, 512)
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
return err
}
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" {
mimetype = http.DetectContentType(buffer[:n])
}
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
return nil
case isBinary(string(buffer[:n])) || i.Size > 10*1024*1024: // 10 MB
i.Type = "blob"
return nil
default:
i.Type = "text"
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(i.Path)
if err != nil {
return err
}
if !modify {
i.Type = "textImmutable"
}
i.Content = string(content)
}
return nil
}
func (i *FileInfo) detectSubtitles() {
if i.Type != "video" {
return
}
i.Subtitles = []string{}
ext := filepath.Ext(i.Path)
// TODO: detect multiple languages. Base.Lang.vtt
path := strings.TrimSuffix(i.Path, ext) + ".vtt"
if _, err := i.Fs.Stat(path); err == nil {
i.Subtitles = append(i.Subtitles, path)
}
}
func (i *FileInfo) readListing(checker rules.Checker) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
return err
}
listing := &Listing{
Items: []*FileInfo{},
NumDirs: 0,
NumFiles: 0,
}
for _, f := range dir {
name := f.Name()
path := path.Join(i.Path, name)
if !checker.Check(path) {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
info, err := os.Stat(name)
if err == nil {
f = info
}
}
file := &FileInfo{
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
Extension: filepath.Ext(name),
Path: path,
}
if file.IsDir {
listing.NumDirs++
} else {
listing.NumFiles++
err := file.detectType(true)
if err != nil {
return err
}
}
listing.Items = append(listing.Items, file)
}
i.Listing = listing
return nil
}

View File

@ -1,32 +1,14 @@
package lib
package files
import (
"os"
"sort"
"time"
"github.com/maruel/natural"
)
// File describes a file.
type File struct {
*Listing
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
}
// Listing is a collection of files.
type Listing struct {
Items []*File `json:"items"`
Items []*FileInfo `json:"items"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
Sorting Sorting `json:"sorting"`

7
files/sorting.go Normal file
View File

@ -0,0 +1,7 @@
package files
// Sorting contains a sorting order.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}

12
files/utils.go Normal file
View File

@ -0,0 +1,12 @@
package files
func isBinary(content string) bool {
for _, b := range content {
// 65533 is the unknown char
// 8 and below are control chars (e.g. backspace, null, eof, etc)
if b <= 8 || b == 65533 {
return true
}
}
return false
}

View File

@ -4,18 +4,19 @@ import (
"context"
"encoding/json"
"net/http"
"os"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/errors"
"github.com/filebrowser/filebrowser/users"
)
func (e *env) loginHandler(w http.ResponseWriter, r *http.Request) {
user, err := e.Auther.Auth(r)
if err == lib.ErrNoPermission {
user, err := e.auther.Auth(r)
if err == os.ErrPermission {
httpErr(w, r, http.StatusForbidden, nil)
} else if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
@ -30,24 +31,24 @@ type signupBody struct {
}
func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) {
e.RLockSettings()
defer e.RUnlockSettings()
settings := e.GetSettings()
settings, err := e.Settings.Get()
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
return
}
if !settings.Signup {
httpErr(w, r, http.StatusForbidden, nil)
return
}
if r.Body == nil {
httpErr(w, r, http.StatusBadRequest, nil)
return
}
info := &signupBody{}
err := json.NewDecoder(r.Body).Decode(info)
err = json.NewDecoder(r.Body).Decode(info)
if err != nil {
httpErr(w, r, http.StatusBadRequest, nil)
return
@ -58,21 +59,21 @@ func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) {
return
}
user := &lib.User{
user := &users.User{
Username: info.Username,
}
e.ApplyDefaults(user)
settings.Defaults.Apply(user)
pwd, err := lib.HashPwd(info.Password)
pwd, err := users.HashPwd(info.Password)
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
return
}
user.Password = pwd
err = e.SaveUser(user)
if err == lib.ErrExist {
err = e.Users.Save(user)
if err == errors.ErrExist {
httpErr(w, r, http.StatusConflict, nil)
return
} else if err != nil {
@ -86,8 +87,8 @@ func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) {
type userInfo struct {
ID uint `json:"id"`
Locale string `json:"locale"`
ViewMode lib.ViewMode `json:"viewMode"`
Perm lib.Permissions `json:"perm"`
ViewMode users.ViewMode `json:"viewMode"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
LockPassword bool `json:"lockPassword"`
}
@ -119,7 +120,12 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
func (e *env) auth(next http.HandlerFunc) http.HandlerFunc {
keyFunc := func(token *jwt.Token) (interface{}, error) {
return e.GetSettings().Key, nil
settings, err := e.Settings.Get()
if err != nil {
return nil, err
}
return settings.Key, nil
}
nextWithUser := func(w http.ResponseWriter, r *http.Request, id uint) {
@ -154,7 +160,7 @@ func (e *env) renew(w http.ResponseWriter, r *http.Request) {
e.printToken(w, r, user)
}
func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *lib.User) {
func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *users.User) {
claims := &authToken{
User: userInfo{
ID: user.ID,
@ -171,7 +177,14 @@ func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *lib.User)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(e.GetSettings().Key)
settings, err := e.Settings.Get()
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
return
}
signed, err := token.SignedString(settings.Key)
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)

View File

@ -7,7 +7,10 @@ import (
"strconv"
"time"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/auth"
"github.com/filebrowser/filebrowser/storage"
"github.com/filebrowser/filebrowser/errors"
"github.com/filebrowser/filebrowser/users"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)
@ -24,22 +27,19 @@ type modifyRequest struct {
}
type env struct {
*lib.FileBrowser
Auther lib.Auther
*storage.Storage
auther auth.Auther
}
// NewHandler builds an HTTP handler on the top of a File Browser instance.
func NewHandler(fb *lib.FileBrowser) (http.Handler, error) {
authMethod := fb.GetSettings().AuthMethod
func NewHandler(storage *storage.Storage) (http.Handler, error) {
/* authMethod := fb.GetSettings().AuthMethod
auther, err := fb.GetAuther(authMethod)
if err != nil {
return nil, err
}
} */
e := &env{
FileBrowser: fb,
Auther: auther,
}
e := &env{}
r := mux.NewRouter()
@ -109,10 +109,10 @@ func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) {
}
}
func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) {
func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) {
id := r.Context().Value(keyUserID).(uint)
user, err := e.GetUser(id)
if err == lib.ErrNotExist {
user, err := e.Users.Get(id)
if err == errors.ErrNotExist {
httpErr(w, r, http.StatusForbidden, nil)
return nil, false
}
@ -125,7 +125,7 @@ func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool)
return user, true
}
func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) {
func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) {
user, ok := e.getUser(w, r)
if !ok {
return nil, false

View File

@ -7,14 +7,15 @@ import (
"path/filepath"
"strings"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/files"
"github.com/filebrowser/filebrowser/users"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
const apiRawPrefix = "/api/raw"
func parseQueryFiles(r *http.Request, f *lib.File, u *lib.User) ([]string, error) {
func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) {
files := []string{}
names := strings.Split(r.URL.Query().Get("files"), ",")
@ -67,6 +68,8 @@ func (e *env) rawHandler(w http.ResponseWriter, r *http.Request) {
return
}
files.NewFileInfo(user.Fs, path, user.Perm.Modify)
file, err := e.NewFile(path, user)
if err != nil {
httpErr(w, r, httpFsErr(err), err)
@ -141,7 +144,7 @@ func (e *env) rawHandler(w http.ResponseWriter, r *http.Request) {
}
}
func fileHandler(w http.ResponseWriter, r *http.Request, file *lib.File, user *lib.User) {
func fileHandler(w http.ResponseWriter, r *http.Request, file *files.File, user *users.User) {
fd, err := user.Fs.Open(file.Path)
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)

View File

@ -10,7 +10,8 @@ import (
"strings"
"github.com/filebrowser/filebrowser/fileutils"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/users"
)
const apiResourcePrefix = "/api/resources"
@ -30,7 +31,7 @@ func httpFsErr(err error) int {
}
}
func (e *env) getResourceData(w http.ResponseWriter, r *http.Request, prefix string) (string, *lib.User, bool) {
func (e *env) getResourceData(w http.ResponseWriter, r *http.Request, prefix string) (string, *users.User, bool) {
user, ok := e.getUser(w, r)
if !ok {
return "", nil, ok
@ -65,7 +66,7 @@ func (e *env) resourceGetHandler(w http.ResponseWriter, r *http.Request) {
}
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err = e.Checksum(file,user, checksum)
err = e.Checksum(file, user, checksum)
if err == lib.ErrInvalidOption {
httpErr(w, r, http.StatusBadRequest, nil)
return

View File

@ -4,15 +4,15 @@ import (
"encoding/json"
"net/http"
"github.com/filebrowser/filebrowser/lib"
"github.com/jinzhu/copier"
"github.com/filebrowser/filebrowser/rules"
"github.com/filebrowser/filebrowser/settings"
)
type settingsData struct {
Signup bool `json:"signup"`
Defaults lib.UserDefaults `json:"defaults"`
Rules []lib.Rule `json:"rules"`
Branding lib.Branding `json:"branding"`
Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"`
Branding settings.Branding `json:"branding"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
}
@ -23,16 +23,19 @@ func (e *env) settingsGetHandler(w http.ResponseWriter, r *http.Request) {
return
}
e.RLockSettings()
defer e.RUnlockSettings()
settings, err := e.Settings.Get()
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
return
}
data := &settingsData{
Signup: e.GetSettings().Signup,
Defaults: e.GetSettings().Defaults,
Rules: e.GetSettings().Rules,
Branding: e.GetSettings().Branding,
Shell: e.GetSettings().Shell,
Commands: e.GetSettings().Commands,
Signup: settings.Signup,
Defaults: settings.Defaults,
Rules: settings.Rules,
Branding: settings.Branding,
Shell: settings.Shell,
Commands: settings.Commands,
}
renderJSON(w, r, data)
@ -51,10 +54,11 @@ func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) {
return
}
e.RLockSettings()
settings := &lib.Settings{}
err = copier.Copy(settings, e.GetSettings())
e.RUnlockSettings()
settings, err := e.Settings.Get()
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
return
}
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
@ -68,7 +72,7 @@ func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) {
settings.Shell = req.Shell
settings.Commands = req.Commands
err = e.SaveSettings(settings)
err = e.Settings.Save(settings)
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
}

View File

@ -8,7 +8,8 @@ import (
"strings"
"time"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/errors"
"github.com/filebrowser/filebrowser/share"
)
const apiSharePrefix = "/api/share"
@ -33,9 +34,9 @@ func (e *env) shareGetHandler(w http.ResponseWriter, r *http.Request) {
return
}
s, err := e.GetLinksByPath(path)
if err == lib.ErrNotExist {
renderJSON(w, r, []*lib.ShareLink{})
s, err := e.Share.Gets(path)
if err == errors.ErrNotExist {
renderJSON(w, r, []*share.Link{})
return
}
@ -46,7 +47,7 @@ func (e *env) shareGetHandler(w http.ResponseWriter, r *http.Request) {
for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) {
e.DeleteLink(link.Hash)
e.Share.Delete(link.Hash)
s = append(s[:i], s[i+1:]...)
}
}
@ -72,7 +73,7 @@ func (e *env) shareDeleteHandler(w http.ResponseWriter, r *http.Request) {
return
}
err := e.DeleteLink(hash)
err := e.Share.Delete(hash)
if err != nil {
httpErr(w, r, http.StatusInternalServerError, err)
return

View File

@ -11,7 +11,6 @@ import (
"github.com/GeertJohan/go.rice"
"github.com/filebrowser/filebrowser/auth"
"github.com/filebrowser/filebrowser/lib"
)
func (e *env) getStaticData() map[string]interface{} {

View File

@ -7,7 +7,8 @@ import (
"strconv"
"strings"
"github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/users"
"github.com/gorilla/mux"
)
@ -22,7 +23,7 @@ func getUserID(r *http.Request) (uint, error) {
type modifyUserRequest struct {
modifyRequest
Data *lib.User `json:"data"`
Data *users.User `json:"data"`
}
func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, bool) {
@ -74,7 +75,7 @@ func (e *env) usersGetHandler(w http.ResponseWriter, r *http.Request) {
renderJSON(w, r, users)
}
func (e *env) userSelfOrAdmin(w http.ResponseWriter, r *http.Request) (*lib.User, uint, bool) {
func (e *env) userSelfOrAdmin(w http.ResponseWriter, r *http.Request) (*users.User, uint, bool) {
user, ok := e.getUser(w, r)
if !ok {
return nil, 0, false
@ -133,7 +134,7 @@ func (e *env) userDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func (e *env) userPostHandler(w http.ResponseWriter, r *http.Request) {
_, ok := e.getAdminUser(w,r)
_, ok := e.getAdminUser(w, r)
if !ok {
return
}
@ -197,7 +198,7 @@ func (e *env) userPutHandler(w http.ResponseWriter, r *http.Request) {
if req.Data.Password != "" {
req.Data.Password, err = lib.HashPwd(req.Data.Password)
} else {
var suser *lib.User
var suser *users.User
suser, err = e.GetUser(modifiedID)
req.Data.Password = suser.Password
}

View File

@ -1,14 +0,0 @@
package lib
import "net/http"
// AuthMethod describes an authentication method.
type AuthMethod string
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
Auth(*http.Request) (*User, error)
// SetInstance attaches the File Browser instance.
SetInstance(*FileBrowser)
}

View File

@ -1,17 +0,0 @@
package lib
import "errors"
var (
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrIsDirectory = errors.New("file is directory")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrInvalidOption = errors.New("invalid option")
ErrPathIsRel = errors.New("path is relative")
ErrNoPermission = errors.New("permission denied")
ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrEmptyKey = errors.New("empty key")
ErrInvalidDataType = errors.New("invalid data type")
)

View File

@ -1,404 +0,0 @@
package lib
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"log"
"mime"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"github.com/mholt/caddy"
"github.com/spf13/afero"
)
var defaultEvents = []string{
"save",
"copy",
"rename",
"upload",
"delete",
}
// FileBrowser represents a File Browser instance which must
// be created through NewFileBrowser.
type FileBrowser struct {
settings *Settings
storage StorageBackend
mux sync.RWMutex
}
// NewFileBrowser creates a new File Browser instance from a
// storage backend. If that backend doesn't contain settings
// on it (returns ErrNotExist), then we generate a new key
// and base settings.
func NewFileBrowser(backend StorageBackend) (*FileBrowser, error) {
settings, err := backend.GetSettings()
if err == ErrNotExist {
var key []byte
key, err = generateRandomBytes(64)
if err != nil {
return nil, err
}
settings = &Settings{Key: key}
err = backend.SaveSettings(settings)
}
if err != nil {
return nil, err
}
return &FileBrowser{
settings: settings,
storage: backend,
}, nil
}
// RLockSettings locks the settings for reading.
func (f *FileBrowser) RLockSettings() {
f.mux.RLock()
}
// RUnlockSettings unlocks the settings for reading.
func (f *FileBrowser) RUnlockSettings() {
f.mux.RUnlock()
}
// RulesCheck matches a path against the user rules and the
// global rules. Returns true if allowed, false if not.
func (f *FileBrowser) RulesCheck(u *User, path string) bool {
for _, rule := range u.Rules {
if rule.Matches(path) {
return rule.Allow
}
}
f.mux.RLock()
defer f.mux.RUnlock()
for _, rule := range f.settings.Rules {
if rule.Matches(path) {
return rule.Allow
}
}
return true
}
// RunHook runs the hooks for the before and after event.
func (f *FileBrowser) RunHook(fn func() error, evt, path, dst string, user *User) error {
path = user.FullPath(path)
dst = user.FullPath(dst)
if val, ok := f.settings.Commands["before_"+evt]; ok {
for _, command := range val {
err := f.exec(command, "before_"+evt, path, dst, user)
if err != nil {
return err
}
}
}
err := fn()
if err != nil {
return err
}
if val, ok := f.settings.Commands["after_"+evt]; ok {
for _, command := range val {
err := f.exec(command, "after_"+evt, path, dst, user)
if err != nil {
return err
}
}
}
return nil
}
// ParseCommand parses the command taking in account if the current
// instance uses a shell to run the commands or just calls the binary
// directyly.
func (f *FileBrowser) ParseCommand(raw string) ([]string, error) {
f.RLockSettings()
defer f.RUnlockSettings()
command := []string{}
if len(f.settings.Shell) == 0 {
cmd, args, err := caddy.SplitCommandAndArgs(raw)
if err != nil {
return nil, err
}
_, err = exec.LookPath(cmd)
if err != nil {
return nil, err
}
command = append(command, cmd)
command = append(command, args...)
} else {
command = append(f.settings.Shell, raw)
}
return command, nil
}
// ApplyDefaults applies the default options to a user.
func (f *FileBrowser) ApplyDefaults(u *User) {
f.RLockSettings()
u.Scope = f.settings.Defaults.Scope
u.Locale = f.settings.Defaults.Locale
u.ViewMode = f.settings.Defaults.ViewMode
u.Perm = f.settings.Defaults.Perm
u.Sorting = f.settings.Defaults.Sorting
u.Commands = f.settings.Defaults.Commands
f.RUnlockSettings()
}
// NewFile creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
func (f *FileBrowser) NewFile(path string, user *User) (*File, error) {
if !f.RulesCheck(user, path) {
return nil, os.ErrPermission
}
info, err := user.Fs.Stat(path)
if err != nil {
return nil, err
}
file := &File{
Path: path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
}
if file.IsDir {
return file, f.readListing(file, user)
}
err = f.detectType(file, user)
if err != nil {
return nil, err
}
if file.Type == "video" {
f.detectSubtitles(file, user)
}
return file, err
}
// Checksum checksums a given File for a given User, using a specific
// algorithm. The checksums data is saved on File object.
func (f *FileBrowser) Checksum(file *File, user *User, algo string) error {
if file.IsDir {
return ErrIsDirectory
}
if file.Checksums == nil {
file.Checksums = map[string]string{}
}
i, err := user.Fs.Open(file.Path)
if err != nil {
return err
}
defer i.Close()
var h hash.Hash
switch algo {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return ErrInvalidOption
}
_, err = io.Copy(h, i)
if err != nil {
return err
}
file.Checksums[algo] = hex.EncodeToString(h.Sum(nil))
return nil
}
func (f *FileBrowser) readListing(file *File, user *User) error {
afs := &afero.Afero{Fs: user.Fs}
files, err := afs.ReadDir(file.Path)
if err != nil {
return err
}
listing := &Listing{
Items: []*File{},
NumDirs: 0,
NumFiles: 0,
}
for _, i := range files {
name := i.Name()
path := path.Join(file.Path, name)
if !f.RulesCheck(user, path) {
continue
}
if strings.HasPrefix(i.Mode().String(), "L") {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
info, err := os.Stat(name)
if err == nil {
i = info
}
}
file := &File{
Name: name,
Size: i.Size(),
ModTime: i.ModTime(),
Mode: i.Mode(),
IsDir: i.IsDir(),
Extension: filepath.Ext(name),
Path: path,
}
if file.IsDir {
listing.NumDirs++
} else {
listing.NumFiles++
err := f.detectType(file, user)
if err != nil {
return err
}
}
listing.Items = append(listing.Items, file)
}
file.Listing = listing
return nil
}
func (f *FileBrowser) detectType(file *File, user *User) error {
i, err := user.Fs.Open(file.Path)
if err != nil {
return err
}
defer i.Close()
buffer := make([]byte, 512)
n, err := i.Read(buffer)
if err != nil && err != io.EOF {
return err
}
mimetype := mime.TypeByExtension(file.Extension)
if mimetype == "" {
mimetype = http.DetectContentType(buffer[:n])
}
switch {
case strings.HasPrefix(mimetype, "video"):
file.Type = "video"
return nil
case strings.HasPrefix(mimetype, "audio"):
file.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
file.Type = "image"
return nil
case isBinary(string(buffer[:n])) || file.Size > 10*1024*1024: // 10 MB
file.Type = "blob"
return nil
default:
file.Type = "text"
afs := &afero.Afero{Fs: user.Fs}
content, err := afs.ReadFile(file.Path)
if err != nil {
return err
}
file.Content = string(content)
}
if !user.Perm.Modify && file.Type == "text" {
file.Type = "textImmutable"
}
return nil
}
func (f *FileBrowser) detectSubtitles(file *File, user *User) {
file.Subtitles = []string{}
ext := filepath.Ext(file.Path)
base := strings.TrimSuffix(file.Path, ext)
// TODO: detect multiple languages. Like base.lang.vtt
path := base + ".vtt"
if _, err := user.Fs.Stat(path); err == nil {
file.Subtitles = append(file.Subtitles, path)
}
}
func (f *FileBrowser) exec(raw, evt, path, dst string, user *User) error {
blocking := true
if strings.HasSuffix(raw, "&") {
blocking = false
raw = strings.TrimSpace(strings.TrimSuffix(raw, "&"))
}
command, err := f.ParseCommand(raw)
if err != nil {
return err
}
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope))
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if !blocking {
log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " "))
return cmd.Start()
}
log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " "))
return cmd.Run()
}

View File

@ -1,38 +0,0 @@
package lib
// Settings contain the main settings of the application.
type Settings struct {
Key []byte `json:"key"`
BaseURL string `json:"baseURL"`
Signup bool `json:"signup"`
Defaults UserDefaults `json:"defaults"`
AuthMethod AuthMethod `json:"authMethod"`
Branding Branding `json:"branding"`
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Rules []Rule `json:"rules"` // TODO: use this add to cli
}
// Sorting contains a sorting order.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}
// Branding contains the branding settings of the app.
type Branding struct {
Name string `json:"name"`
DisableExternal bool `json:"disableExternal"`
Files string `json:"files"`
}
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode ViewMode `json:"viewMode"`
Sorting Sorting `json:"sorting"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
}

View File

@ -1,199 +0,0 @@
package lib
import (
"strings"
)
// StorageBackend is the interface used to persist data.
type StorageBackend interface {
GetUserByID(uint) (*User, error)
GetUserByUsername(string) (*User, error)
GetUsers() ([]*User, error)
SaveUser(u *User) error
UpdateUser(u *User, fields ...string) error
DeleteUserByID(uint) error
DeleteUserByUsername(string) error
GetSettings() (*Settings, error)
SaveSettings(*Settings) error
GetAuther(AuthMethod) (Auther, error)
SaveAuther(Auther) error
GetLinkByHash(hash string) (*ShareLink, error)
GetLinkPermanent(path string) (*ShareLink, error)
GetLinksByPath(path string) ([]*ShareLink, error)
SaveLink(s *ShareLink) error
DeleteLink(hash string) error
}
// GetUser allows you to get a user by its name or username. The provided
// id must be a string for username lookup or a uint for id lookup. If id
// is neither, a ErrInvalidDataType will be returned.
func (f *FileBrowser) GetUser(id interface{}) (*User, error) {
var (
user *User
err error
)
switch id.(type) {
case string:
user, err = f.storage.GetUserByUsername(id.(string))
case uint:
user, err = f.storage.GetUserByID(id.(uint))
default:
return nil, ErrInvalidDataType
}
if err != nil {
return nil, err
}
user.clean()
return user, err
}
// GetUsers gets a list of all users.
func (f *FileBrowser) GetUsers() ([]*User, error) {
users, err := f.storage.GetUsers()
if err != nil {
return nil, err
}
for _, user := range users {
user.clean()
}
return users, err
}
// UpdateUser updates a user in the database.
func (f *FileBrowser) UpdateUser(user *User, fields ...string) error {
err := user.clean(fields...)
if err != nil {
return err
}
return f.storage.UpdateUser(user, fields...)
}
// SaveUser saves the user in a storage.
func (f *FileBrowser) SaveUser(user *User) error {
if err := user.clean(); err != nil {
return err
}
return f.storage.SaveUser(user)
}
// DeleteUser allows you to delete a user by its name or username. The provided
// id must be a string for username lookup or a uint for id lookup. If id
// is neither, a ErrInvalidDataType will be returned.
func (f *FileBrowser) DeleteUser(id interface{}) (err error) {
switch id.(type) {
case string:
err = f.storage.DeleteUserByUsername(id.(string))
case uint:
err = f.storage.DeleteUserByID(id.(uint))
default:
err = ErrInvalidDataType
}
return
}
// GetSettings returns the settings for the current instance.
func (f *FileBrowser) GetSettings() *Settings {
return f.settings
}
// SaveSettings saves the settings for the current instance.
func (f *FileBrowser) SaveSettings(s *Settings) error {
s.BaseURL = strings.TrimSuffix(s.BaseURL, "/")
if len(s.Key) == 0 {
return ErrEmptyKey
}
if s.Defaults.Locale == "" {
s.Defaults.Locale = "en"
}
if s.Defaults.Commands == nil {
s.Defaults.Commands = []string{}
}
if s.Defaults.ViewMode == "" {
s.Defaults.ViewMode = MosaicViewMode
}
if s.Rules == nil {
s.Rules = []Rule{}
}
if s.Shell == nil {
s.Shell = []string{}
}
if s.Commands == nil {
s.Commands = map[string][]string{}
}
for _, event := range defaultEvents {
if _, ok := s.Commands["before_"+event]; !ok {
s.Commands["before_"+event] = []string{}
}
if _, ok := s.Commands["after_"+event]; !ok {
s.Commands["after_"+event] = []string{}
}
}
err := f.storage.SaveSettings(s)
if err != nil {
return err
}
f.mux.Lock()
f.settings = s
f.mux.Unlock()
return nil
}
// GetAuther wraps a StorageBackend.GetAuther and calls SetInstance on the auther.
func (f *FileBrowser) GetAuther(t AuthMethod) (Auther, error) {
auther, err := f.storage.GetAuther(t)
if err != nil {
return nil, err
}
auther.SetInstance(f)
return auther, nil
}
// SaveAuther wraps a StorageBackend.SaveAuther.
func (f *FileBrowser) SaveAuther(a Auther) error {
return f.storage.SaveAuther(a)
}
// GetLinkByHash wraps a StorageBackend.GetLinkByHash.
func (f *FileBrowser) GetLinkByHash(hash string) (*ShareLink, error) {
return f.storage.GetLinkByHash(hash)
}
// GetLinkPermanent wraps a StorageBackend.GetLinkPermanent
func (f *FileBrowser) GetLinkPermanent(path string) (*ShareLink, error) {
return f.storage.GetLinkPermanent(path)
}
// GetLinksByPath wraps a StorageBackend.GetLinksByPath
func (f *FileBrowser) GetLinksByPath(path string) ([]*ShareLink, error) {
return f.storage.GetLinksByPath(path)
}
// SaveLink wraps a StorageBackend.SaveLink
func (f *FileBrowser) SaveLink(s *ShareLink) error {
return f.storage.SaveLink(s)
}
// DeleteLink wraps a StorageBackend.DeleteLink
func (f *FileBrowser) DeleteLink(hash string) error {
return f.storage.DeleteLink(hash)
}

View File

@ -1,37 +0,0 @@
package lib
import (
"crypto/rand"
"golang.org/x/crypto/bcrypt"
)
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
return b, err
}
func isBinary(content string) bool {
for _, b := range content {
// 65533 is the unknown char
// 8 and below are control chars (e.g. backspace, null, eof, etc)
if b <= 8 || b == 65533 {
return true
}
}
return false
}
// HashPwd hashes a password.
func HashPwd(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPwd checks if a password is correct.
func CheckPwd(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

View File

@ -1,10 +1,15 @@
package lib
package rules
import (
"regexp"
"strings"
)
// Checker is a Rules checker.
type Checker interface {
Check(path string) bool
}
// Rule is a allow/disallow rule.
type Rule struct {
Regex bool `json:"regex"`

8
settings/branding.go Normal file
View File

@ -0,0 +1,8 @@
package settings
// Branding contains the branding settings of the app.
type Branding struct {
Name string `json:"name"`
DisableExternal bool `json:"disableExternal"`
Files string `json:"files"`
}

27
settings/defaults.go Normal file
View File

@ -0,0 +1,27 @@
package settings
import (
"github.com/filebrowser/filebrowser/files"
"github.com/filebrowser/filebrowser/users"
)
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
Sorting files.Sorting `json:"sorting"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
}
// Apply applies the default options to a user.
func (d *UserDefaults) Apply(u *users.User) {
u.Scope = d.Scope
u.Locale = d.Locale
u.ViewMode = d.ViewMode
u.Perm = d.Perm
u.Sorting = d.Sorting
u.Commands = d.Commands
}

24
settings/settings.go Normal file
View File

@ -0,0 +1,24 @@
package settings
import "github.com/filebrowser/filebrowser/rules"
// AuthMethod describes an authentication method.
type AuthMethod string
// Settings contain the main settings of the application.
type Settings struct {
Key []byte `json:"key"`
BaseURL string `json:"baseURL"`
Signup bool `json:"signup"`
Defaults UserDefaults `json:"defaults"`
AuthMethod AuthMethod `json:"authMethod"`
Branding Branding `json:"branding"`
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"` // TODO: use this add to cli
}
// GetRules implements rules.Provider.
func (s *Settings) GetRules() []rules.Rule {
return s.Rules
}

88
settings/storage.go Normal file
View File

@ -0,0 +1,88 @@
package settings
import (
"strings"
"github.com/filebrowser/filebrowser/errors"
"github.com/filebrowser/filebrowser/rules"
"github.com/filebrowser/filebrowser/users"
)
// StorageBackend is a settings storage backend.
type StorageBackend interface {
Get() (*Settings, error)
Save(*Settings) error
}
// Storage is a settings storage.
type Storage struct {
back StorageBackend
}
// NewStorage creates a settings storage from a backend.
func NewStorage(back StorageBackend) *Storage {
return &Storage{back: back}
}
// Get returns the settings for the current instance.
func (s *Storage) Get() (*Settings, error) {
return s.back.Get()
}
var defaultEvents = []string{
"save",
"copy",
"rename",
"upload",
"delete",
}
// Save saves the settings for the current instance.
func (s *Storage) Save(set *Settings) error {
set.BaseURL = strings.TrimSuffix(set.BaseURL, "/")
if len(set.Key) == 0 {
return errors.ErrEmptyKey
}
if set.Defaults.Locale == "" {
set.Defaults.Locale = "en"
}
if set.Defaults.Commands == nil {
set.Defaults.Commands = []string{}
}
if set.Defaults.ViewMode == "" {
set.Defaults.ViewMode = users.MosaicViewMode
}
if set.Rules == nil {
set.Rules = []rules.Rule{}
}
if set.Shell == nil {
set.Shell = []string{}
}
if set.Commands == nil {
set.Commands = map[string][]string{}
}
for _, event := range defaultEvents {
if _, ok := set.Commands["before_"+event]; !ok {
set.Commands["before_"+event] = []string{}
}
if _, ok := set.Commands["after_"+event]; !ok {
set.Commands["after_"+event] = []string{}
}
}
err := s.back.Save(set)
if err != nil {
return err
}
return nil
}

View File

@ -1,9 +1,9 @@
package lib
package share
import "time"
// ShareLink is the information needed to build a shareable link.
type ShareLink struct {
// Link is the information needed to build a shareable link.
type Link struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`

45
share/storage.go Normal file
View File

@ -0,0 +1,45 @@
package share
// StorageBackend is the interface to implement for a share storage.
type StorageBackend interface {
GetByHash(hash string) (*Link, error)
GetPermanent(path string) (*Link, error)
Gets(path string) ([]*Link, error)
Save(s *Link) error
Delete(hash string) error
}
// Storage is a storage.
type Storage struct {
back StorageBackend
}
// NewStorage creates a share links storage from a backend.
func NewStorage(back StorageBackend) *Storage {
return &Storage{back: back}
}
// GetByHash wraps a StorageBackend.GetByHash.
func (s *Storage) GetByHash(hash string) (*Link, error) {
return s.back.GetByHash(hash)
}
// GetPermanent wraps a StorageBackend.GetPermanent
func (s *Storage) GetPermanent(path string) (*Link, error) {
return s.back.GetPermanent(path)
}
// Gets wraps a StorageBackend.Gets
func (s *Storage) Gets(path string) ([]*Link, error) {
return s.back.Gets(path)
}
// Save wraps a StorageBackend.Save
func (s *Storage) Save(l *Link) error {
return s.back.Save(l)
}
// Delete wraps a StorageBackend.Delete
func (s *Storage) Delete(hash string) error {
return s.back.Delete(hash)
}

View File

@ -2,7 +2,7 @@ package bolt
import "github.com/asdine/storm"
// Backend implements lib.StorageBackend
// Backend implements storage.Backend
// using Bolt DB.
type Backend struct {
DB *storm.DB

View File

@ -3,7 +3,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/auth"
"github.com/filebrowser/filebrowser/lib"
)
func (b Backend) get(name string, to interface{}) error {
@ -19,12 +19,12 @@ func (b Backend) save(name string, from interface{}) error {
return b.DB.Set("config", name, from)
}
func (b Backend) GetSettings() (*lib.Settings, error) {
settings := &lib.Settings{}
func (b Backend) GetSettings() (*settings.Settings, error) {
settings := &settings.Settings{}
return settings, b.get("settings", settings)
}
func (b Backend) SaveSettings(s *lib.Settings) error {
func (b Backend) SaveSettings(s *settings.Settings) error {
return b.save("settings", s)
}

View File

@ -3,7 +3,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
"github.com/filebrowser/filebrowser/lib"
)
func (s Backend) GetLinkByHash(hash string) (*lib.ShareLink, error) {

View File

@ -4,7 +4,7 @@ import (
"reflect"
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/lib"
)
func (st Backend) GetUserByID(id uint) (*lib.User, error) {

17
storage/storage.go Normal file
View File

@ -0,0 +1,17 @@
package storage
import (
"github.com/filebrowser/filebrowser/auth"
"github.com/filebrowser/filebrowser/settings"
"github.com/filebrowser/filebrowser/share"
"github.com/filebrowser/filebrowser/users"
)
// Storage is a storage powered by a Backend whih makes the neccessary
// verifications when fetching and saving data to ensure consistency.
type Storage struct {
Users *users.Storage
Share *share.Storage
Auth *auth.Storage
Settings *settings.Storage
}

10
storage/utils.go Normal file
View File

@ -0,0 +1,10 @@
package storage
import "crypto/rand"
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
return b, err
}

17
users/password.go Normal file
View File

@ -0,0 +1,17 @@
package users
import (
"golang.org/x/crypto/bcrypt"
)
// HashPwd hashes a password.
func HashPwd(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPwd checks if a password is correct.
func CheckPwd(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

13
users/permissions.go Normal file
View File

@ -0,0 +1,13 @@
package users
// Permissions describe a user's permissions.
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}

101
users/storage.go Normal file
View File

@ -0,0 +1,101 @@
package users
import (
"github.com/filebrowser/filebrowser/errors"
)
// StorageBackend is the interface to implement for a users storage.
type StorageBackend interface {
GetByID(uint) (*User, error)
GetByUsername(string) (*User, error)
Gets() ([]*User, error)
Save(u *User) error
Update(u *User, fields ...string) error
DeleteByID(uint) error
DeleteByUsername(string) error
}
// Storage is a users storage.
type Storage struct {
back StorageBackend
}
// NewStorage creates a users storage from a backend.
func NewStorage(back StorageBackend) *Storage {
return &Storage{back: back}
}
// Get allows you to get a user by its name or username. The provided
// id must be a string for username lookup or a uint for id lookup. If id
// is neither, a ErrInvalidDataType will be returned.
func (s *Storage) Get(id interface{}) (*User, error) {
var (
user *User
err error
)
switch id.(type) {
case string:
user, err = s.back.GetByUsername(id.(string))
case uint:
user, err = s.back.GetByID(id.(uint))
default:
return nil, errors.ErrInvalidDataType
}
if err != nil {
return nil, err
}
user.Clean()
return user, err
}
// Gets gets a list of all users.
func (s *Storage) Gets() ([]*User, error) {
users, err := s.back.Gets()
if err != nil {
return nil, err
}
for _, user := range users {
user.Clean()
}
return users, err
}
// Update updates a user in the database.
func (s *Storage) Update(user *User, fields ...string) error {
err := user.Clean(fields...)
if err != nil {
return err
}
return s.back.Update(user, fields...)
}
// Save saves the user in a storage.
func (s *Storage) Save(user *User) error {
if err := user.Clean(); err != nil {
return err
}
return s.back.Save(user)
}
// Delete allows you to delete a user by its name or username. The provided
// id must be a string for username lookup or a uint for id lookup. If id
// is neither, a ErrInvalidDataType will be returned.
func (s *Storage) Delete(id interface{}) (err error) {
switch id.(type) {
case string:
err = s.back.DeleteByUsername(id.(string))
case uint:
err = s.back.DeleteByID(id.(uint))
default:
err = errors.ErrInvalidDataType
}
return
}

View File

@ -1,9 +1,12 @@
package lib
package users
import (
"github.com/filebrowser/filebrowser/errors"
"path/filepath"
"regexp"
"github.com/filebrowser/filebrowser/files"
"github.com/filebrowser/filebrowser/rules"
"github.com/spf13/afero"
)
@ -15,18 +18,6 @@ const (
MosaicViewMode ViewMode = "mosaic"
)
// Permissions describe a user's permissions.
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}
// User describes a user.
type User struct {
ID uint `storm:"id,increment" json:"id"`
@ -38,9 +29,14 @@ type User struct {
ViewMode ViewMode `json:"viewMode"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting Sorting `json:"sorting"`
Sorting files.Sorting `json:"sorting"`
Fs afero.Fs `json:"-"`
Rules []Rule `json:"rules"`
Rules []rules.Rule `json:"rules"`
}
// GetRules implements rules.Provider.
func (u *User) GetRules() []rules.Rule {
return u.Rules
}
var checkableFields = []string{
@ -53,7 +49,9 @@ var checkableFields = []string{
"Rules",
}
func (u *User) clean(fields ...string) error {
// Clean cleans up a user and verifies if all its fields
// are alright to be saved.
func (u *User) Clean(fields ...string) error {
if len(fields) == 0 {
fields = checkableFields
}
@ -62,15 +60,15 @@ func (u *User) clean(fields ...string) error {
switch field {
case "Username":
if u.Username == "" {
return ErrEmptyUsername
return errors.ErrEmptyUsername
}
case "Password":
if u.Password == "" {
return ErrEmptyPassword
return errors.ErrEmptyPassword
}
case "Scope":
if !filepath.IsAbs(u.Scope) {
return ErrPathIsRel
return errors.ErrScopeIsRelative
}
case "ViewMode":
if u.ViewMode == "" {
@ -86,7 +84,7 @@ func (u *User) clean(fields ...string) error {
}
case "Rules":
if u.Rules == nil {
u.Rules = []Rule{}
u.Rules = []rules.Rule{}
}
}
}

View File

@ -1,4 +1,4 @@
package lib
package version
const (
// Version is the current File Browser version.