├── .github └── workflows │ ├── changelog.yml │ ├── go.yml │ ├── node.yml │ └── release.yml ├── .gitignore ├── .vscode └── launch.json ├── CONTRIBUTION.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── flydav │ ├── app │ ├── args.go │ ├── entry.go │ ├── logger.go │ └── webdav.go │ ├── conf │ └── conf.go │ ├── main.go │ └── service │ └── auth.go ├── conf ├── config.default.toml ├── config.default.yaml └── config.example.toml ├── docs └── README.zh-CN.md ├── go.mod ├── go.sum ├── pkg ├── logger │ ├── lib.go │ └── lib_test.go └── misc │ ├── lib.go │ └── lib_test.go ├── scripts ├── installer.sh └── ui_installer.sh └── ui ├── .eslintcache ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── commitlint.config.js ├── index.html ├── jest.config.js ├── lint-staged.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.svg └── manifest.webmanifest ├── src ├── app │ ├── app.module.css │ ├── app.tsx │ ├── settings.module.css │ ├── settings.tsx │ └── tmp.md ├── components │ ├── progress_bar.module.css │ └── progress_bar.tsx ├── index.css ├── infrastructure │ └── tests │ │ └── setup-tests.ts ├── main.tsx ├── shared.module.css └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Changelog 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | # Fetch depth 0 is required for Changelog generation 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Create changelog text 17 | id: changelog 18 | uses: loopwerk/tag-changelog@v1 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - name: Print changelog 23 | run: | 24 | cat < 0 { 38 | w.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", conf.CORS.MaxAge)) 39 | } 40 | if len(conf.CORS.ExposedHeaders) > 0 { 41 | w.Header().Set("Access-Control-Expose-Headers", strings.Join(conf.CORS.ExposedHeaders, ",")) 42 | } 43 | 44 | next.ServeHTTP(w, r) 45 | }) 46 | }) 47 | } 48 | 49 | if conf.UI.Enabled { 50 | prefix := filepath.Join(conf.UI.Path) 51 | // http.Handle(prefix, http.StripPrefix(prefix, http.FileServer(http.Dir(conf.UI.Source)))) 52 | server.AddMiddleware(func(next http.HandlerFunc) http.HandlerFunc { 53 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | if strings.HasPrefix(r.URL.Path, prefix) { 55 | http.StripPrefix(prefix, http.FileServer(http.Dir(conf.UI.Source))).ServeHTTP(w, r) 56 | return 57 | } 58 | if strings.HasPrefix(r.URL.Path, "/assets") { 59 | http.StripPrefix("/assets", http.FileServer(http.Dir(conf.UI.Source+"/assets"))).ServeHTTP(w, r) 60 | } 61 | next.ServeHTTP(w, r) 62 | }) 63 | }) 64 | fmt.Println("UI: ", fmt.Sprintf("http://%s:%d%s", conf.Server.Host, conf.Server.Port, conf.UI.Path)) 65 | } 66 | server.Listen() 67 | } 68 | -------------------------------------------------------------------------------- /cmd/flydav/app/logger.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/natefinch/lumberjack" 8 | "github.com/pluveto/flydav/cmd/flydav/conf" 9 | "github.com/pluveto/flydav/pkg/logger" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func InitLogger(cnf conf.Log, verbose bool) { 14 | if(verbose){ 15 | println("verbose mode enabled") 16 | } 17 | newLoggerCount := len(cnf.Stdout) + len(cnf.File) 18 | if newLoggerCount != 0 { 19 | for i := 0; i < newLoggerCount; i++ { 20 | logger.AddLogger(logrus.New()) 21 | } 22 | } else { 23 | // if no logger configured, use default logger 24 | if(verbose){ 25 | println("no logger configured, use default logger") 26 | } 27 | logger.SetOutput(os.Stdout) 28 | return 29 | } 30 | nextLoggerIndex := 1 31 | 32 | if verbose { 33 | logger.SetLevel(logrus.DebugLevel) 34 | // enable source code line numbers 35 | logger.SetReportCaller(true) 36 | } else { 37 | println("Verbose mode disabled") 38 | logger.SetLevel(levelToLogrusLevel(cnf.Level)) 39 | } 40 | 41 | for _, stdout := range cnf.Stdout { 42 | currentLogger := logger.DefaultCombinedLogger.GetLogger(nextLoggerIndex) 43 | switch stdout.Format { 44 | case conf.LogFormatJSON: 45 | currentLogger.SetFormatter(&logrus.JSONFormatter{}) 46 | case conf.LogFormatText: 47 | currentLogger.SetFormatter(&logrus.TextFormatter{}) 48 | } 49 | switch stdout.Output { 50 | case conf.LogOutputStdout: 51 | currentLogger.SetOutput(os.Stdout) 52 | case conf.LogOutputStderr: 53 | currentLogger.SetOutput(os.Stderr) 54 | } 55 | nextLoggerIndex++ 56 | } 57 | 58 | for _, file := range cnf.File { 59 | currentLogger := logger.DefaultCombinedLogger.GetLogger(nextLoggerIndex) 60 | 61 | switch file.Format { 62 | case conf.LogFormatJSON: 63 | currentLogger.SetFormatter(&logrus.JSONFormatter{}) 64 | case conf.LogFormatText: 65 | currentLogger.SetFormatter(&logrus.TextFormatter{}) 66 | } 67 | currentLogger.SetOutput(&lumberjack.Logger{ 68 | Filename: file.Path, 69 | MaxSize: file.MaxSize, 70 | MaxAge: file.MaxAge, 71 | MaxBackups: 3, 72 | Compress: true, 73 | }) 74 | nextLoggerIndex++ 75 | } 76 | 77 | } 78 | 79 | // levelToLogrusLevel converts a string to a logrus.Level 80 | func levelToLogrusLevel(level string) logrus.Level { 81 | level = strings.ToLower(level) 82 | switch level { 83 | case "debug": 84 | return logrus.DebugLevel 85 | case "info": 86 | return logrus.InfoLevel 87 | case "warn": 88 | return logrus.WarnLevel 89 | case "warning": 90 | return logrus.WarnLevel 91 | case "error": 92 | return logrus.ErrorLevel 93 | case "fatal": 94 | return logrus.FatalLevel 95 | case "panic": 96 | return logrus.PanicLevel 97 | default: 98 | return logrus.InfoLevel 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cmd/flydav/app/webdav.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/pluveto/flydav/pkg/logger" 11 | "github.com/sirupsen/logrus" 12 | "golang.org/x/net/webdav" 13 | ) 14 | 15 | type AuthService interface { 16 | Authenticate(username, password string) error 17 | GetAuthorizedSubDir(username string) (string, error) 18 | GetPathPrefix(username string) (string, error) 19 | } 20 | 21 | type WebdavServer struct { 22 | AuthService AuthService 23 | Host string 24 | Port int 25 | Path string 26 | FsDir string 27 | Middlewares []func(http.HandlerFunc) http.HandlerFunc 28 | } 29 | 30 | func NewWebdavServer(authService AuthService, host string, port int, path string, fsDir string) *WebdavServer { 31 | return &WebdavServer{ 32 | AuthService: authService, 33 | Host: host, 34 | Port: port, 35 | Path: path, 36 | FsDir: fsDir, 37 | } 38 | } 39 | 40 | func (s *WebdavServer) AddMiddleware(middleware func(http.HandlerFunc) http.HandlerFunc) { 41 | s.Middlewares = append(s.Middlewares, middleware) 42 | } 43 | 44 | func (s *WebdavServer) check() { 45 | if nil == s.AuthService { 46 | logger.Fatal("AuthService is nil") 47 | } 48 | if s.FsDir == "" { 49 | logger.Fatal("FsDir is empty") 50 | } 51 | // if !path.IsAbs(s.FsDir) { 52 | // logger.Fatal("FsDir is not an absolute path") 53 | // } 54 | 55 | var err error 56 | s.FsDir, err = filepath.Abs(s.FsDir) 57 | if err != nil { 58 | logger.Fatal("FsDir is not a valid path", err) 59 | } 60 | 61 | // must exists 62 | if _, err := os.Stat(s.FsDir); os.IsNotExist(err) { 63 | logger.Fatal("FsDir does not exist", err) 64 | } 65 | 66 | if !strings.HasPrefix(s.FsDir, "/home/") { 67 | webdavTmpDir := filepath.Join(os.TempDir(), "flydav") 68 | if !strings.HasPrefix(s.FsDir, webdavTmpDir) { 69 | logger.Warn("You're using a path which isn't under /home/ as mapped directory. This may cause security issues.") 70 | } 71 | } 72 | logger.Debug("FsDir: ", s.FsDir) 73 | } 74 | 75 | func (s *WebdavServer) wrapHandler(h http.HandlerFunc) http.HandlerFunc { 76 | return func(w http.ResponseWriter, r *http.Request) { 77 | for _, middleware := range s.Middlewares { 78 | h = middleware(h) 79 | } 80 | h(w, r) 81 | } 82 | } 83 | 84 | func (s *WebdavServer) Listen() { 85 | s.check() 86 | 87 | lock := webdav.NewMemLS() 88 | http.HandleFunc("/", s.wrapHandler(func(w http.ResponseWriter, r *http.Request) { 89 | logger.Info("request: ", r.Method, r.URL.Path) 90 | username, password, ok := r.BasicAuth() 91 | if !ok { 92 | w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) 93 | http.Error(w, "Unauthorized.", http.StatusUnauthorized) 94 | return 95 | } 96 | err := s.AuthService.Authenticate(username, password) 97 | if err != nil { 98 | w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) 99 | http.Error(w, "Unauthorized.", http.StatusUnauthorized) 100 | logger.Error("Unauthorized: ", err) 101 | return 102 | } 103 | subFsDir, err := s.AuthService.GetAuthorizedSubDir(username) 104 | if err != nil { 105 | http.Error(w, "Internal Error.", http.StatusInternalServerError) 106 | logger.Errorf("Error when getting authorized sub dir for user %s: %s", username, err) 107 | return 108 | } 109 | userPrefix, err := s.AuthService.GetPathPrefix(username) 110 | if err != nil { 111 | http.Error(w, "Internal Error.", http.StatusInternalServerError) 112 | logger.Errorf("Error when getting path prefix for user %s: %s", username, err) 113 | } 114 | davHandler := &webdav.Handler{ 115 | Prefix: buildPathPrefix(s.Path, userPrefix), 116 | FileSystem: buildDirName(s.FsDir, subFsDir), 117 | LockSystem: lock, 118 | Logger: davLogger, 119 | } 120 | 121 | davHandler.ServeHTTP(w, r) 122 | })) 123 | 124 | addr := fmt.Sprintf("%s:%d", s.Host, s.Port) 125 | err := http.ListenAndServe(addr, nil) 126 | logger.Fatal("failed to listen and serve on", addr, ":", err) 127 | } 128 | 129 | func buildDirName(fsDir, subFsDir string) webdav.Dir { 130 | if subFsDir == "" { 131 | return webdav.Dir(fsDir) 132 | } 133 | dir := filepath.Join(fsDir, subFsDir) 134 | if _, err := os.Stat(dir); os.IsNotExist(err) { 135 | os.MkdirAll(dir, 0755) 136 | } 137 | return webdav.Dir(dir) 138 | } 139 | 140 | func buildPathPrefix(path, userPrefix string) string { 141 | if userPrefix == "" { 142 | return path 143 | } 144 | return filepath.Join(path, userPrefix) 145 | } 146 | 147 | func davLogger(r *http.Request, err error) { 148 | ent := logger.WithFields(logrus.Fields{ 149 | "method": r.Method, 150 | "path": r.URL.Path, 151 | }) 152 | if err != nil { 153 | ent.Error(err) 154 | } else { 155 | ent.Info() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /cmd/flydav/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/pluveto/flydav/pkg/logger" 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | type HashMethond string 13 | 14 | const BcryptHash HashMethond = "bcrypt" 15 | const SHA256Hash HashMethond = "sha256" 16 | 17 | func GetDefaultConf() Conf { 18 | defaultFsDir, _ := os.Getwd() 19 | if !strings.HasPrefix(defaultFsDir, "/home") { 20 | webdavDir := filepath.Join(os.TempDir(), "flydav") 21 | err := os.MkdirAll(webdavDir, 0755) 22 | if err != nil { 23 | logger.Fatal("Failed to create webdav tmp dir", err) 24 | } 25 | defaultFsDir = webdavDir 26 | } 27 | return Conf{ 28 | Log: Log{ 29 | Level: "warn", 30 | Stdout: []Stdout{}, 31 | File: []File{}, 32 | }, 33 | Server: Server{ 34 | Host: "127.0.0.1", 35 | Port: 7086, 36 | Path: "/webdav", 37 | FsDir: defaultFsDir, 38 | }, 39 | Auth: Auth{ 40 | User: []User{ 41 | { 42 | Username: "flydav", 43 | PasswordHash: (func() string { 44 | b, _ := bcrypt.GenerateFromPassword([]byte("flydavdefaultpassword"), bcrypt.DefaultCost) 45 | return string(b) 46 | })(), 47 | PasswordCrypt: BcryptHash, 48 | }, 49 | }, 50 | }, 51 | UI: UI{ 52 | Enabled: false, 53 | Path: "/ui", 54 | Source: "", 55 | }, 56 | CORS: CORS{ 57 | Enabled: false, 58 | }, 59 | } 60 | } 61 | 62 | type Conf struct { 63 | Log Log `toml:"log" yaml:"log"` 64 | Server Server `toml:"server" yaml:"server"` 65 | Auth Auth `toml:"auth" yaml:"auth"` 66 | UI UI `toml:"ui" yaml:"ui"` 67 | CORS CORS `toml:"cors" yaml:"cors"` 68 | } 69 | 70 | type CORS struct { 71 | Enabled bool `toml:"enabled" yaml:"enabled"` 72 | AllowedOrigins []string `toml:"allowed_origins" yaml:"allowed_origins"` 73 | AllowedMethods []string `toml:"allowed_methods" yaml:"allowed_methods"` 74 | AllowedHeaders []string `toml:"allowed_headers" yaml:"allowed_headers"` 75 | ExposedHeaders []string `toml:"exposed_headers" yaml:"exposed_headers"` 76 | AllowCredentials bool `toml:"allow_credentials" yaml:"allow_credentials"` 77 | MaxAge int 78 | } 79 | 80 | type Server struct { 81 | Host string `toml:"host" yaml:"host"` 82 | Port int `toml:"port" yaml:"port"` 83 | Path string `toml:"path" yaml:"path"` 84 | FsDir string `toml:"fs_dir" yaml:"fs_dir"` 85 | } 86 | 87 | type UI struct { 88 | Enabled bool `toml:"enabled" yaml:"enabled"` 89 | Path string `toml:"path" yaml:"path"` // Path prefix. TODO: ui.path cannot equals to server.path 90 | Source string `toml:"source" yaml:"source"` // Source location of the UI 91 | } 92 | 93 | type User struct { 94 | SubPath string `toml:"sub_path" yaml:"sub_path"` 95 | SubFsDir string `toml:"sub_fs_dir" yaml:"sub_fs_dir"` 96 | Username string `toml:"username" yaml:"username"` 97 | PasswordHash string `toml:"password_hash" yaml:"password_hash"` 98 | PasswordCrypt HashMethond `toml:"password_crypt" yaml:"password_crypt"` 99 | } 100 | type Auth struct { 101 | User []User `toml:"user" yaml:"user"` 102 | } 103 | 104 | type File struct { 105 | Format LogFormat `toml:"format" yaml:"format"` 106 | Path string `toml:"path" yaml:"path"` 107 | MaxSize int `toml:"max_size" yaml:"max_size"` 108 | MaxAge int `toml:"max_age" yaml:"max_age"` 109 | } 110 | 111 | type LogFormat string 112 | 113 | const ( 114 | LogFormatJSON LogFormat = "json" 115 | LogFormatText LogFormat = "text" 116 | ) 117 | 118 | type LogOutput string 119 | 120 | const ( 121 | LogOutputStdout LogOutput = "stdout" 122 | LogOutputStderr LogOutput = "stderr" 123 | LogOutputFile LogOutput = "file" 124 | ) 125 | 126 | type Stdout struct { 127 | Format LogFormat `toml:"format" yaml:"format"` 128 | Output LogOutput `toml:"output" yaml:"output"` 129 | } 130 | type Log struct { 131 | Level string `toml:"level" yaml:"level"` 132 | File []File `toml:"file" yaml:"file"` 133 | Stdout []Stdout `toml:"stdout" yaml:"stdout"` 134 | } 135 | -------------------------------------------------------------------------------- /cmd/flydav/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/BurntSushi/toml" 9 | "github.com/alexflint/go-arg" 10 | "github.com/pluveto/flydav/cmd/flydav/app" 11 | "github.com/pluveto/flydav/cmd/flydav/conf" 12 | "github.com/pluveto/flydav/pkg/logger" 13 | "github.com/pluveto/flydav/pkg/misc" 14 | "github.com/sirupsen/logrus" 15 | "golang.org/x/crypto/bcrypt" 16 | "golang.org/x/term" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | // main Entry point of the application 21 | func main() { 22 | // main Entry point of the application 23 | var args app.Args 24 | var cnf conf.Conf 25 | var defaultConf = conf.GetDefaultConf() 26 | 27 | args = loadArgsValid() 28 | if args.Verbose { 29 | fmt.Printf("args: %+v\n", args) 30 | } 31 | 32 | cnf = loadConfValid(args.Verbose, args.Config, defaultConf, "config.toml") 33 | if args.Verbose { 34 | fmt.Printf("conf: %+v\n", cnf) 35 | } 36 | overrideConf(&cnf, args) 37 | validateConf(&cnf) 38 | app.InitLogger(cnf.Log, args.Verbose) 39 | logger.Debug("log level: ", logger.GetLevel()) 40 | app.Run(cnf) 41 | } 42 | 43 | func validateConf(conf *conf.Conf) { 44 | if len(conf.Auth.User) == 0 { 45 | logger.Fatal("No user configured") 46 | } 47 | if conf.Auth.User[0].Username == "" { 48 | logger.Fatal("No username configured") 49 | } 50 | if conf.Auth.User[0].PasswordHash == "" { 51 | logger.Fatal("No password configured") 52 | } 53 | } 54 | 55 | func overrideConf(cnf *conf.Conf, args app.Args) { 56 | if args.Verbose { 57 | cnf.Log.Level = logrus.DebugLevel.String() 58 | } 59 | if args.Port != 0 { 60 | cnf.Server.Port = args.Port 61 | } 62 | if args.Host != "" { 63 | cnf.Server.Host = args.Host 64 | } 65 | if args.Username == "" { 66 | args.Username = "flydav" 67 | } 68 | if args.Config == "" { 69 | cnf.Auth.User = []conf.User{ 70 | { 71 | Username: args.Username, 72 | PasswordHash: promptPassword(args.Username), 73 | PasswordCrypt: "bcrypt", 74 | }, 75 | } 76 | } 77 | if args.EnabledUI { 78 | cnf.UI.Enabled = true 79 | } 80 | } 81 | 82 | func promptPassword(username string) string { 83 | var err error 84 | var password []byte 85 | MIN_PASS_LEN := 9 86 | fmt.Printf("Set a temporary password for user %s (at least %d chars): ", username, MIN_PASS_LEN) 87 | for { 88 | password, err = term.ReadPassword(int(os.Stdin.Fd())) 89 | fmt.Println() 90 | if err == nil && len(password) >= MIN_PASS_LEN { 91 | break 92 | } 93 | fmt.Printf("Invalid password. Must be at least %d chars. Try agin: ", MIN_PASS_LEN) 94 | } 95 | b, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 96 | return string(b) 97 | } 98 | 99 | func loadArgsValid() app.Args { 100 | var args app.Args 101 | arg.MustParse(&args) 102 | return args 103 | } 104 | 105 | func getAppDir() string { 106 | dir, err := os.Executable() 107 | if err != nil { 108 | logger.Fatal(err) 109 | } 110 | return filepath.Dir(dir) 111 | } 112 | 113 | func loadConfValid(verbose bool, path string, defaultConf conf.Conf, defaultConfPath string) conf.Conf { 114 | if path == "" { 115 | path = defaultConfPath 116 | if verbose { 117 | fmt.Println("no config file specified, using default config file: ", path) 118 | } 119 | } 120 | // app executable dir + config.toml has the highest priority 121 | preferredPath := filepath.Join(getAppDir(), path) 122 | if _, err := os.Stat(preferredPath); err == nil { 123 | path = preferredPath 124 | if verbose { 125 | fmt.Println("using config file: ", path) 126 | } 127 | } 128 | 129 | err := decode(path, &defaultConf) 130 | if err != nil && verbose { 131 | os.Stderr.WriteString(fmt.Sprintf("Failed to load config file: %s\n", err)) 132 | }else 133 | { 134 | logger.WithField("conf", &defaultConf).Debug("configuration loaded") 135 | } 136 | return defaultConf 137 | } 138 | 139 | func decode(path string, conf *conf.Conf) (error) { 140 | ext, err := misc.MustGetFileExt(path) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | switch ext { 146 | case "toml": 147 | _, err = toml.DecodeFile(path, conf) 148 | case "yaml", "yml": 149 | content, err := os.ReadFile(path) 150 | if err != nil { 151 | return fmt.Errorf("failed to read config file: %s", err) 152 | } 153 | 154 | err = yaml.Unmarshal([]byte(content), conf) 155 | if err != nil { 156 | return fmt.Errorf("failed to parse config file: %s", err) 157 | } 158 | default: 159 | err = fmt.Errorf("unsupported config file extension: %s", ext) 160 | } 161 | 162 | return err 163 | } 164 | -------------------------------------------------------------------------------- /cmd/flydav/service/auth.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "errors" 7 | 8 | "github.com/pluveto/flydav/cmd/flydav/conf" 9 | "github.com/pluveto/flydav/pkg/logger" 10 | "golang.org/x/crypto/bcrypt" 11 | ) 12 | 13 | type BasicAuthService struct { 14 | UserMap map[string]conf.User 15 | } 16 | 17 | func NewBasicAuthService(users []conf.User) *BasicAuthService { 18 | ret := &BasicAuthService{} 19 | ret.UserMap = make(map[string]conf.User) 20 | for _, user := range users { 21 | ret.UserMap[user.Username] = user 22 | } 23 | return ret 24 | } 25 | 26 | var ( 27 | ErrCrendential = errors.New("invalid username or password") 28 | ErrUnsupportedHashMethod = errors.New("unsupported hash method") 29 | ) 30 | 31 | func (s *BasicAuthService) Authenticate(username, password string) error { 32 | user, ok := s.UserMap[username] 33 | if !ok { 34 | logger.Debug("no such user: ", username) 35 | return ErrCrendential 36 | } 37 | hashMethod := user.PasswordCrypt 38 | // todo: sha256 39 | switch hashMethod { 40 | case conf.BcryptHash: 41 | err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) 42 | if user.Username == username && err == nil { 43 | return nil 44 | } 45 | logger.Debug("bcrypt compare error: ", err) 46 | case conf.SHA256Hash: 47 | gen := sha256.New() 48 | gen.Write([]byte(password)) 49 | expectedHash := hex.EncodeToString(gen.Sum(nil)) 50 | if user.Username == username && user.PasswordHash == expectedHash { 51 | return nil 52 | } 53 | logger.Debug("sha256 compare error, expected hash: ", user.PasswordHash, ", actual hash: ", expectedHash) 54 | default: 55 | return ErrUnsupportedHashMethod 56 | } 57 | return ErrCrendential 58 | 59 | } 60 | 61 | func (s *BasicAuthService) GetAuthorizedSubDir(username string) (string, error) { 62 | user, ok := s.UserMap[username] 63 | if !ok { 64 | return "", errors.New("no such user") 65 | } 66 | return user.SubFsDir, nil 67 | } 68 | func (s *BasicAuthService) GetPathPrefix(username string) (string, error) { 69 | user, ok := s.UserMap[username] 70 | if !ok { 71 | return "", errors.New("no such user") 72 | } 73 | return user.SubPath, nil 74 | } 75 | -------------------------------------------------------------------------------- /conf/config.default.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | host = "0.0.0.0" 3 | port = 7086 4 | path = "/webdav" 5 | fs_dir = "/tmp/flydav" 6 | 7 | [ui] 8 | enabled = false 9 | path = "/ui" 10 | source = "/usr/share/flydav/ui" 11 | 12 | [auth] 13 | 14 | # add more users here 15 | # note: the above line is required by auto install script, do not delete. 16 | 17 | [log] 18 | level = "Warning" 19 | [[log.file]] 20 | format = "json" 21 | path = "/var/log/flydav.log" 22 | max_size = 1 # megabytes 23 | max_age = 28 # days 24 | 25 | [[log.stdout]] 26 | format = "text" # or "text" 27 | output = "stdout" # or "stderr" 28 | 29 | [cors] 30 | enabled = true 31 | allowed_origins = ["*"] 32 | allowed_methods = ["GET", "POST", "PUT", "DELETE", "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "OPTIONS", "HEAD", "PATCH"] 33 | allowed_headers = ["*"] 34 | exposed_headers = ["*"] 35 | allow_credentials = true 36 | max_age = 86400 # seconds 37 | -------------------------------------------------------------------------------- /conf/config.default.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | host: 0.0.0.0 3 | port: 7000 4 | path: /webdav 5 | fs_dir: /tmp/flydav 6 | ui: 7 | enabled: false 8 | path: /ui 9 | source: /usr/share/flydav/ui 10 | auth: {} 11 | log: 12 | level: Warning 13 | file: 14 | - format: json 15 | path: /var/log/flydav.log 16 | max_size: 1 17 | max_age: 28 18 | stdout: 19 | - format: text 20 | output: stdout 21 | cors: 22 | enabled: true 23 | allowed_origins: 24 | - '*' 25 | allowed_methods: 26 | - GET 27 | - POST 28 | - PUT 29 | - DELETE 30 | - PROPFIND 31 | - PROPPATCH 32 | - MKCOL 33 | - COPY 34 | - MOVE 35 | - LOCK 36 | - UNLOCK 37 | - OPTIONS 38 | - HEAD 39 | - PATCH 40 | allowed_headers: 41 | - '*' 42 | exposed_headers: 43 | - '*' 44 | allow_credentials: true 45 | max_age: 86400 46 | -------------------------------------------------------------------------------- /conf/config.example.toml: -------------------------------------------------------------------------------- 1 | [log] 2 | level = "Warning" 3 | 4 | [[log.file]] 5 | format = "json" 6 | path = "flydav.log" 7 | max_size = 1 # megabytes 8 | max_age = 28 # days 9 | 10 | [[log.stdout]] 11 | format = "text" # or "text" 12 | output = "stdout" # or "stderr" 13 | -------------------------------------------------------------------------------- /docs/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Flydav 2 | 3 | FlyDav 是一个轻量级的开源 webdav 服务器,它提供的一些核心功能可以满足个人用户和组织的需求。 4 | 5 | FlyDav 的文件大小很小,对于需要快速高效的 webdav 服务器的用户来说,它是理想的解决方案。它提供基本认证,支持多用户,并允许每个用户拥有不同的根目录和路径前缀,这些都是非常好的隔离。此外,FlyDav 还提供了日志轮换和密码安全,是需要安全可靠的 webdav 解决方案的用户的最佳选择。 6 | 7 | FlyDav 的目标是保持方便和简洁,用户能在短时间内将服务部署起来。因此任何无关紧要的功能都不会被加入,从而避免臃肿。 8 | 9 | ## 在 30 秒内开始使用 10 | 11 | 1. 首先从 [发布页](https://github.com/pluveto/flydav/releases) 下载 FlyDav。 12 | 2. 运行 `./flydav -H 0.0.0.0` 来启动服务器。然后你要输入默认用户 `flydav` 的密码。 13 | 3. 在你的 webdav 客户端(比如 RaiDrive)中打开 `http://YOUR_IP:7086/webdav`。 14 | 15 | ## 命令行选项 16 | 17 | ```bash 18 | $ flydav -h 19 | -------------------------------------------------------------------------------- 20 | 用法: flydav [--host HOST] [--port PORT] [--user USER] [--verbose] [--config CONFIG] 。 21 | 22 | 选项: 23 | --host HOST, -H HOST 主机地址 24 | --port PORT, -p PORT 端口 25 | --user USER, -u USER 用户名 26 | --verbose, -v 启用详细输出输出(如果你打算报告或调试错误,这将非常有用) 27 | --config CONFIG, -c CONFIG 28 | 配置文件 29 | --help, -h 显示此帮助并退出 30 | ``` 31 | 32 | 如果你有一个配置文件,你可以忽略这些命令行选项。运行 `flydav -c /path/to/config.toml` 来启动服务器。 33 | 34 | 如果你想用主机、端口、用户名和一次性密码快速启动服务器,你可以运行 `flydav -H IP -p PORT -u USERNAME` 来启动服务器。然后你再输入用户的密码。然后服务器将在 `http://IP:PORT/` 提供服务。 35 | 36 | ## 配置 FlyDav 37 | 38 | 尽管 FlyDav 提供了一些命令行选项,但是你也可以使用配置文件来配置它。从而避免在每次启动服务器时都输入密码。 39 | 40 | 1. 下载 FlyDav。 41 | 2. 现在你有了这个软件,你需要为它创建一个配置文件。首先创建一个名为 "flydav.toml" 的新文件。 42 | 3. 在配置文件中,你将需要添加以下信息。 43 | - `[服务器]`。这一部分将定义 webdav 服务器的主机、端口和路径。 44 | - `host`: 主机的 IP 地址。如果你想让任何 IP 地址都能访问该服务器,这应该被设置为 "0.0.0.0"。 45 | - `port`: webdav 服务器要使用的端口号。 46 | - `path`: webdav 服务器的路径。 47 | - `fs_dir`: 服务器上存放 webdav 文件的目录。 48 | - `[auth]`: 这一部分将定义 webdav 服务器的认证设置。 49 | - `[[auth.user]]`: 这一节将为每个可以访问 webdav 服务器的用户定义用户名和凭证。 50 | - `username`: 用户的用户名。 51 | - `sub_fs_dir': 用户可以访问的 fs_dir 的子目录。 52 | - `sub_path`: 用户访问 webdav 服务器的路径 53 | - `password_hash`: 用户的散列密码。 54 | - `password_crypt`: 用于哈希密码的哈希算法的类型。这应该被设置为 "bcrypt" 或 "sha256"。 55 | - `[log]`: 这一部分将定义 webdav 服务器的日志设置。 56 | - `level`: 服务器的日志级别。这可以设置为 "debug"、"info"、"warning"、"error" 或 "fatal"。 57 | - `[[log.file]]`。这个小节将定义日志文件的设置。如果你不想将日志记录到一个文件中,请忽略这个小节。 58 | - `format`: 日志文件的格式。这可以设置为 "json" 或 "text"。 59 | - `path`: 日志文件的路径。 60 | - `max_size`: 日志文件的最大尺寸,以兆字节为单位。 61 | - `max_age`: 日志文件的最大年龄,以天为单位。 62 | - `[[log.stdout]]`: 本小节将定义日志输出到控制台的设置。如果你不想向控制台输出日志,请忽略这个小节。 63 | - `format`: 日志输出的格式。可以设置为 "json" 或 "text"。 64 | - `output`: 日志输出的输出流。可以设置为 "stdout" 或 "stderr"。 65 | 66 | 4. 保存配置文件并运行 FlyDav 服务器。现在你应该可以用配置好的设置访问 webdav 服务器了。 67 | 68 | 要获得一个配置文件的例子,请到 [conf dir](https://github.com/pluveto/flydav/blob/main/conf)。 69 | 70 | ## 以服务方式安装 71 | 72 | ### 在 Linux 上以服务方式安装 73 | 74 | 1. 创建编辑 `/etc/systemd/system/flydav.service`,并添加以下内容: 75 | 76 | ```ini 77 | [Unit] 78 | Description = Flydav Server 79 | After = network.target syslog.target 80 | Wants = network.target 81 | 82 | [Service] 83 | Type = simple 84 | # !!! 把配置文件和程序位置改成你自己的 !!! 85 | ExecStart = /usr/bin/flydav -c /etc/flydav/flydav.toml 86 | 87 | [Install] 88 | WantedBy = multi-user.target 89 | ``` 90 | 91 | 2. 运行 `systemctl daemon-reload`,重新加载systemd守护程序。 92 | 3. 运行 `systemctl enable flydav` 来启用该服务。 93 | 4. 运行 `systemctl start flydav` 来启动服务。 94 | 95 | ## 一键安装 96 | 97 | 运行: 98 | 99 | ```bash 100 | curl -s https://raw.githubusercontent.com/pluveto/flydav/main/scripts/install.sh | sudo bash 101 | ``` 102 | 103 | 开启代理方式运行: 104 | 105 | ```bash 106 | curl -s https://raw.githubusercontent.com/pluveto/flydav/main/scripts/install.sh | sudo http_proxy=http://192.168.56.1:7890 https_proxy=http://192.168.56.1:7890 bash 107 | ``` 108 | 109 | 然后按照提示输入配置信息,完成安装。 110 | 111 | ### 管理该服务 112 | 113 | - 运行 `systemctl status flydav` 来检查服务的状态。 114 | - 运行 `systemctl stop flydav` 来停止该服务。 115 | 116 | ## 功能 117 | 118 | - [x] 基本认证 119 | - [x] 多个用户 120 | - [x] 每个用户的根目录不同 121 | - [x] 每个用户有不同的路径前缀 122 | - [x] 日志 123 | - [ ] SSL 124 | - 正在支持 125 | - 你可以使用 Nginx 这样的反向代理来启用 SSL 126 | 127 | ## 许可证 128 | 129 | 在MIT许可下授权——详见[LICENSE](../LICENSE)文件 130 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pluveto/flydav 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.2.1 7 | github.com/alexflint/go-arg v1.4.3 8 | github.com/natefinch/lumberjack v2.0.0+incompatible 9 | github.com/sirupsen/logrus v1.9.0 10 | github.com/stretchr/testify v1.8.1 11 | golang.org/x/crypto v0.5.0 12 | golang.org/x/net v0.5.0 13 | golang.org/x/term v0.4.0 14 | ) 15 | 16 | require ( 17 | github.com/alexflint/go-scalar v1.1.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | golang.org/x/sys v0.4.0 // indirect 21 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 22 | gopkg.in/yaml.v2 v2.4.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= 4 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= 5 | github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= 6 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 11 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 15 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 18 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 19 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 23 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 24 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 25 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 26 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 27 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 28 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 29 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 31 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= 33 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 37 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 38 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 39 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 40 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /pkg/logger/lib.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var DefaultCombinedLogger = New() 12 | 13 | type nothingWriter struct{} 14 | 15 | func (nothingWriter) Write(p []byte) (n int, err error) { 16 | return len(p), nil 17 | } 18 | 19 | func init() { 20 | DefaultCombinedLogger.AddHook(&logHook{}) 21 | var nilWrite nothingWriter 22 | DefaultCombinedLogger.SetOutput(nilWrite) 23 | } 24 | 25 | type CombinedLogger struct { 26 | *logrus.Logger 27 | additionalLoggers []*logrus.Logger 28 | } 29 | 30 | func New() *CombinedLogger { 31 | return &CombinedLogger{ 32 | Logger: logrus.New(), 33 | additionalLoggers: []*logrus.Logger{}, 34 | } 35 | 36 | } 37 | 38 | type logHook struct { 39 | } 40 | 41 | func (hook *logHook) Levels() []logrus.Level { 42 | return logrus.AllLevels 43 | } 44 | 45 | func (hook *logHook) Fire(entry *logrus.Entry) error { 46 | for _, logger := range DefaultCombinedLogger.additionalLoggers { 47 | // pass entry to all additionalLoggers 48 | logger.WithFields(entry.Data).Log(entry.Level, entry.Message) 49 | } 50 | return nil 51 | } 52 | 53 | func AddLogger(logger *logrus.Logger) { 54 | DefaultCombinedLogger.additionalLoggers = append(DefaultCombinedLogger.additionalLoggers, logger) 55 | } 56 | 57 | func (logger *CombinedLogger) GetLogger(index int) *logrus.Logger { 58 | if index == 0 { 59 | return logger.Logger 60 | } 61 | if index > len(logger.additionalLoggers) || index < 0 { 62 | return nil 63 | } 64 | return logger.additionalLoggers[index-1] 65 | } 66 | 67 | func (logger *CombinedLogger) Apply(index int, fn func(*logrus.Logger)) { 68 | if index == 0 { 69 | fn(logger.Logger) 70 | return 71 | } 72 | if index > len(logger.additionalLoggers) || index < 0 { 73 | panic("index of logger out of range") 74 | } 75 | fn(logger.additionalLoggers[index-1]) 76 | } 77 | 78 | func (logger *CombinedLogger) ApplyAll(fn func(*logrus.Logger)) { 79 | fn(logger.Logger) 80 | for _, l := range logger.additionalLoggers { 81 | fn(l) 82 | } 83 | } 84 | 85 | // SetOutput sets the standard logger output. 86 | func SetOutput(out io.Writer) { 87 | DefaultCombinedLogger.ApplyAll( 88 | func(l *logrus.Logger) { 89 | l.SetOutput(out) 90 | }, 91 | ) 92 | } 93 | 94 | // SetFormatter sets the standard logger formatter. 95 | func SetFormatter(formatter logrus.Formatter) { 96 | DefaultCombinedLogger.ApplyAll(func(l *logrus.Logger) { 97 | l.SetFormatter(formatter) 98 | }) 99 | } 100 | 101 | // SetReportCaller sets whether the standard logger will include the calling 102 | // method as a field. 103 | func SetReportCaller(include bool) { 104 | DefaultCombinedLogger.ApplyAll(func(l *logrus.Logger) { 105 | l.SetReportCaller(include) 106 | }) 107 | } 108 | 109 | // SetLevel sets the standard logger level. 110 | func SetLevel(level logrus.Level) { 111 | DefaultCombinedLogger.ApplyAll(func(l *logrus.Logger) { 112 | l.SetLevel(level) 113 | }) 114 | } 115 | 116 | // GetLevel returns the standard logger level. 117 | func GetLevel() logrus.Level { 118 | return DefaultCombinedLogger.GetLevel() 119 | } 120 | 121 | // IsLevelEnabled checks if the log level of the standard logger is greater than the level param 122 | func IsLevelEnabled(level logrus.Level) bool { 123 | return DefaultCombinedLogger.IsLevelEnabled(level) 124 | } 125 | 126 | // AddHook adds a hook to the standard logger hooks. 127 | func AddHook(hook logrus.Hook) { 128 | DefaultCombinedLogger.AddHook(hook) 129 | } 130 | 131 | // WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key. 132 | func WithError(err error) *logrus.Entry { 133 | return DefaultCombinedLogger.WithField(logrus.ErrorKey, err) 134 | } 135 | 136 | // WithContext creates an entry from the standard logger and adds a context to it. 137 | func WithContext(ctx context.Context) *logrus.Entry { 138 | return DefaultCombinedLogger.WithContext(ctx) 139 | } 140 | 141 | // WithField creates an entry from the standard logger and adds a field to 142 | // it. If you want multiple fields, use `WithFields`. 143 | // 144 | // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal 145 | // or Panic on the Entry it returns. 146 | func WithField(key string, value interface{}) *logrus.Entry { 147 | return DefaultCombinedLogger.WithField(key, value) 148 | } 149 | 150 | // WithFields creates an entry from the standard logger and adds multiple 151 | // fields to it. This is simply a helper for `WithField`, invoking it 152 | // once for each field. 153 | // 154 | // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal 155 | // or Panic on the Entry it returns. 156 | func WithFields(fields logrus.Fields) *logrus.Entry { 157 | return DefaultCombinedLogger.WithFields(fields) 158 | } 159 | 160 | // WithTime creates an entry from the standard logger and overrides the time of 161 | // logs generated with it. 162 | // 163 | // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal 164 | // or Panic on the Entry it returns. 165 | func WithTime(t time.Time) *logrus.Entry { 166 | return DefaultCombinedLogger.WithTime(t) 167 | } 168 | 169 | // Trace logs a message at level Trace on the standard logger. 170 | func Trace(args ...interface{}) { 171 | DefaultCombinedLogger.Trace(args...) 172 | } 173 | 174 | // Debug logs a message at level Debug on the standard logger. 175 | func Debug(args ...interface{}) { 176 | DefaultCombinedLogger.Debug(args...) 177 | } 178 | 179 | // Print logs a message at level Info on the standard logger. 180 | func Print(args ...interface{}) { 181 | DefaultCombinedLogger.Print(args...) 182 | } 183 | 184 | // Info logs a message at level Info on the standard logger. 185 | func Info(args ...interface{}) { 186 | DefaultCombinedLogger.Info(args...) 187 | } 188 | 189 | // Warn logs a message at level Warn on the standard logger. 190 | func Warn(args ...interface{}) { 191 | DefaultCombinedLogger.Warn(args...) 192 | } 193 | 194 | // Warning logs a message at level Warn on the standard logger. 195 | func Warning(args ...interface{}) { 196 | DefaultCombinedLogger.Warning(args...) 197 | } 198 | 199 | // Error logs a message at level Error on the standard logger. 200 | func Error(args ...interface{}) { 201 | DefaultCombinedLogger.Error(args...) 202 | } 203 | 204 | // Panic logs a message at level Panic on the standard logger. 205 | func Panic(args ...interface{}) { 206 | DefaultCombinedLogger.Panic(args...) 207 | } 208 | 209 | // Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1. 210 | func Fatal(args ...interface{}) { 211 | DefaultCombinedLogger.Fatal(args...) 212 | } 213 | 214 | // TraceFn logs a message from a func at level Trace on the standard logger. 215 | func TraceFn(fn logrus.LogFunction) { 216 | DefaultCombinedLogger.TraceFn(fn) 217 | } 218 | 219 | // DebugFn logs a message from a func at level Debug on the standard logger. 220 | func DebugFn(fn logrus.LogFunction) { 221 | DefaultCombinedLogger.DebugFn(fn) 222 | } 223 | 224 | // PrintFn logs a message from a func at level Info on the standard logger. 225 | func PrintFn(fn logrus.LogFunction) { 226 | DefaultCombinedLogger.PrintFn(fn) 227 | } 228 | 229 | // InfoFn logs a message from a func at level Info on the standard logger. 230 | func InfoFn(fn logrus.LogFunction) { 231 | DefaultCombinedLogger.InfoFn(fn) 232 | } 233 | 234 | // WarnFn logs a message from a func at level Warn on the standard logger. 235 | func WarnFn(fn logrus.LogFunction) { 236 | DefaultCombinedLogger.WarnFn(fn) 237 | } 238 | 239 | // WarningFn logs a message from a func at level Warn on the standard logger. 240 | func WarningFn(fn logrus.LogFunction) { 241 | DefaultCombinedLogger.WarningFn(fn) 242 | } 243 | 244 | // ErrorFn logs a message from a func at level Error on the standard logger. 245 | func ErrorFn(fn logrus.LogFunction) { 246 | DefaultCombinedLogger.ErrorFn(fn) 247 | } 248 | 249 | // PanicFn logs a message from a func at level Panic on the standard logger. 250 | func PanicFn(fn logrus.LogFunction) { 251 | DefaultCombinedLogger.PanicFn(fn) 252 | } 253 | 254 | // FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1. 255 | func FatalFn(fn logrus.LogFunction) { 256 | DefaultCombinedLogger.FatalFn(fn) 257 | } 258 | 259 | // Tracef logs a message at level Trace on the standard logger. 260 | func Tracef(format string, args ...interface{}) { 261 | DefaultCombinedLogger.Tracef(format, args...) 262 | } 263 | 264 | // Debugf logs a message at level Debug on the standard logger. 265 | func Debugf(format string, args ...interface{}) { 266 | DefaultCombinedLogger.Debugf(format, args...) 267 | } 268 | 269 | // Printf logs a message at level Info on the standard logger. 270 | func Printf(format string, args ...interface{}) { 271 | DefaultCombinedLogger.Printf(format, args...) 272 | } 273 | 274 | // Infof logs a message at level Info on the standard logger. 275 | func Infof(format string, args ...interface{}) { 276 | DefaultCombinedLogger.Infof(format, args...) 277 | } 278 | 279 | // Warnf logs a message at level Warn on the standard logger. 280 | func Warnf(format string, args ...interface{}) { 281 | DefaultCombinedLogger.Warnf(format, args...) 282 | } 283 | 284 | // Warningf logs a message at level Warn on the standard logger. 285 | func Warningf(format string, args ...interface{}) { 286 | DefaultCombinedLogger.Warningf(format, args...) 287 | } 288 | 289 | // Errorf logs a message at level Error on the standard logger. 290 | func Errorf(format string, args ...interface{}) { 291 | DefaultCombinedLogger.Errorf(format, args...) 292 | } 293 | 294 | // Panicf logs a message at level Panic on the standard logger. 295 | func Panicf(format string, args ...interface{}) { 296 | DefaultCombinedLogger.Panicf(format, args...) 297 | } 298 | 299 | // Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1. 300 | func Fatalf(format string, args ...interface{}) { 301 | DefaultCombinedLogger.Fatalf(format, args...) 302 | } 303 | 304 | // Traceln logs a message at level Trace on the standard logger. 305 | func Traceln(args ...interface{}) { 306 | DefaultCombinedLogger.Traceln(args...) 307 | } 308 | 309 | // Debugln logs a message at level Debug on the standard logger. 310 | func Debugln(args ...interface{}) { 311 | DefaultCombinedLogger.Debugln(args...) 312 | } 313 | 314 | // Println logs a message at level Info on the standard logger. 315 | func Println(args ...interface{}) { 316 | DefaultCombinedLogger.Println(args...) 317 | } 318 | 319 | // Infoln logs a message at level Info on the standard logger. 320 | func Infoln(args ...interface{}) { 321 | DefaultCombinedLogger.Infoln(args...) 322 | } 323 | 324 | // Warnln logs a message at level Warn on the standard logger. 325 | func Warnln(args ...interface{}) { 326 | DefaultCombinedLogger.Warnln(args...) 327 | } 328 | 329 | // Warningln logs a message at level Warn on the standard logger. 330 | func Warningln(args ...interface{}) { 331 | DefaultCombinedLogger.Warningln(args...) 332 | } 333 | 334 | // Errorln logs a message at level Error on the standard logger. 335 | func Errorln(args ...interface{}) { 336 | DefaultCombinedLogger.Errorln(args...) 337 | } 338 | 339 | // Panicln logs a message at level Panic on the standard logger. 340 | func Panicln(args ...interface{}) { 341 | DefaultCombinedLogger.Panicln(args...) 342 | } 343 | 344 | // Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1. 345 | func Fatalln(args ...interface{}) { 346 | DefaultCombinedLogger.Fatalln(args...) 347 | } 348 | -------------------------------------------------------------------------------- /pkg/logger/lib_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCombinedLogger_GetLogger(t *testing.T) { 10 | combinedLogger := New() 11 | assert.NotNil(t, combinedLogger.GetLogger(0)) 12 | assert.Nil(t, combinedLogger.GetLogger(1)) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/misc/lib.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | func MustGetFileExt(path string) (string, error) { 10 | ext := filepath.Ext(path) 11 | if ext == "" || ext == "." { 12 | return "", fmt.Errorf("invalid file path " + path) 13 | } 14 | return strings.TrimPrefix(ext, "."), nil 15 | } 16 | 17 | -------------------------------------------------------------------------------- /pkg/misc/lib_test.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import "testing" 4 | func TestMustGetFileExt(t *testing.T) { 5 | tests := []struct { 6 | path string 7 | expected string 8 | err bool 9 | }{ 10 | {"file.txt", "txt", false}, 11 | {"path/to/file.jpg", "jpg", false}, 12 | {"no_extension", "", true}, 13 | {"", "", true}, 14 | } 15 | 16 | for _, test := range tests { 17 | ext, err := MustGetFileExt(test.path) 18 | if test.err && err == nil { 19 | t.Errorf("Expected error for path %s, but got nil", test.path) 20 | } else if !test.err && err != nil { 21 | t.Errorf("Unexpected error for path %s: %v", test.path, err) 22 | } 23 | if ext != test.expected { 24 | t.Errorf("Expected extension %s for path %s, but got %s", test.expected, test.path, ext) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scripts/installer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script will download latest release from 4 | # https://github.com/pluveto/flydav/releases 5 | # and install it to /usr/local/bin/flydav 6 | # with configuration file /etc/flydav/flydav.toml 7 | 8 | DOWNLOADER="curl" 9 | 10 | # ------------------ common ------------------ 11 | 12 | get_downloader() { 13 | if [ -x "$(command -v curl)" ]; then 14 | DOWNLOADER="curl" 15 | elif [ -x "$(command -v wget)" ]; then 16 | DOWNLOADER="wget" 17 | else 18 | echo "No downloader found. Please install curl or wget." 19 | exit 1 20 | fi 21 | } 22 | 23 | must_has() { 24 | if ! command -v "$1" >/dev/null 2>&1; then 25 | echo "This script requires $1 but it's not installed. Aborting." 26 | exit 1 27 | fi 28 | } 29 | 30 | must_run() { 31 | if ! "$@"; then 32 | echo "Failed to run $*" 33 | exit 1 34 | fi 35 | } 36 | 37 | must_be_root() { 38 | if [ "$(id -u)" != "0" ]; then 39 | echo "This script must be run as root" 40 | exit 1 41 | fi 42 | } 43 | 44 | must_be_linux() { 45 | if [ "$(uname)" != "Linux" ]; then 46 | echo "This script only works on Linux" 47 | exit 1 48 | fi 49 | } 50 | 51 | 52 | supported_platforms=( 53 | linux-386 54 | linux-amd64 55 | linux-arm 56 | linux-arm64 57 | mac-amd64 58 | mac-arm64 59 | ) 60 | 61 | # ------------------ adhoc ------------------ 62 | 63 | REPO_API=https://api.github.com/repos/pluveto/flydav 64 | REPO=https://github.com/pluveto/flydav 65 | VERSION= 66 | DEBUG=1 67 | 68 | get_latest_release() { 69 | echo "Getting latest release from $REPO_API" 70 | if [ "$DOWNLOADER" = "curl" ]; then 71 | VERSION=$(curl -s "$REPO_API/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 72 | elif [ "$DOWNLOADER" = "wget" ]; then 73 | VERSION=$(wget -qO- "$REPO_API/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 74 | fi 75 | if [ -z "$VERSION" ]; then 76 | echo "Failed to get latest release" 77 | exit 1 78 | fi 79 | } 80 | 81 | clean_up() { 82 | if [ -d /tmp/flydav_install ]; then 83 | rm -rfd /tmp/flydav_install 84 | fi 85 | } 86 | 87 | 88 | must_has unzip 89 | 90 | 91 | get_downloader 92 | 93 | get_latest_release 94 | 95 | must_be_root 96 | 97 | must_be_linux 98 | 99 | ARCH=$(uname -m) 100 | ARCH=${ARCH/x86_64/amd64} 101 | ARCH=${ARCH/aarch64/arm64} 102 | 103 | DOWNLOAD_URL="$REPO/releases/download/$VERSION/flydav-app-linux-$ARCH.zip" 104 | 105 | echo "Downloading flydav $VERSION from $DOWNLOAD_URL" 106 | 107 | must_run mkdir -p /tmp/flydav_install 108 | 109 | if [ ! -f /tmp/flydav_install/flydav.zip ]; then 110 | if [ "$DOWNLOADER" = "curl" ]; then 111 | must_run curl -L "$DOWNLOAD_URL" -o /tmp/flydav_install/flydav.zip 112 | elif [ "$DOWNLOADER" = "wget" ]; then 113 | must_run wget -O /tmp/flydav_install/flydav.zip "$DOWNLOAD_URL" 114 | fi 115 | echo "Downloaded flydav to /tmp/flydav_install/flydav.zip" 116 | else 117 | echo "Skip downloading flydav" 118 | fi 119 | 120 | # assert flydav.zip size is larger than 1MB 121 | if [ "$(stat -c%s /tmp/flydav_install/flydav.zip)" -lt 1000000 ]; then 122 | echo "Downloaded flydav is too small, please check your network" 123 | clean_up 124 | exit 1 125 | fi 126 | 127 | must_run unzip /tmp/flydav_install/flydav.zip -d /tmp/flydav_install 128 | 129 | must_run chmod +x /tmp/flydav_install/dist/linux_${ARCH}/flydav 130 | 131 | 132 | DOWNLOAD_CONFIG=1 133 | # if already has configuration, ask user to keep it or not 134 | if [ -f /etc/flydav/flydav.toml ]; then 135 | echo "Configuration file /etc/flydav/flydav.toml already exists." 136 | echo "Do you want to keep it? (y/n)" 137 | read -r answer 138 | if [ "$answer" = "n" ]; then 139 | must_run rm -f /etc/flydav/flydav.toml 140 | else 141 | DOWNLOAD_CONFIG=0 142 | fi 143 | fi 144 | 145 | if [ "$DOWNLOAD_CONFIG" = "1" ]; then 146 | 147 | TMP_CONFIG_PATH=/tmp/flydav_install/flydav.toml 148 | CONFIG_DOWNLOAD_URL="https://raw.githubusercontent.com/pluveto/flydav/main/conf/config.default.toml" 149 | 150 | echo "Downloading configuration file from $CONFIG_DOWNLOAD_URL" 151 | 152 | if [ "$DOWNLOADER" = "curl" ]; then 153 | must_run curl -L "$CONFIG_DOWNLOAD_URL" -o "$TMP_CONFIG_PATH" 154 | elif [ "$DOWNLOADER" = "wget" ]; then 155 | must_run wget -O "$TMP_CONFIG_PATH" "$CONFIG_DOWNLOAD_URL" 156 | fi 157 | echo "Downloaded configuration file to $TMP_CONFIG_PATH" 158 | 159 | read -r -d '' USER_CONFIG_TMPL <<'EOF' 160 | [[auth.user]] 161 | username = "::::username::::" 162 | sub_fs_dir = "::::sub_fs_dir::::" 163 | sub_path = "::::sub_path::::" 164 | password_hash = "::::password_hash::::" 165 | password_crypt = "sha256" 166 | EOF 167 | 168 | USERNAME=flydav 169 | 170 | echo "Username (default: flydav):" 171 | read -r answer 172 | if [ -n "$answer" ]; then 173 | USERNAME="$answer" 174 | fi 175 | 176 | echo "Password for $USERNAME:" 177 | read -r PASSWORD 178 | # password must be longer than or eq 10 characters 179 | while [ ${#PASSWORD} -lt 10 ]; do 180 | echo "Password must be longer than or eq 10 characters" 181 | echo "Please enter a password for $USERNAME:" 182 | read -r PASSWORD 183 | done 184 | PASSWORD_HASH=$(echo -n "$USERNAME" | sha256sum | cut -d ' ' -f 1) 185 | 186 | FS_DIR=/tmp/flydav 187 | SUB_FS_DIR= 188 | echo "Filesystem root dir: (default: /tmp/flydav)" 189 | read -r answer 190 | if [ -n "$answer" ]; then 191 | FS_DIR="$answer" 192 | fi 193 | echo "Sub filesystem dir: (default: $SUB_FS_DIR)" 194 | read -r answer 195 | if [ -n "$answer" ]; then 196 | SUB_FS_DIR="$answer" 197 | fi 198 | TMP_USER_FS_ROOT="$FS_DIR/$SUB_FS_DIR" 199 | echo "User dir will be $TMP_USER_FS_ROOT Do you want to continue? (y/n)" 200 | read -r answer 201 | 202 | if [ "$answer" != "y" ] && [ "$answer" != "yes" ] && [ -n "$answer" ]; then 203 | echo "Installation cancelled" 204 | clean_up 205 | exit 1 206 | fi 207 | 208 | HTTP_HOST=0.0.0.0 209 | HTTP_PORT=7086 210 | echo "HTTP host(default: $HTTP_HOST): " 211 | read -r answer 212 | if [ -n "$answer" ]; then 213 | HTTP_HOST="$answer" 214 | fi 215 | echo "HTTP port(default: $HTTP_PORT): " 216 | read -r answer 217 | if [ -n "$answer" ]; then 218 | HTTP_PORT="$answer" 219 | fi 220 | 221 | HTTP_PATH_PREFIX=/webdav 222 | echo "HTTP path prefix(default: $HTTP_PATH_PREFIX): " 223 | read -r answer 224 | if [ -n "$answer" ]; then 225 | HTTP_PATH_PREFIX="$answer" 226 | fi 227 | 228 | echo "The service will be running at http://$HTTP_HOST:$HTTP_PORT$HTTP_PATH_PREFIX" 229 | 230 | echo "Do you want to continue? (y/n)" 231 | read -r answer 232 | if [ "$answer" != "y" ] && [ "$answer" != "yes" ] && [ -n "$answer" ]; then 233 | echo "Installation cancelled" 234 | clean_up 235 | exit 1 236 | fi 237 | 238 | echo "Creating configuration file" 239 | must_run mkdir -p /etc/flydav 240 | 241 | must_run sed -i "/# add more users here/r /dev/stdin" "$TMP_CONFIG_PATH" <<< "$USER_CONFIG_TMPL" 242 | 243 | function sedeasy { 244 | sed -i "s/$(printf '%s\n' "$1" | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/$(echo $2 | sed -e 's/[\/&]/\\&/g')/g" $3 245 | } 246 | must_run sedeasy "::::username::::" "$USERNAME" "$TMP_CONFIG_PATH" 247 | must_run sedeasy "::::sub_fs_dir::::" "$SUB_FS_DIR" "$TMP_CONFIG_PATH" 248 | must_run sedeasy "::::sub_path::::" "$SUB_PATH" "$TMP_CONFIG_PATH" 249 | must_run sedeasy "::::password_hash::::" "$PASSWORD_HASH" "$TMP_CONFIG_PATH" 250 | 251 | must_run sedeasy "host = \"0.0.0.0\"" "host = \"$HTTP_HOST\"" "$TMP_CONFIG_PATH" 252 | must_run sedeasy "port = 7086" "port = $HTTP_PORT" "$TMP_CONFIG_PATH" 253 | must_run sedeasy "path = \"\/webdav\"" "path = \"$HTTP_PATH_PREFIX\"" "$TMP_CONFIG_PATH" 254 | must_run sedeasy "fs_dir = \"\/tmp\/flydav\"" "fs_dir = \"$FS_DIR\"" "$TMP_CONFIG_PATH" 255 | 256 | echo "Configuration file created at $TMP_CONFIG_PATH" 257 | 258 | must_run mv "$TMP_CONFIG_PATH" /etc/flydav/flydav.toml 259 | fi 260 | 261 | echo "Installing binary" 262 | must_run mv "/tmp/flydav_install/dist/linux_${ARCH}/flydav" "/usr/local/bin/flydav" 263 | 264 | # check if service is already installed 265 | if [ -f /etc/systemd/system/flydav.service ]; then 266 | echo "Service already installed" 267 | echo "Do you want to reinstall? (y/n)" 268 | read -r answer 269 | if [ "$answer" != "y" ] && [ "$answer" != "yes" ] && [ -n "$answer" ]; then 270 | echo "Installation cancelled" 271 | clean_up 272 | exit 1 273 | fi 274 | 275 | echo "Removing old service" 276 | must_run systemctl stop flydav 277 | must_run systemctl disable flydav 278 | must_run rm -r /etc/systemd/system/flydav.service 279 | must_run systemctl daemon-reload 280 | 281 | fi 282 | 283 | echo "Creating user flydav" 284 | # create if not exists 285 | if ! id -u flydav >/dev/null 2>&1; then 286 | must_run useradd -r -s /bin/false flydav 287 | fi 288 | 289 | # if TMP_USER_FS_ROOT is not existing 290 | if [ ! -d "$TMP_USER_FS_ROOT" ]; then 291 | 292 | echo "Creating directory $TMP_USER_FS_ROOT" 293 | must_run mkdir -p "$TMP_USER_FS_ROOT" 294 | 295 | echo "Setting permissions for $TMP_USER_FS_ROOT" 296 | must_run chown -R flydav:flydav "$TMP_USER_FS_ROOT" 297 | 298 | fi 299 | 300 | echo "Creating systemd service" 301 | 302 | read -r -d '' SERVICE_TMPL <<'EOF' 303 | [Unit] 304 | Description=Flydav WebDAV server 305 | After=network.target 306 | 307 | [Service] 308 | User=flydav 309 | Group=flydav 310 | ExecStart=/usr/local/bin/flydav -c /etc/flydav/flydav.toml 311 | Restart=on-failure 312 | RestartSec=5 313 | StartLimitInterval=60s 314 | StartLimitBurst=3 315 | 316 | [Install] 317 | WantedBy=multi-user.target 318 | EOF 319 | 320 | must_run echo "$SERVICE_TMPL" > /etc/systemd/system/flydav.service 321 | 322 | echo "Enabling systemd service" 323 | must_run systemctl daemon-reload 324 | must_run systemctl enable flydav.service 325 | 326 | echo "Starting systemd service" 327 | must_run systemctl start flydav.service 328 | 329 | echo "Installation complete" 330 | -------------------------------------------------------------------------------- /scripts/ui_installer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script will download latest release from 4 | # https://github.com/pluveto/flydav/releases 5 | # and install it to /usr/local/bin/flydav 6 | # with configuration file /etc/flydav/flydav.toml 7 | 8 | DOWNLOADER="curl" 9 | 10 | # ------------------ common ------------------ 11 | 12 | get_downloader() { 13 | if [ -x "$(command -v curl)" ]; then 14 | DOWNLOADER="curl" 15 | elif [ -x "$(command -v wget)" ]; then 16 | DOWNLOADER="wget" 17 | else 18 | echo "No downloader found. Please install curl or wget." 19 | exit 1 20 | fi 21 | } 22 | 23 | must_has() { 24 | if ! command -v "$1" >/dev/null 2>&1; then 25 | echo "This script requires $1 but it's not installed. Aborting." 26 | exit 1 27 | fi 28 | } 29 | 30 | must_run() { 31 | if ! "$@"; then 32 | echo "Failed to run $*" 33 | exit 1 34 | fi 35 | } 36 | 37 | must_be_root() { 38 | if [ "$(id -u)" != "0" ]; then 39 | echo "This script must be run as root" 40 | exit 1 41 | fi 42 | } 43 | 44 | must_be_linux() { 45 | if [ "$(uname)" != "Linux" ]; then 46 | echo "This script only works on Linux" 47 | exit 1 48 | fi 49 | } 50 | 51 | 52 | supported_platforms=( 53 | linux-386 54 | linux-amd64 55 | linux-arm 56 | linux-arm64 57 | mac-amd64 58 | mac-arm64 59 | ) 60 | 61 | # ------------------ adhoc ------------------ 62 | 63 | REPO_API=https://api.github.com/repos/pluveto/flydav 64 | REPO=https://github.com/pluveto/flydav 65 | VERSION= 66 | DEBUG=1 67 | 68 | get_latest_release() { 69 | echo "Getting latest release from $REPO_API" 70 | if [ "$DOWNLOADER" = "curl" ]; then 71 | VERSION=$(curl -s "$REPO_API/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 72 | elif [ "$DOWNLOADER" = "wget" ]; then 73 | VERSION=$(wget -qO- "$REPO_API/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 74 | fi 75 | if [ -z "$VERSION" ]; then 76 | echo "Failed to get latest release" 77 | exit 1 78 | fi 79 | } 80 | 81 | clean_up() { 82 | if [ -d /tmp/flydav_install ]; then 83 | rm -rfd /tmp/flydav_install 84 | fi 85 | } 86 | 87 | 88 | must_has unzip 89 | 90 | 91 | get_downloader 92 | 93 | get_latest_release 94 | 95 | must_be_root 96 | 97 | must_be_linux 98 | 99 | ARCH=$(uname -m) 100 | ARCH=${ARCH/x86_64/amd64} 101 | ARCH=${ARCH/aarch64/arm64} 102 | 103 | DOWNLOAD_URL="$REPO/releases/download/$VERSION/flydav-ui-dist.zip" 104 | 105 | echo "Downloading flydav $VERSION from $DOWNLOAD_URL" 106 | 107 | must_run mkdir -p /tmp/flydav_install 108 | 109 | if [ ! -f /tmp/flydav_install/flydav-ui-dist.zip ]; then 110 | if [ "$DOWNLOADER" = "curl" ]; then 111 | must_run curl -L "$DOWNLOAD_URL" -o /tmp/flydav_install/flydav-ui-dist.zip 112 | elif [ "$DOWNLOADER" = "wget" ]; then 113 | must_run wget -O /tmp/flydav_install/flydav-ui-dist.zip "$DOWNLOAD_URL" 114 | fi 115 | echo "Downloaded flydav to /tmp/flydav_install/flydav-ui-dist.zip" 116 | else 117 | echo "Skip downloading flydav" 118 | fi 119 | 120 | # assert flydav-ui-dist.zip size is larger than 10KB 121 | if [ "$(stat -c%s /tmp/flydav_install/flydav-ui-dist.zip)" -lt 10000 ]; then 122 | echo "Downloaded flydav-ui is too small, please check your network" 123 | clean_up 124 | exit 1 125 | fi 126 | 127 | echo "Install dir for flydav-ui (default: /usr/local/share/flydav/ui):" 128 | read -r answer 129 | if [ -n "$answer" ]; then 130 | UI_INSTALL_DIR="$answer" 131 | else 132 | UI_INSTALL_DIR=/usr/local/share/flydav/ui 133 | fi 134 | 135 | mkdir -p "$UI_INSTALL_DIR" 136 | 137 | must_run unzip /tmp/flydav_install/flydav-ui-dist.zip -d $UI_INSTALL_DIR 138 | 139 | echo "Flydav UI is installed to $UI_INSTALL_DIR" 140 | echo "Edit your flydav config file and restart to enable UI" 141 | 142 | clean_up 143 | -------------------------------------------------------------------------------- /ui/.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/jvidalv/Projects/vital/src/app/app.tsx":"1","/Users/jvidalv/Projects/vital/src/components/atoms/button/button.test.tsx":"2","/Users/jvidalv/Projects/vital/src/components/atoms/button/button.tsx":"3","/Users/jvidalv/Projects/vital/src/components/atoms/button/index.ts":"4","/Users/jvidalv/Projects/vital/src/components/atoms/logos/index.ts":"5","/Users/jvidalv/Projects/vital/src/components/atoms/logos/vite.tsx":"6","/Users/jvidalv/Projects/vital/src/components/molecules/copy-button/copy-button.tsx":"7","/Users/jvidalv/Projects/vital/src/components/molecules/copy-button/index.ts":"8","/Users/jvidalv/Projects/vital/src/components/organisms/card/card.tsx":"9","/Users/jvidalv/Projects/vital/src/components/organisms/card/index.ts":"10","/Users/jvidalv/Projects/vital/src/infrastructure/tests/setup-tests.ts":"11","/Users/jvidalv/Projects/vital/src/main.tsx":"12","/Users/jvidalv/Projects/vital/src/vite-env.d.ts":"13","/Users/jvidalv/Projects/vital/vite.config.ts":"14"},{"size":4994,"mtime":1673625660635,"results":"15","hashOfConfig":"16"},{"size":615,"mtime":1639219113176,"results":"17","hashOfConfig":"16"},{"size":360,"mtime":1639219113177,"results":"18","hashOfConfig":"16"},{"size":55,"mtime":1639219113177,"results":"19","hashOfConfig":"16"},{"size":188,"mtime":1639219113177,"results":"20","hashOfConfig":"16"},{"size":1847,"mtime":1639219113177,"results":"21","hashOfConfig":"16"},{"size":1298,"mtime":1673621698012,"results":"22","hashOfConfig":"16"},{"size":68,"mtime":1639219113177,"results":"23","hashOfConfig":"16"},{"size":1158,"mtime":1673626447768,"results":"24","hashOfConfig":"16"},{"size":49,"mtime":1673626447771,"results":"25","hashOfConfig":"16"},{"size":36,"mtime":1639219113178,"results":"26","hashOfConfig":"16"},{"size":221,"mtime":1639219113178,"results":"27","hashOfConfig":"16"},{"size":38,"mtime":1639219113178,"results":"28","hashOfConfig":"16"},{"size":629,"mtime":1639219113179,"results":"29","hashOfConfig":"16"},{"filePath":"30","messages":"31","suppressedMessages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1h7ceoa",{"filePath":"33","messages":"34","suppressedMessages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","suppressedMessages":"38","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"39","messages":"40","suppressedMessages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"42","messages":"43","suppressedMessages":"44","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"45","messages":"46","suppressedMessages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","suppressedMessages":"50","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"51","messages":"52","suppressedMessages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"54","messages":"55","suppressedMessages":"56","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"57","messages":"58","suppressedMessages":"59","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"60","messages":"61","suppressedMessages":"62","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"63","messages":"64","suppressedMessages":"65","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"66","messages":"67","suppressedMessages":"68","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"69","messages":"70","suppressedMessages":"71","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/jvidalv/Projects/vital/src/app/app.tsx",[],[],"/Users/jvidalv/Projects/vital/src/components/atoms/button/button.test.tsx",[],[],"/Users/jvidalv/Projects/vital/src/components/atoms/button/button.tsx",[],[],"/Users/jvidalv/Projects/vital/src/components/atoms/button/index.ts",[],[],"/Users/jvidalv/Projects/vital/src/components/atoms/logos/index.ts",[],[],"/Users/jvidalv/Projects/vital/src/components/atoms/logos/vite.tsx",[],[],"/Users/jvidalv/Projects/vital/src/components/molecules/copy-button/copy-button.tsx",[],[],"/Users/jvidalv/Projects/vital/src/components/molecules/copy-button/index.ts",[],[],"/Users/jvidalv/Projects/vital/src/components/organisms/card/card.tsx",[],[],"/Users/jvidalv/Projects/vital/src/components/organisms/card/index.ts",[],[],"/Users/jvidalv/Projects/vital/src/infrastructure/tests/setup-tests.ts",[],[],"/Users/jvidalv/Projects/vital/src/main.tsx",[],[],"/Users/jvidalv/Projects/vital/src/vite-env.d.ts",[],[],"/Users/jvidalv/Projects/vital/vite.config.ts",[],[]] -------------------------------------------------------------------------------- /ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "plugins": ["@typescript-eslint"], 8 | "extends": [ 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/ban-ts-comment": "warn", 15 | "comma-dangle": "off", 16 | "multiline-ternary": "off", 17 | "no-use-before-define": "off", 18 | "space-before-function-paren": "off", 19 | "react/prop-types": "off", 20 | "react/no-unescaped-entities": "off", 21 | "react/display-name": "off", 22 | "react/react-in-jsx-scope": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | dist 4 | -------------------------------------------------------------------------------- /ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Josep Vidal Vidal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # FlyDav UI 2 | 3 | A frontend user interface providing basic browsing and download. -------------------------------------------------------------------------------- /ui/commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | FlyDav UI 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: "./src", 3 | preset: "ts-jest", 4 | testEnvironment: "jsdom", 5 | coverageDirectory: "coverage", 6 | coverageProvider: "v8", 7 | setupFilesAfterEnv: ["/infrastructure/tests/setup-tests.ts"], 8 | extensionsToTreatAsEsm: [".ts", ".tsx"], 9 | moduleDirectories: ["node_modules", "./src"], 10 | moduleNameMapper: { 11 | "\\.(css)$": "identity-obj-proxy", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /ui/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.{ts,tsx,css}": ["prettier . --write"], 3 | "*.{ts,tsx}": [ 4 | "eslint . --cache --fix --ext .tsx --ext .ts", 5 | () => "yarn tsc", 6 | () => "yarn jest", 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flydav-ui", 3 | "version": "1.0.0", 4 | "description": "", 5 | "homepage": "https://github.com/pluveto/flydav", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/pluveto/flydav.git", 9 | "directory": "ui" 10 | }, 11 | "author": "Pluveto ", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/pluveto/flydav/issues" 15 | }, 16 | "keywords": [ 17 | "webdav", 18 | "client", 19 | "ui" 20 | ], 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not IE 11" 25 | ], 26 | "engines": { 27 | "node": ">=14", 28 | "yarn": ">=1.22.5" 29 | }, 30 | "scripts": { 31 | "start": "vite", 32 | "build": "vite build", 33 | "test": "jest" 34 | }, 35 | "dependencies": { 36 | "@heroicons/react": "2.0.13", 37 | "react": "18.2.0", 38 | "react-dom": "18.2.0", 39 | "webdav": "^4.11.2" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "17.4.2", 43 | "@commitlint/config-conventional": "17.4.2", 44 | "@tailwindcss/forms": "0.5.3", 45 | "@testing-library/jest-dom": "5.16.5", 46 | "@testing-library/react": "13.4.0", 47 | "@types/jest": "29.2.5", 48 | "@types/node": "18.11.18", 49 | "@types/react": "18.0.26", 50 | "@typescript-eslint/eslint-plugin": "5.48.1", 51 | "@typescript-eslint/parser": "5.48.1", 52 | "autoprefixer": "10.4.13", 53 | "eslint": "8.31.0", 54 | "eslint-plugin-import": "2.27.4", 55 | "eslint-plugin-react": "7.32.0", 56 | "eslint-plugin-react-hooks": "4.6.0", 57 | "identity-obj-proxy": "3.0.0", 58 | "jest": "29.3.1", 59 | "jest-environment-jsdom": "29.3.1", 60 | "lint-staged": "13.1.0", 61 | "postcss": "8.4.21", 62 | "prettier": "2.8.2", 63 | "tailwindcss": "3.2.4", 64 | "ts-jest": "29.0.5", 65 | "typescript": "4.9.4", 66 | "vite": "4.0.4", 67 | "vite-preset-react": "2.3.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | modules: true, 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /ui/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Vital", 3 | "name": "Starter template for Vite with React (TypeScript). Supports Tailwind with CSS-Modules. Jest and @react/testing-library configured and ready to go. Also ESLint, Prettier, Husky, Commit-lint and Atomic Design for components.", 4 | "icons": [ 5 | { 6 | "src": "favicon.svg", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#111827", 14 | "background_color": "#111827" 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/app.module.css: -------------------------------------------------------------------------------- 1 | .fileList { 2 | @apply flex flex-col text-gray-600 font-mono; 3 | } 4 | 5 | .fileListHeader { 6 | @apply flex flex-row text-lg font-semibold tracking-widest text-gray-800 7 | } 8 | 9 | .fileListCell { 10 | @apply flex px-2 py-2; 11 | flex: 1; 12 | } 13 | 14 | .fileListIcon { 15 | @apply mr-1; 16 | } 17 | 18 | .fileListCellParent { 19 | @apply px-2 py-2; 20 | } 21 | 22 | .fileListSizeCell { 23 | @apply w-1/4 px-2 py-2; 24 | max-width: 100px; 25 | } 26 | 27 | .fileListLastmodCell{ 28 | @apply w-1/3 px-2 py-2; 29 | max-width: 500px; 30 | } 31 | 32 | .fileListItem { 33 | @apply flex flex-row text-sm tracking-widest 34 | } 35 | 36 | .searchFileInput { 37 | @apply w-full px-2 py-2 mr-2 text-sm text-gray-600 border border-gray-300 rounded-md 38 | } 39 | 40 | .pathInput { 41 | @apply flex-1 px-2 py-2 text-sm text-gray-600 border border-gray-300 rounded-md 42 | } 43 | 44 | .buttonDefault { 45 | @apply mr-2 px-2 py-1.5 bg-gray-200 rounded 46 | } 47 | 48 | .buttonPrimary { 49 | @apply ml-2 px-4 py-1.5 bg-blue-500 text-white rounded 50 | } 51 | 52 | .entryLink { 53 | @apply text-blue-500 hover:text-blue-600 cursor-pointer 54 | } 55 | 56 | .footer{ 57 | @apply p-4 flex flex-row justify-between items-center text-sm text-gray-600 58 | } 59 | 60 | .copyRight{ 61 | @apply text-gray-600 text-sm 62 | } 63 | 64 | .projectLink{ 65 | @apply text-blue-500 hover:text-blue-600 underline cursor-pointer 66 | } -------------------------------------------------------------------------------- /ui/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | 2 | import styles from "./app.module.css"; 3 | import sharedStyles from "../shared.module.css" 4 | 5 | import { AuthType, createClient, FileStat, ResponseDataDetailed, WebDAVClient } from "webdav"; 6 | import React from "react"; 7 | import ProgressBar from "components/progress_bar"; 8 | import Settings, { SettingsObject } from "./settings"; 9 | 10 | 11 | 12 | const dirname = (path: String) => { 13 | let ret = path.replace(/\\/g, '/').replace(/\/[^/]*$/, ''); 14 | if (ret.length == 0) { 15 | return "/" 16 | } 17 | return ret; 18 | } 19 | 20 | const dateFormat = (input: string) => { 21 | const date = new Date(input) 22 | return date.toJSON(); 23 | 24 | } 25 | 26 | interface FileStatExtended extends FileStat { 27 | isParent: boolean 28 | } 29 | 30 | const App = (): JSX.Element => { 31 | 32 | const [path, setPath] = React.useState("/"); 33 | const [pathUncommitted, setPathUncommitted] = React.useState("/"); 34 | const [files, setFiles] = React.useState([]); 35 | const [loading, setLoading] = React.useState("Loading"); 36 | const [downloadProgress, setDownloadProgress] = React.useState(0); 37 | const [showSettingsModal, setShowSettingsModal] = React.useState(false); 38 | const [refreshFlag, setRefreshFlag] = React.useState(false); 39 | 40 | const [settingsData, setSettingsData] = React.useState({ 41 | url: "", 42 | username: "", 43 | password: "" 44 | }) 45 | 46 | const [client, setClient] = React.useState(null) 47 | 48 | // try load settings from localStorage 49 | React.useEffect(() => { 50 | const settingsStr = localStorage.getItem("settings") 51 | if (settingsStr) { 52 | let settings = JSON.parse(settingsStr) 53 | setSettingsData(settings) 54 | console.log("loaded settings from localStorage: ", settingsData) 55 | } else { 56 | console.log("no settings in localStorage") 57 | setShowSettingsModal(true) 58 | } 59 | }, []) 60 | 61 | React.useEffect(() => { 62 | if (settingsData.url == "") return; 63 | setClient(createClient(settingsData.url, { 64 | username: settingsData.username, 65 | password: settingsData.password, 66 | authType: AuthType.Password 67 | })) 68 | }, [settingsData]) 69 | 70 | React.useEffect(() => { 71 | setRefreshFlag(false); 72 | setLoading("Loading"); 73 | setFiles([]) 74 | pathUncommitted != path && setPathUncommitted(path); 75 | console.log("client: ", client); 76 | 77 | client?.getDirectoryContents(path).then((files: FileStat[] | ResponseDataDetailed) => { 78 | // if is ResponseDataDetailed, unwrap 79 | if (!Array.isArray(files)) { 80 | alert("Error: unexpected response type") 81 | } 82 | console.log("files: ", files); 83 | 84 | let filesUnwrapped = files as FileStatExtended[]; 85 | 86 | filesUnwrapped.map(f => { 87 | f.isParent = false 88 | }) 89 | 90 | setFiles(filesUnwrapped); 91 | }).catch(r => { 92 | console.log(r); 93 | if (r.status == 404) { 94 | setPath("/") 95 | setLoading("Not found. Check settings."); 96 | } 97 | }).finally(() => { 98 | setLoading(""); 99 | }); 100 | }, [path, client, refreshFlag]); 101 | 102 | async function handleClickFile(file: FileStat) { 103 | const buff: Buffer = await client?.getFileContents(file.filename, { 104 | format: "binary", 105 | onDownloadProgress: e => { 106 | setDownloadProgress(e.loaded / e.total * 100) 107 | }, 108 | }) as Buffer 109 | saveBuffer(buff, file.basename) 110 | } 111 | 112 | const saveBuffer = (buf: Buffer, filename: string) => { 113 | const a = document.createElement('a'); 114 | a.style.display = 'none'; 115 | document.body.appendChild(a); 116 | const blob = new Blob([buf], { type: 'octet/stream' }); 117 | const url = window.URL.createObjectURL(blob); 118 | a.href = url; 119 | a.download = filename; 120 | a.click(); 121 | window.URL.revokeObjectURL(url); 122 | document.body.removeChild(a); 123 | }; 124 | return ( 125 |
126 | { 127 | showSettingsModal && 128 | { 132 | setSettingsData(settings); 133 | // save to localStorage 134 | localStorage.setItem("settings", JSON.stringify(settings)); 135 | setShowSettingsModal(false); 136 | } 137 | } 138 | onDiscard={() => setShowSettingsModal(false)} 139 | > 140 | 141 | } 142 |
143 |

144 | FlyDav UI 145 |

146 |
147 | 148 | 149 | 152 |
153 |
154 | 155 | {downloadProgress > 0 && downloadProgress < 100 && } 156 |
157 |
158 | 159 | setPathUncommitted(e.target.value)}> 160 | 163 | 164 | 165 | 166 |
167 |
168 |
169 |
170 |
171 |
Name
172 |
Size
173 |
Last Modified
174 |
175 | {loading &&
176 | Loading... 177 |
} 178 | { 179 | files.filter(file => file.type == "directory").map((file: FileStatExtended, idx, arr) => { 180 | return ( 181 |
182 | 189 |
{file.size}
190 |
{dateFormat(file.lastmod)}
191 |
192 | ) 193 | }) 194 | } 195 | { 196 | files.filter(file => file.type == "file").map((file: FileStat, idx, arr) => { 197 | return ( 198 |
199 | 204 |
{file.size}
205 |
{dateFormat(file.lastmod)}
206 |
207 | ) 208 | }) 209 | } 210 |
211 |
212 |
213 |
214 | Copyright {new Date().getFullYear()} - FlyDav Project 215 |
216 |
217 |
218 | ); 219 | }; 220 | 221 | export default App; 222 | -------------------------------------------------------------------------------- /ui/src/app/settings.module.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden 3 | outline-none focus:outline-none; 4 | } 5 | .mask { 6 | @apply fixed inset-0 z-40 bg-black opacity-25; 7 | } 8 | .modalInner { 9 | @apply relative my-6 mx-auto w-auto max-w-3xl; 10 | } 11 | .modalContent { 12 | @apply p-4 relative flex w-full flex-col rounded-lg border-0 bg-white shadow-lg 13 | outline-none focus:outline-none; 14 | } 15 | .formEntry { 16 | @apply md:flex md:items-center mb-6 w-80 ; 17 | } 18 | .formEntryLabelOuter { 19 | @apply md:w-1/3; 20 | } 21 | .formEntryLabel { 22 | @apply block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4; 23 | } 24 | .formEntryValueOuter { 25 | @apply md:w-2/3; 26 | } 27 | .formEntryInput { 28 | @apply bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 29 | text-gray-700 leading-tight focus:outline-none focus:bg-white; 30 | } 31 | .modalTitle { 32 | @apply text-xl mb-2; 33 | } 34 | 35 | .modalEnd { 36 | @apply flex justify-end border-t-2 pt-4 border-gray-100; 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/app/settings.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./settings.module.css"; 2 | import sharedStyles from "../shared.module.css" 3 | import { useEffect, useState } from "react"; 4 | 5 | interface SettingsProps { 6 | initialValue: SettingsObject 7 | onSave: (o: SettingsObject) => void 8 | onDiscard: () => void 9 | } 10 | 11 | export interface SettingsObject{ 12 | url: string, 13 | username: string, 14 | password: string 15 | } 16 | 17 | const Settings = (props: SettingsProps) => { 18 | const [url, setUrl] = useState(""); 19 | const [username, setUsername] = useState(""); 20 | const [password, setPassword] = useState(""); 21 | 22 | useEffect(()=>{ 23 | setUrl(props.initialValue.url) 24 | setUsername(props.initialValue.username) 25 | setPassword(props.initialValue.password) 26 | }, [props.initialValue]) 27 | 28 | return (
29 |
30 |
31 |
32 | 33 |

Settings

34 |
35 |
36 | 39 |
40 |
41 | setUrl((e.target as HTMLInputElement).value)} 43 | type="text" value={url}> 44 |
45 |
46 |
47 |
48 | 51 |
52 |
53 | setUsername((e.target as HTMLInputElement).value)} 55 | type="text" value={username}> 56 |
57 |
58 |
59 |
60 | 63 |
64 |
65 | setPassword((e.target as HTMLInputElement).value)} 67 | type="password" value={password}> 68 |
69 |
70 |
71 | 72 | 75 |
76 | 77 |
78 |
79 |
80 |
81 |
) 82 | } 83 | 84 | export default Settings -------------------------------------------------------------------------------- /ui/src/app/tmp.md: -------------------------------------------------------------------------------- 1 | # tailwind 2 | -------------------------------------------------------------------------------- /ui/src/components/progress_bar.module.css: -------------------------------------------------------------------------------- 1 | .barOuter { 2 | @apply w-full bg-gray-200 h-2.5 3 | } 4 | .barInner { 5 | @apply bg-blue-600 h-2.5 6 | } -------------------------------------------------------------------------------- /ui/src/components/progress_bar.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./progress_bar.module.css"; 2 | 3 | 4 | 5 | interface ProgressBarProps { 6 | current: number, 7 | total: number 8 | } 9 | const ProgressBar = (props: ProgressBarProps) => { 10 | return ( 11 |
12 |
15 |
16 | ); 17 | 18 | }; 19 | export default ProgressBar -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html { 7 | -moz-osx-font-smoothing: grayscale; 8 | -webkit-font-smoothing: antialiased; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/infrastructure/tests/setup-tests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "app/app"; 5 | const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); 6 | root.render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /ui/src/shared.module.css: -------------------------------------------------------------------------------- 1 | 2 | .buttonPrimary { 3 | @apply ml-2 px-4 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600; 4 | } 5 | .buttonDefault { 6 | @apply mr-2 px-2 py-1.5 bg-gray-200 rounded hover:bg-gray-300; 7 | } -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./*.html", "./src/**/*.css"], 3 | plugins: [require("@tailwindcss/forms")], 4 | }; 5 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "skipLibCheck": false, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "paths": { 20 | "app/*": ["src/app/*"], 21 | "components/*": ["src/components/*"], 22 | "hooks/*": ["src/hooks/*"] 23 | } 24 | }, 25 | "include": ["./src"] 26 | } 27 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import react from "@vitejs/plugin-react"; 4 | 5 | export default defineConfig((configEnv) => { 6 | const isDevelopment = configEnv.mode === "development"; 7 | 8 | return { 9 | plugins: [react()], 10 | resolve: { 11 | alias: { 12 | app: resolve(__dirname, "src", "app"), 13 | components: resolve(__dirname, "src", "components"), 14 | hooks: resolve(__dirname, "src", "hooks"), 15 | }, 16 | }, 17 | css: { 18 | modules: { 19 | generateScopedName: isDevelopment 20 | ? "[name]__[local]__[hash:base64:5]" 21 | : "[hash:base64:5]", 22 | }, 23 | }, 24 | }; 25 | }); 26 | --------------------------------------------------------------------------------