From 9fab210649b068223100b55ef8e6da18d6025c3d Mon Sep 17 00:00:00 2001 From: Andrey Nikitin Date: Fri, 12 Jul 2024 19:42:22 +0300 Subject: [PATCH 1/3] filestore: add dirs(0775) and files(0644) chmod(2) perms options --- cmd/tusd/cli/composer.go | 4 +-- cmd/tusd/cli/flags.go | 29 ++++++++++++++++++++++ docker/entrypoint.sh | 4 +++ docs/_advanced-topics/usage-package.md | 2 +- examples/server/main.go | 2 +- pkg/filestore/filestore.go | 34 ++++++++++++++++---------- pkg/handler/composer_test.go | 2 +- pkg/hooks/hooks_test.go | 2 +- 8 files changed, 60 insertions(+), 19 deletions(-) diff --git a/cmd/tusd/cli/composer.go b/cmd/tusd/cli/composer.go index 2d17f8582..6fa1997da 100644 --- a/cmd/tusd/cli/composer.go +++ b/cmd/tusd/cli/composer.go @@ -144,11 +144,11 @@ func CreateComposer() { } stdout.Printf("Using '%s' as directory storage.\n", dir) - if err := os.MkdirAll(dir, os.FileMode(0774)); err != nil { + if err := os.MkdirAll(dir, os.FileMode(Flags.DirPerms)); err != nil { stderr.Fatalf("Unable to ensure directory exists: %s", err) } - store := filestore.New(dir) + store := filestore.New(dir, Flags.DirPerms, Flags.FilePerms) store.UseIn(Composer) locker := filelocker.New(dir) diff --git a/cmd/tusd/cli/flags.go b/cmd/tusd/cli/flags.go index 33e00db50..f9a9092fe 100644 --- a/cmd/tusd/cli/flags.go +++ b/cmd/tusd/cli/flags.go @@ -2,7 +2,9 @@ package cli import ( "flag" + "fmt" "path/filepath" + "strconv" "strings" "time" @@ -74,11 +76,36 @@ var Flags struct { AcquireLockTimeout time.Duration FilelockHolderPollInterval time.Duration FilelockAcquirerPollInterval time.Duration + FilePerms uint32 + DirPerms uint32 GracefulRequestCompletionTimeout time.Duration ExperimentalProtocol bool } +type ChmodPermsValue struct { + perms *uint32 +} + +func (v ChmodPermsValue) String() string { + if v.perms != nil { + return fmt.Sprintf("%o", *v.perms) + } + return "" +} + +func (v ChmodPermsValue) Set(s string) error { + if u, err := strconv.ParseUint(s, 8, 32); err != nil { + return err + } else { + *v.perms = uint32(u) + } + return nil +} + func ParseFlags() { + Flags.DirPerms = 0775 + Flags.FilePerms = 0664 + fs := grouped_flags.NewFlagGroupSet(flag.ExitOnError) fs.AddGroup("Listening options", func(f *flag.FlagSet) { @@ -116,6 +143,8 @@ func ParseFlags() { f.StringVar(&Flags.UploadDir, "upload-dir", "./data", "Directory to store uploads in") f.DurationVar(&Flags.FilelockHolderPollInterval, "filelock-holder-poll-interval", 5*time.Second, "The holder of a lock polls regularly to see if another request handler needs the lock. This flag specifies the poll interval.") f.DurationVar(&Flags.FilelockAcquirerPollInterval, "filelock-acquirer-poll-interval", 2*time.Second, "The acquirer of a lock polls regularly to see if the lock has been released. This flag specifies the poll interval.") + f.Var(&ChmodPermsValue{&Flags.DirPerms}, "dir-perms", "The created directory chmod(2) OCTAL value permissions.") + f.Var(&ChmodPermsValue{&Flags.FilePerms}, "file-perms", "The created file chmod(2) OCTAL value permissions.") }) fs.AddGroup("AWS S3 storage options", func(f *flag.FlagSet) { diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 88a70efbc..3ec3f3957 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -6,4 +6,8 @@ set -o pipefail . /usr/local/share/load-env.sh +if [ -n "$UMASK" ]; then + umask "$UMASK" +fi + exec tusd "$@" diff --git a/docs/_advanced-topics/usage-package.md b/docs/_advanced-topics/usage-package.md index a003a90aa..147ad45b2 100644 --- a/docs/_advanced-topics/usage-package.md +++ b/docs/_advanced-topics/usage-package.md @@ -27,7 +27,7 @@ func main() { // If you want to save them on a different medium, for example // a remote FTP server, you can implement your own storage backend // by implementing the tusd.DataStore interface. - store := filestore.New("./uploads") + store := filestore.New("./uploads", 0774, 0664) // A locking mechanism helps preventing data loss or corruption from // parallel requests to a upload resource. A good match for the disk-based diff --git a/examples/server/main.go b/examples/server/main.go index d0624062d..680c9420c 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -16,7 +16,7 @@ func main() { // If you want to save them on a different medium, for example // a remote FTP server, you can implement your own storage backend // by implementing the tusd.DataStore interface. - store := filestore.New("./uploads") + store := filestore.New("./uploads", 0774, 0664) // A locking mechanism helps preventing data loss or corruption from // parallel requests to a upload resource. A good match for the disk-based diff --git a/pkg/filestore/filestore.go b/pkg/filestore/filestore.go index 6f9ab5229..c0b091593 100644 --- a/pkg/filestore/filestore.go +++ b/pkg/filestore/filestore.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" @@ -25,22 +26,22 @@ import ( "github.com/tus/tusd/v2/pkg/handler" ) -var defaultFilePerm = os.FileMode(0664) -var defaultDirectoryPerm = os.FileMode(0754) - // See the handler.DataStore interface for documentation about the different // methods. type FileStore struct { // Relative or absolute path to store files in. FileStore does not check // whether the path exists, use os.MkdirAll in this case on your own. Path string + + DirPerm fs.FileMode + FilePerm fs.FileMode } // New creates a new file based storage backend. The directory specified will // be used as the only storage entry. This method does not check // whether the path exists, use os.MkdirAll to ensure. -func New(path string) FileStore { - return FileStore{path} +func New(path string, dirPerms uint32, filePerms uint32) FileStore { + return FileStore{path, os.FileMode(dirPerms), os.FileMode(filePerms)} } // UseIn sets this store as the core data store in the passed composer and adds @@ -73,7 +74,7 @@ func (store FileStore) NewUpload(ctx context.Context, info handler.FileInfo) (ha } // Create binary file with no content - if err := createFile(binPath, nil); err != nil { + if err := createFile(binPath, store.DirPerm, store.FilePerm, nil); err != nil { return nil, err } @@ -81,6 +82,8 @@ func (store FileStore) NewUpload(ctx context.Context, info handler.FileInfo) (ha info: info, infoPath: infoPath, binPath: binPath, + dirPerm: store.DirPerm, + filePerm: store.FilePerm, } // writeInfo creates the file by itself if necessary @@ -133,6 +136,8 @@ func (store FileStore) GetUpload(ctx context.Context, id string) (handler.Upload info: info, binPath: binPath, infoPath: infoPath, + dirPerm: store.DirPerm, + filePerm: store.FilePerm, }, nil } @@ -166,6 +171,9 @@ type fileUpload struct { infoPath string // binPath is the path to the binary file (which has no extension) binPath string + + dirPerm fs.FileMode + filePerm fs.FileMode } func (upload *fileUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) { @@ -173,7 +181,7 @@ func (upload *fileUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) } func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { - file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) + file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.filePerm) if err != nil { return 0, err } @@ -212,7 +220,7 @@ func (upload *fileUpload) Terminate(ctx context.Context) error { } func (upload *fileUpload) ConcatUploads(ctx context.Context, uploads []handler.Upload) (err error) { - file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) + file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.filePerm) if err != nil { return err } @@ -253,7 +261,7 @@ func (upload *fileUpload) writeInfo() error { if err != nil { return err } - return createFile(upload.infoPath, data) + return createFile(upload.infoPath, upload.dirPerm, upload.filePerm, data) } func (upload *fileUpload) FinishUpload(ctx context.Context) error { @@ -262,19 +270,19 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) error { // createFile creates the file with the content. If the corresponding directory does not exist, // it is created. If the file already exists, its content is removed. -func createFile(path string, content []byte) error { - file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultFilePerm) +func createFile(path string, dirPerm fs.FileMode, filePerm fs.FileMode, content []byte) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePerm) if err != nil { if os.IsNotExist(err) { // An upload ID containing slashes is mapped onto different directories on disk, // for example, `myproject/uploadA` should be put into a folder called `myproject`. // If we get an error indicating that a directory is missing, we try to create it. - if err := os.MkdirAll(filepath.Dir(path), defaultDirectoryPerm); err != nil { + if err := os.MkdirAll(filepath.Dir(path), dirPerm); err != nil { return fmt.Errorf("failed to create directory for %s: %s", path, err) } // Try creating the file again. - file, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultFilePerm) + file, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePerm) if err != nil { // If that still doesn't work, error out. return err diff --git a/pkg/handler/composer_test.go b/pkg/handler/composer_test.go index cb2e2dee7..d391394f0 100644 --- a/pkg/handler/composer_test.go +++ b/pkg/handler/composer_test.go @@ -9,7 +9,7 @@ import ( func ExampleNewStoreComposer() { composer := handler.NewStoreComposer() - fs := filestore.New("./data") + fs := filestore.New("./data", 0774, 0664) fs.UseIn(composer) ml := memorylocker.New() diff --git a/pkg/hooks/hooks_test.go b/pkg/hooks/hooks_test.go index 693a8b6ad..327f208be 100644 --- a/pkg/hooks/hooks_test.go +++ b/pkg/hooks/hooks_test.go @@ -19,7 +19,7 @@ func TestNewHandlerWithHooks(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - store := filestore.New("some-path") + store := filestore.New("some-path", 0774, 0664) config := handler.Config{ StoreComposer: handler.NewStoreComposer(), } From bd7578d820b07019b495774abfb7325500aaa8a9 Mon Sep 17 00:00:00 2001 From: Andrey Nikitin Date: Mon, 15 Jul 2024 13:27:07 +0300 Subject: [PATCH 2/3] filestore: use FileStore *options struct --- cmd/tusd/cli/composer.go | 5 +- cmd/tusd/cli/flags.go | 5 +- docs/_advanced-topics/usage-package.md | 7 ++- examples/server/main.go | 2 +- pkg/filestore/filestore.go | 70 ++++++++++++++++++-------- pkg/handler/composer_test.go | 5 +- pkg/hooks/hooks_test.go | 5 +- 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/cmd/tusd/cli/composer.go b/cmd/tusd/cli/composer.go index 6fa1997da..2118a982f 100644 --- a/cmd/tusd/cli/composer.go +++ b/cmd/tusd/cli/composer.go @@ -148,7 +148,10 @@ func CreateComposer() { stderr.Fatalf("Unable to ensure directory exists: %s", err) } - store := filestore.New(dir, Flags.DirPerms, Flags.FilePerms) + store := filestore.NewWithOptions(dir, &filestore.FileStoreOptions{ + DirPerm: Flags.DirPerms, + FilePerm: Flags.FilePerms, + }) store.UseIn(Composer) locker := filelocker.New(dir) diff --git a/cmd/tusd/cli/flags.go b/cmd/tusd/cli/flags.go index f9a9092fe..b91332562 100644 --- a/cmd/tusd/cli/flags.go +++ b/cmd/tusd/cli/flags.go @@ -9,6 +9,7 @@ import ( "time" "github.com/tus/tusd/v2/internal/grouped_flags" + "github.com/tus/tusd/v2/pkg/filestore" "github.com/tus/tusd/v2/pkg/hooks" "golang.org/x/exp/slices" ) @@ -103,8 +104,8 @@ func (v ChmodPermsValue) Set(s string) error { } func ParseFlags() { - Flags.DirPerms = 0775 - Flags.FilePerms = 0664 + Flags.DirPerms = filestore.DefaultDirPerm + Flags.FilePerms = filestore.DefaultFilePerm fs := grouped_flags.NewFlagGroupSet(flag.ExitOnError) diff --git a/docs/_advanced-topics/usage-package.md b/docs/_advanced-topics/usage-package.md index 147ad45b2..8417d2a43 100644 --- a/docs/_advanced-topics/usage-package.md +++ b/docs/_advanced-topics/usage-package.md @@ -27,7 +27,12 @@ func main() { // If you want to save them on a different medium, for example // a remote FTP server, you can implement your own storage backend // by implementing the tusd.DataStore interface. - store := filestore.New("./uploads", 0774, 0664) + store := filestore.New("./uploads") + // or use options + // filestore.NewWithOptions(dir, &filestore.FileStoreOptions{ + // DirPerm: 0777, // see also umask(2) + // FilePerm: 0666, // see also umask(2) + // }) // A locking mechanism helps preventing data loss or corruption from // parallel requests to a upload resource. A good match for the disk-based diff --git a/examples/server/main.go b/examples/server/main.go index 680c9420c..d0624062d 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -16,7 +16,7 @@ func main() { // If you want to save them on a different medium, for example // a remote FTP server, you can implement your own storage backend // by implementing the tusd.DataStore interface. - store := filestore.New("./uploads", 0774, 0664) + store := filestore.New("./uploads") // A locking mechanism helps preventing data loss or corruption from // parallel requests to a upload resource. A good match for the disk-based diff --git a/pkg/filestore/filestore.go b/pkg/filestore/filestore.go index c0b091593..879d68300 100644 --- a/pkg/filestore/filestore.go +++ b/pkg/filestore/filestore.go @@ -28,20 +28,50 @@ import ( // See the handler.DataStore interface for documentation about the different // methods. + +const DefaultDirPerm = 0775 +const DefaultFilePerm = 0664 + +type FileStoreOptions struct { + DirPerm uint32 + FilePerm uint32 +} + +var defaultOptions = FileStoreOptions{ + DirPerm: DefaultDirPerm, + FilePerm: DefaultFilePerm, +} + type FileStore struct { // Relative or absolute path to store files in. FileStore does not check // whether the path exists, use os.MkdirAll in this case on your own. Path string - DirPerm fs.FileMode - FilePerm fs.FileMode + DirModePerm fs.FileMode + FileModePerm fs.FileMode } // New creates a new file based storage backend. The directory specified will // be used as the only storage entry. This method does not check // whether the path exists, use os.MkdirAll to ensure. -func New(path string, dirPerms uint32, filePerms uint32) FileStore { - return FileStore{path, os.FileMode(dirPerms), os.FileMode(filePerms)} +func New(path string) FileStore { + return FileStore{ + Path: path, + DirModePerm: os.FileMode(defaultOptions.DirPerm) & os.ModePerm, + FileModePerm: os.FileMode(defaultOptions.FilePerm) & os.ModePerm, + } +} + +func NewWithOptions(path string, options *FileStoreOptions) FileStore { + if options == nil { + options = &defaultOptions + } + + return FileStore{ + Path: path, + DirModePerm: os.FileMode(options.DirPerm) & os.ModePerm, + FileModePerm: os.FileMode(options.FilePerm) & os.ModePerm, + } } // UseIn sets this store as the core data store in the passed composer and adds @@ -74,16 +104,16 @@ func (store FileStore) NewUpload(ctx context.Context, info handler.FileInfo) (ha } // Create binary file with no content - if err := createFile(binPath, store.DirPerm, store.FilePerm, nil); err != nil { + if err := createFile(binPath, store.DirModePerm, store.FileModePerm, nil); err != nil { return nil, err } upload := &fileUpload{ - info: info, - infoPath: infoPath, - binPath: binPath, - dirPerm: store.DirPerm, - filePerm: store.FilePerm, + info: info, + infoPath: infoPath, + binPath: binPath, + dirModePerm: store.DirModePerm, + fileModePerm: store.FileModePerm, } // writeInfo creates the file by itself if necessary @@ -133,11 +163,11 @@ func (store FileStore) GetUpload(ctx context.Context, id string) (handler.Upload info.Offset = stat.Size() return &fileUpload{ - info: info, - binPath: binPath, - infoPath: infoPath, - dirPerm: store.DirPerm, - filePerm: store.FilePerm, + info: info, + binPath: binPath, + infoPath: infoPath, + dirModePerm: store.DirModePerm, + fileModePerm: store.FileModePerm, }, nil } @@ -172,8 +202,8 @@ type fileUpload struct { // binPath is the path to the binary file (which has no extension) binPath string - dirPerm fs.FileMode - filePerm fs.FileMode + dirModePerm fs.FileMode + fileModePerm fs.FileMode } func (upload *fileUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) { @@ -181,7 +211,7 @@ func (upload *fileUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) } func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { - file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.filePerm) + file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.fileModePerm) if err != nil { return 0, err } @@ -220,7 +250,7 @@ func (upload *fileUpload) Terminate(ctx context.Context) error { } func (upload *fileUpload) ConcatUploads(ctx context.Context, uploads []handler.Upload) (err error) { - file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.filePerm) + file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.fileModePerm) if err != nil { return err } @@ -261,7 +291,7 @@ func (upload *fileUpload) writeInfo() error { if err != nil { return err } - return createFile(upload.infoPath, upload.dirPerm, upload.filePerm, data) + return createFile(upload.infoPath, upload.dirModePerm, upload.fileModePerm, data) } func (upload *fileUpload) FinishUpload(ctx context.Context) error { diff --git a/pkg/handler/composer_test.go b/pkg/handler/composer_test.go index d391394f0..9b8fb311a 100644 --- a/pkg/handler/composer_test.go +++ b/pkg/handler/composer_test.go @@ -9,7 +9,10 @@ import ( func ExampleNewStoreComposer() { composer := handler.NewStoreComposer() - fs := filestore.New("./data", 0774, 0664) + fs := filestore.New("./data", &filestore.FileStoreOptions{ + DirPerm: 0775, + FilePerm: 0664, + }) fs.UseIn(composer) ml := memorylocker.New() diff --git a/pkg/hooks/hooks_test.go b/pkg/hooks/hooks_test.go index 327f208be..df6dc4e40 100644 --- a/pkg/hooks/hooks_test.go +++ b/pkg/hooks/hooks_test.go @@ -19,7 +19,10 @@ func TestNewHandlerWithHooks(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - store := filestore.New("some-path", 0774, 0664) + store := filestore.NewWithOptions("some-path", &filestore.FileStoreOptions{ + DirPerm: 0775, + FilePerm: 0664, + }) config := handler.Config{ StoreComposer: handler.NewStoreComposer(), } From 3af7c3b3eb0182abb206a4262d59fd68863995e0 Mon Sep 17 00:00:00 2001 From: Andrey Nikitin Date: Mon, 15 Jul 2024 13:28:08 +0300 Subject: [PATCH 3/3] docker: make UMASK env optional (fix docker error) --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 3ec3f3957..f708b7a3d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -6,7 +6,7 @@ set -o pipefail . /usr/local/share/load-env.sh -if [ -n "$UMASK" ]; then +if printenv UMASK >/dev/null; then umask "$UMASK" fi