diff --git a/cmd/config.go b/cmd/config.go index 3ee8dab8..43e07730 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -49,6 +49,10 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("branding.files", "", "path to directory with images and custom styles") flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links") flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") + // NB: these are string so they can be presented as octal in the help text + // as that's the conventional representation for modes in Unix. + flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with") + flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with") } //nolint:gocyclo @@ -170,6 +174,8 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale) fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode) fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick) + fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode) + fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode) fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " ")) fmt.Fprintf(w, "\tSorting:\n") fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By) diff --git a/cmd/config_init.go b/cmd/config_init.go index d9710514..f168a448 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -43,6 +43,8 @@ override the options.`, Theme: mustGetString(flags, "branding.theme"), Files: mustGetString(flags, "branding.files"), }, + FileMode: mustGetMode(flags, "file-mode"), + DirMode: mustGetMode(flags, "dir-mode"), } ser := &settings.Server{ diff --git a/cmd/config_set.go b/cmd/config_set.go index 05816795..6ba99e73 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -65,6 +65,10 @@ you want to change. Other options will remain unchanged.`, set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name) case "branding.files": set.Branding.Files = mustGetString(flags, flag.Name) + case "file-mode": + set.FileMode = mustGetMode(flags, flag.Name) + case "dir-mode": + set.DirMode = mustGetMode(flags, flag.Name) } }) diff --git a/cmd/utils.go b/cmd/utils.go index 909a1558..e88233fb 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "log" "os" "path/filepath" + "strconv" "strings" "github.com/asdine/storm/v3" @@ -14,12 +16,13 @@ import ( "github.com/spf13/pflag" yaml "gopkg.in/yaml.v2" - "github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage/bolt" ) +const dbPerms = 0640 + func checkErr(err error) { if err != nil { log.Fatal(err) @@ -32,6 +35,13 @@ func mustGetString(flags *pflag.FlagSet, flag string) string { return s } +func mustGetMode(flags *pflag.FlagSet, flag string) fs.FileMode { + s := mustGetString(flags, flag) + b, err := strconv.ParseUint(s, 0, 32) + checkErr(err) + return fs.FileMode(b) +} + func mustGetBool(flags *pflag.FlagSet, flag string) bool { b, err := flags.GetBool(flag) checkErr(err) @@ -106,7 +116,7 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc { log.Println("Using database: " + absPath) data.hadDB = exists - db, err := storm.Open(path, storm.BoltOptions(files.PermFile, nil)) + db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil)) checkErr(err) defer db.Close() data.store, err = bolt.NewStorage(db) diff --git a/files/file.go b/files/file.go index 1ba1cd58..8e27e549 100644 --- a/files/file.go +++ b/files/file.go @@ -27,9 +27,6 @@ import ( "github.com/filebrowser/filebrowser/v2/rules" ) -const PermFile = 0640 -const PermDir = 0750 - var ( reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$") reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$") diff --git a/fileutils/copy.go b/fileutils/copy.go index 57c961da..6c80c5c9 100644 --- a/fileutils/copy.go +++ b/fileutils/copy.go @@ -1,6 +1,7 @@ package fileutils import ( + "io/fs" "os" "path" @@ -8,7 +9,7 @@ import ( ) // Copy copies a file or folder from one place to another. -func Copy(fs afero.Fs, src, dst string) error { +func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error { if src = path.Clean("/" + src); src == "" { return os.ErrNotExist } @@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error { return os.ErrInvalid } - info, err := fs.Stat(src) + info, err := afs.Stat(src) if err != nil { return err } if info.IsDir() { - return CopyDir(fs, src, dst) + return CopyDir(afs, src, dst, fileMode, dirMode) } - return CopyFile(fs, src, dst) + return CopyFile(afs, src, dst, fileMode, dirMode) } diff --git a/fileutils/dir.go b/fileutils/dir.go index 07a3528e..e0b049db 100644 --- a/fileutils/dir.go +++ b/fileutils/dir.go @@ -2,6 +2,7 @@ package fileutils import ( "errors" + "io/fs" "github.com/spf13/afero" ) @@ -9,20 +10,20 @@ import ( // CopyDir copies a directory from source to dest and all // of its sub-directories. It doesn't stop if it finds an error // during the copy. Returns an error if any. -func CopyDir(fs afero.Fs, source, dest string) error { +func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error { // Get properties of source. - srcinfo, err := fs.Stat(source) + srcinfo, err := afs.Stat(source) if err != nil { return err } // Create the destination directory. - err = fs.MkdirAll(dest, srcinfo.Mode()) + err = afs.MkdirAll(dest, srcinfo.Mode()) if err != nil { return err } - dir, _ := fs.Open(source) + dir, _ := afs.Open(source) obs, err := dir.Readdir(-1) if err != nil { return err @@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error { if obj.IsDir() { // Create sub-directories, recursively. - err = CopyDir(fs, fsource, fdest) + err = CopyDir(afs, fsource, fdest, fileMode, dirMode) if err != nil { errs = append(errs, err) } } else { // Perform the file copy. - err = CopyFile(fs, fsource, fdest) + err = CopyFile(afs, fsource, fdest, fileMode, dirMode) if err != nil { errs = append(errs, err) } diff --git a/fileutils/file.go b/fileutils/file.go index a12f2720..784f728f 100644 --- a/fileutils/file.go +++ b/fileutils/file.go @@ -2,29 +2,28 @@ package fileutils import ( "io" + "io/fs" "os" "path" "path/filepath" "github.com/spf13/afero" - - "github.com/filebrowser/filebrowser/v2/files" ) // MoveFile moves file from src to dst. // By default the rename filesystem system call is used. If src and dst point to different volumes // the file copy is used as a fallback -func MoveFile(fs afero.Fs, src, dst string) error { - if fs.Rename(src, dst) == nil { +func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error { + if afs.Rename(src, dst) == nil { return nil } // fallback - err := Copy(fs, src, dst) + err := Copy(afs, src, dst, fileMode, dirMode) if err != nil { - _ = fs.Remove(dst) + _ = afs.Remove(dst) return err } - if err := fs.RemoveAll(src); err != nil { + if err := afs.RemoveAll(src); err != nil { return err } return nil @@ -32,9 +31,9 @@ func MoveFile(fs afero.Fs, src, dst string) error { // CopyFile copies a file from source to dest and returns // an error if any. -func CopyFile(fs afero.Fs, source, dest string) error { +func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error { // Open the source file. - src, err := fs.Open(source) + src, err := afs.Open(source) if err != nil { return err } @@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error { // Makes the directory needed to create the dst // file. - err = fs.MkdirAll(filepath.Dir(dest), files.PermDir) + err = afs.MkdirAll(filepath.Dir(dest), dirMode) if err != nil { return err } // Create the destination file. - dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile) + dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { return err } @@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error { } // Copy the mode - info, err := fs.Stat(source) + info, err := afs.Stat(source) if err != nil { return err } - err = fs.Chmod(dest, info.Mode()) + err = afs.Chmod(dest, info.Mode()) if err != nil { return err } diff --git a/http/resource.go b/http/resource.go index f657856e..74d7e6c3 100644 --- a/http/resource.go +++ b/http/resource.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "net/http" "net/url" @@ -105,7 +106,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc { // Directories creation on POST. if strings.HasSuffix(r.URL.Path, "/") { - err := d.user.Fs.MkdirAll(r.URL.Path, files.PermDir) + err := d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode) return errToStatus(err), err } @@ -134,7 +135,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc { } err = d.RunHook(func() error { - info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body) + info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode) if writeErr != nil { return writeErr } @@ -171,7 +172,7 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d } err = d.RunHook(func() error { - info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body) + info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode) if writeErr != nil { return writeErr } @@ -243,14 +244,14 @@ func checkParent(src, dst string) error { return nil } -func addVersionSuffix(source string, fs afero.Fs) string { +func addVersionSuffix(source string, afs afero.Fs) string { counter := 1 dir, name := path.Split(source) ext := filepath.Ext(name) base := strings.TrimSuffix(name, ext) for { - if _, err := fs.Stat(source); err != nil { + if _, err := afs.Stat(source); err != nil { break } renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext) @@ -261,14 +262,14 @@ func addVersionSuffix(source string, fs afero.Fs) string { return source } -func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) { +func writeFile(afs afero.Fs, dst string, in io.Reader, fileMode, dirMode fs.FileMode) (os.FileInfo, error) { dir, _ := path.Split(dst) - err := fs.MkdirAll(dir, files.PermDir) + err := afs.MkdirAll(dir, dirMode) if err != nil { return nil, err } - file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile) + file, err := afs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { return nil, err } @@ -306,7 +307,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach return fbErrors.ErrPermissionDenied } - return fileutils.Copy(d.user.Fs, src, dst) + return fileutils.Copy(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode) case "rename": if !d.user.Perm.Rename { return fbErrors.ErrPermissionDenied @@ -332,7 +333,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach return err } - return fileutils.MoveFile(d.user.Fs, src, dst) + return fileutils.MoveFile(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode) default: return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams) } diff --git a/http/tus_handlers.go b/http/tus_handlers.go index f7ed1718..05671cf7 100644 --- a/http/tus_handlers.go +++ b/http/tus_handlers.go @@ -92,7 +92,7 @@ func tusPostHandler() handleFunc { case errors.Is(err, afero.ErrFileNotFound): dirPath := filepath.Dir(r.URL.Path) if _, statErr := d.user.Fs.Stat(dirPath); os.IsNotExist(statErr) { - if mkdirErr := d.user.Fs.MkdirAll(dirPath, files.PermDir); mkdirErr != nil { + if mkdirErr := d.user.Fs.MkdirAll(dirPath, d.settings.DirMode); mkdirErr != nil { return http.StatusInternalServerError, err } } @@ -121,7 +121,7 @@ func tusPostHandler() handleFunc { fileFlags |= os.O_TRUNC } - openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, files.PermFile) + openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, d.settings.FileMode) if err != nil { return errToStatus(err), err } @@ -239,7 +239,7 @@ func tusPatchHandler() handleFunc { ) } - openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, files.PermFile) + openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, d.settings.FileMode) if err != nil { return http.StatusInternalServerError, fmt.Errorf("could not open file: %w", err) } diff --git a/settings/settings.go b/settings/settings.go index 2199968b..787921e9 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "crypto/rand" + "io/fs" "log" "strings" "time" @@ -11,6 +12,8 @@ import ( const DefaultUsersHomeBasePath = "/users" const DefaultMinimumPasswordLength = 12 +const DefaultFileMode = 0640 +const DefaultDirMode = 0750 // AuthMethod describes an authentication method. type AuthMethod string @@ -29,6 +32,8 @@ type Settings struct { Shell []string `json:"shell"` Rules []rules.Rule `json:"rules"` MinimumPasswordLength uint `json:"minimumPasswordLength"` + FileMode fs.FileMode `json:"fileMode"` + DirMode fs.FileMode `json:"dirMode"` } // GetRules implements rules.Provider. diff --git a/settings/storage.go b/settings/storage.go index 64dc4896..d401996d 100644 --- a/settings/storage.go +++ b/settings/storage.go @@ -42,6 +42,12 @@ func (s *Storage) Get() (*Settings, error) { RetryCount: DefaultTusRetryCount, } } + if set.FileMode == 0 { + set.FileMode = DefaultFileMode + } + if set.DirMode == 0 { + set.DirMode = DefaultDirMode + } return set, nil }