ugh
License: MIT Signed-off-by: Henrique Dias <hacdias@gmail.com>
This commit is contained in:
parent
27e12c687b
commit
18ad9d78b3
185
_remove/filebrowser.go
Normal file
185
_remove/filebrowser.go
Normal 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
15
auth/auth.go
Normal 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)
|
||||
}
|
||||
28
auth/json.go
28
auth/json.go
@ -4,13 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
"github.com/filebrowser/filebrowser/settings"
|
||||
"github.com/filebrowser/filebrowser/users"
|
||||
)
|
||||
|
||||
// MethodJSONAuth is used to identify json auth.
|
||||
const MethodJSONAuth lib.AuthMethod = "json"
|
||||
const MethodJSONAuth settings.AuthMethod = "json"
|
||||
|
||||
type jsonCred struct {
|
||||
Password string `json:"password"`
|
||||
@ -21,20 +23,20 @@ type jsonCred struct {
|
||||
// JSONAuth is a json implementaion of an Auther.
|
||||
type JSONAuth struct {
|
||||
ReCaptcha *ReCaptcha
|
||||
instance *lib.FileBrowser
|
||||
storage *users.Storage
|
||||
}
|
||||
|
||||
// Auth authenticates the user via a json in content body.
|
||||
func (a *JSONAuth) Auth(r *http.Request) (*lib.User, error) {
|
||||
func (a *JSONAuth) Auth(r *http.Request) (*users.User, error) {
|
||||
var cred jsonCred
|
||||
|
||||
if r.Body == nil {
|
||||
return nil, lib.ErrNoPermission
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
return nil, lib.ErrNoPermission
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
@ -46,21 +48,21 @@ func (a *JSONAuth) Auth(r *http.Request) (*lib.User, error) {
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, lib.ErrNoPermission
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
}
|
||||
|
||||
u, err := a.instance.GetUser(cred.Username)
|
||||
if err != nil || !lib.CheckPwd(cred.Password, u.Password) {
|
||||
return nil, lib.ErrNoPermission
|
||||
u, err := a.storage.Get(cred.Username)
|
||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// SetInstance attaches the instance to the auther.
|
||||
func (a *JSONAuth) SetInstance(i *lib.FileBrowser) {
|
||||
a.instance = i
|
||||
// SetStorage attaches the storage to the auther.
|
||||
func (a *JSONAuth) SetStorage(s *users.Storage) {
|
||||
a.storage = s
|
||||
}
|
||||
|
||||
const reCaptchaAPI = "/recaptcha/api/siteverify"
|
||||
|
||||
17
auth/none.go
17
auth/none.go
@ -3,23 +3,24 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
"github.com/filebrowser/filebrowser/settings"
|
||||
"github.com/filebrowser/filebrowser/users"
|
||||
)
|
||||
|
||||
// MethodNoAuth is used to identify no auth.
|
||||
const MethodNoAuth lib.AuthMethod = "noauth"
|
||||
const MethodNoAuth settings.AuthMethod = "noauth"
|
||||
|
||||
// NoAuth is no auth implementation of auther.
|
||||
type NoAuth struct {
|
||||
instance *lib.FileBrowser
|
||||
storage *users.Storage
|
||||
}
|
||||
|
||||
// Auth uses authenticates user 1.
|
||||
func (a *NoAuth) Auth(r *http.Request) (*lib.User, error) {
|
||||
return a.instance.GetUser(1)
|
||||
func (a *NoAuth) Auth(r *http.Request) (*users.User, error) {
|
||||
return a.storage.Get(1)
|
||||
}
|
||||
|
||||
// SetInstance attaches the instance to the auther.
|
||||
func (a *NoAuth) SetInstance(i *lib.FileBrowser) {
|
||||
a.instance = i
|
||||
// SetStorage attaches the storage to the auther.
|
||||
func (a *NoAuth) SetStorage(s *users.Storage) {
|
||||
a.storage = s
|
||||
}
|
||||
|
||||
@ -2,31 +2,34 @@ package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
"github.com/filebrowser/filebrowser/settings"
|
||||
"github.com/filebrowser/filebrowser/users"
|
||||
"github.com/filebrowser/filebrowser/errors"
|
||||
)
|
||||
|
||||
// MethodProxyAuth is used to identify no auth.
|
||||
const MethodProxyAuth lib.AuthMethod = "proxy"
|
||||
const MethodProxyAuth settings.AuthMethod = "proxy"
|
||||
|
||||
// ProxyAuth is a proxy implementation of an auther.
|
||||
type ProxyAuth struct {
|
||||
Header string
|
||||
instance *lib.FileBrowser
|
||||
Header string
|
||||
storage *users.Storage
|
||||
}
|
||||
|
||||
// Auth authenticates the user via an HTTP header.
|
||||
func (a *ProxyAuth) Auth(r *http.Request) (*lib.User, error) {
|
||||
func (a *ProxyAuth) Auth(r *http.Request) (*users.User, error) {
|
||||
username := r.Header.Get(a.Header)
|
||||
user, err := a.instance.GetUser(username)
|
||||
if err == lib.ErrNotExist {
|
||||
return nil, lib.ErrNoPermission
|
||||
user, err := a.storage.Get(username)
|
||||
if err == errors.ErrNotExist {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
// SetInstance attaches the instance to the auther.
|
||||
func (a *ProxyAuth) SetInstance(i *lib.FileBrowser) {
|
||||
a.instance = i
|
||||
// SetStorage attaches the storage to the auther.
|
||||
func (a *ProxyAuth) SetStorage(s *users.Storage) {
|
||||
a.storage = s
|
||||
}
|
||||
|
||||
39
auth/storage.go
Normal file
39
auth/storage.go
Normal 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)
|
||||
}
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/filebrowser/filebrowser/auth"
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ package cmd
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/filebrowser/filebrowser/bolt"
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
15
errors/errors.go
Normal file
15
errors/errors.go
Normal 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
241
files/file.go
Normal 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
|
||||
}
|
||||
@ -1,35 +1,17 @@
|
||||
package lib
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/maruel/natural"
|
||||
)
|
||||
|
||||
// File describes a file.
|
||||
type File struct {
|
||||
*Listing
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"extension"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
Mode os.FileMode `json:"mode"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Type string `json:"type"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
}
|
||||
|
||||
// Listing is a collection of files.
|
||||
type Listing struct {
|
||||
Items []*File `json:"items"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Items []*FileInfo `json:"items"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
}
|
||||
|
||||
// ApplySort applies the sort order using .Order and .Sort
|
||||
7
files/sorting.go
Normal file
7
files/sorting.go
Normal 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
12
files/utils.go
Normal 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
|
||||
}
|
||||
53
http/auth.go
53
http/auth.go
@ -4,18 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/request"
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
"github.com/filebrowser/filebrowser/errors"
|
||||
"github.com/filebrowser/filebrowser/users"
|
||||
)
|
||||
|
||||
|
||||
func (e *env) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := e.Auther.Auth(r)
|
||||
if err == lib.ErrNoPermission {
|
||||
user, err := e.auther.Auth(r)
|
||||
if err == os.ErrPermission {
|
||||
httpErr(w, r, http.StatusForbidden, nil)
|
||||
} else if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
@ -30,24 +31,24 @@ type signupBody struct {
|
||||
}
|
||||
|
||||
func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
e.RLockSettings()
|
||||
defer e.RUnlockSettings()
|
||||
settings, err := e.Settings.Get()
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings := e.GetSettings()
|
||||
|
||||
if !settings.Signup {
|
||||
httpErr(w, r, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if r.Body == nil {
|
||||
httpErr(w, r, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
info := &signupBody{}
|
||||
err := json.NewDecoder(r.Body).Decode(info)
|
||||
err = json.NewDecoder(r.Body).Decode(info)
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusBadRequest, nil)
|
||||
return
|
||||
@ -58,21 +59,21 @@ func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user := &lib.User{
|
||||
user := &users.User{
|
||||
Username: info.Username,
|
||||
}
|
||||
|
||||
e.ApplyDefaults(user)
|
||||
settings.Defaults.Apply(user)
|
||||
|
||||
pwd, err := lib.HashPwd(info.Password)
|
||||
pwd, err := users.HashPwd(info.Password)
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = pwd
|
||||
err = e.SaveUser(user)
|
||||
if err == lib.ErrExist {
|
||||
err = e.Users.Save(user)
|
||||
if err == errors.ErrExist {
|
||||
httpErr(w, r, http.StatusConflict, nil)
|
||||
return
|
||||
} else if err != nil {
|
||||
@ -86,8 +87,8 @@ func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type userInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode lib.ViewMode `json:"viewMode"`
|
||||
Perm lib.Permissions `json:"perm"`
|
||||
ViewMode users.ViewMode `json:"viewMode"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
}
|
||||
@ -119,7 +120,12 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
|
||||
|
||||
func (e *env) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return e.GetSettings().Key, nil
|
||||
settings, err := e.Settings.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return settings.Key, nil
|
||||
}
|
||||
|
||||
nextWithUser := func(w http.ResponseWriter, r *http.Request, id uint) {
|
||||
@ -154,7 +160,7 @@ func (e *env) renew(w http.ResponseWriter, r *http.Request) {
|
||||
e.printToken(w, r, user)
|
||||
}
|
||||
|
||||
func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *lib.User) {
|
||||
func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *users.User) {
|
||||
claims := &authToken{
|
||||
User: userInfo{
|
||||
ID: user.ID,
|
||||
@ -171,7 +177,14 @@ func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *lib.User)
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString(e.GetSettings().Key)
|
||||
|
||||
settings, err := e.Settings.Get()
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
signed, err := token.SignedString(settings.Key)
|
||||
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
|
||||
28
http/http.go
28
http/http.go
@ -7,7 +7,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
"github.com/filebrowser/filebrowser/auth"
|
||||
"github.com/filebrowser/filebrowser/storage"
|
||||
"github.com/filebrowser/filebrowser/errors"
|
||||
"github.com/filebrowser/filebrowser/users"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@ -24,22 +27,19 @@ type modifyRequest struct {
|
||||
}
|
||||
|
||||
type env struct {
|
||||
*lib.FileBrowser
|
||||
Auther lib.Auther
|
||||
*storage.Storage
|
||||
auther auth.Auther
|
||||
}
|
||||
|
||||
// NewHandler builds an HTTP handler on the top of a File Browser instance.
|
||||
func NewHandler(fb *lib.FileBrowser) (http.Handler, error) {
|
||||
authMethod := fb.GetSettings().AuthMethod
|
||||
func NewHandler(storage *storage.Storage) (http.Handler, error) {
|
||||
/* authMethod := fb.GetSettings().AuthMethod
|
||||
auther, err := fb.GetAuther(authMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} */
|
||||
|
||||
e := &env{
|
||||
FileBrowser: fb,
|
||||
Auther: auther,
|
||||
}
|
||||
e := &env{}
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
@ -109,10 +109,10 @@ func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) {
|
||||
func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) {
|
||||
id := r.Context().Value(keyUserID).(uint)
|
||||
user, err := e.GetUser(id)
|
||||
if err == lib.ErrNotExist {
|
||||
user, err := e.Users.Get(id)
|
||||
if err == errors.ErrNotExist {
|
||||
httpErr(w, r, http.StatusForbidden, nil)
|
||||
return nil, false
|
||||
}
|
||||
@ -125,7 +125,7 @@ func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool)
|
||||
return user, true
|
||||
}
|
||||
|
||||
func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*lib.User, bool) {
|
||||
func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) {
|
||||
user, ok := e.getUser(w, r)
|
||||
if !ok {
|
||||
return nil, false
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -4,17 +4,17 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/filebrowser/filebrowser/lib"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/filebrowser/filebrowser/rules"
|
||||
"github.com/filebrowser/filebrowser/settings"
|
||||
)
|
||||
|
||||
type settingsData struct {
|
||||
Signup bool `json:"signup"`
|
||||
Defaults lib.UserDefaults `json:"defaults"`
|
||||
Rules []lib.Rule `json:"rules"`
|
||||
Branding lib.Branding `json:"branding"`
|
||||
Shell []string `json:"shell"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Signup bool `json:"signup"`
|
||||
Defaults settings.UserDefaults `json:"defaults"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Branding settings.Branding `json:"branding"`
|
||||
Shell []string `json:"shell"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
}
|
||||
|
||||
func (e *env) settingsGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@ -23,16 +23,19 @@ func (e *env) settingsGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
e.RLockSettings()
|
||||
defer e.RUnlockSettings()
|
||||
settings, err := e.Settings.Get()
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
data := &settingsData{
|
||||
Signup: e.GetSettings().Signup,
|
||||
Defaults: e.GetSettings().Defaults,
|
||||
Rules: e.GetSettings().Rules,
|
||||
Branding: e.GetSettings().Branding,
|
||||
Shell: e.GetSettings().Shell,
|
||||
Commands: e.GetSettings().Commands,
|
||||
Signup: settings.Signup,
|
||||
Defaults: settings.Defaults,
|
||||
Rules: settings.Rules,
|
||||
Branding: settings.Branding,
|
||||
Shell: settings.Shell,
|
||||
Commands: settings.Commands,
|
||||
}
|
||||
|
||||
renderJSON(w, r, data)
|
||||
@ -51,10 +54,11 @@ func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
e.RLockSettings()
|
||||
settings := &lib.Settings{}
|
||||
err = copier.Copy(settings, e.GetSettings())
|
||||
e.RUnlockSettings()
|
||||
settings, err := e.Settings.Get()
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
@ -68,7 +72,7 @@ func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
settings.Shell = req.Shell
|
||||
settings.Commands = req.Commands
|
||||
|
||||
err = e.SaveSettings(settings)
|
||||
err = e.Settings.Save(settings)
|
||||
if err != nil {
|
||||
httpErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{} {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
14
lib/auth.go
14
lib/auth.go
@ -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)
|
||||
}
|
||||
@ -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")
|
||||
)
|
||||
@ -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()
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
199
lib/storage.go
199
lib/storage.go
@ -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)
|
||||
}
|
||||
37
lib/utils.go
37
lib/utils.go
@ -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
|
||||
}
|
||||
@ -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
8
settings/branding.go
Normal 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
27
settings/defaults.go
Normal 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
24
settings/settings.go
Normal 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
88
settings/storage.go
Normal 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
|
||||
}
|
||||
@ -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
45
share/storage.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
@ -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
17
storage/storage.go
Normal 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
10
storage/utils.go
Normal 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
17
users/password.go
Normal 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
13
users/permissions.go
Normal 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
101
users/storage.go
Normal 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
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
package lib
|
||||
package users
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/errors"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/filebrowser/filebrowser/files"
|
||||
"github.com/filebrowser/filebrowser/rules"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@ -15,32 +18,25 @@ const (
|
||||
MosaicViewMode ViewMode = "mosaic"
|
||||
)
|
||||
|
||||
// Permissions describe a user's permissions.
|
||||
type Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
Create bool `json:"create"`
|
||||
Rename bool `json:"rename"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Share bool `json:"share"`
|
||||
Download bool `json:"download"`
|
||||
}
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode ViewMode `json:"viewMode"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-"`
|
||||
Rules []Rule `json:"rules"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode ViewMode `json:"viewMode"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
}
|
||||
|
||||
// GetRules implements rules.Provider.
|
||||
func (u *User) GetRules() []rules.Rule {
|
||||
return u.Rules
|
||||
}
|
||||
|
||||
var checkableFields = []string{
|
||||
@ -53,7 +49,9 @@ var checkableFields = []string{
|
||||
"Rules",
|
||||
}
|
||||
|
||||
func (u *User) clean(fields ...string) error {
|
||||
// Clean cleans up a user and verifies if all its fields
|
||||
// are alright to be saved.
|
||||
func (u *User) Clean(fields ...string) error {
|
||||
if len(fields) == 0 {
|
||||
fields = checkableFields
|
||||
}
|
||||
@ -62,15 +60,15 @@ func (u *User) clean(fields ...string) error {
|
||||
switch field {
|
||||
case "Username":
|
||||
if u.Username == "" {
|
||||
return ErrEmptyUsername
|
||||
return errors.ErrEmptyUsername
|
||||
}
|
||||
case "Password":
|
||||
if u.Password == "" {
|
||||
return ErrEmptyPassword
|
||||
return errors.ErrEmptyPassword
|
||||
}
|
||||
case "Scope":
|
||||
if !filepath.IsAbs(u.Scope) {
|
||||
return ErrPathIsRel
|
||||
return errors.ErrScopeIsRelative
|
||||
}
|
||||
case "ViewMode":
|
||||
if u.ViewMode == "" {
|
||||
@ -86,7 +84,7 @@ func (u *User) clean(fields ...string) error {
|
||||
}
|
||||
case "Rules":
|
||||
if u.Rules == nil {
|
||||
u.Rules = []Rule{}
|
||||
u.Rules = []rules.Rule{}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package lib
|
||||
package version
|
||||
|
||||
const (
|
||||
// Version is the current File Browser version.
|
||||
Loading…
Reference in New Issue
Block a user