diff --git a/cmd/tusd/cli/composer.go b/cmd/tusd/cli/composer.go index 2d17f8582..2118a982f 100644 --- a/cmd/tusd/cli/composer.go +++ b/cmd/tusd/cli/composer.go @@ -144,11 +144,14 @@ 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.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 33e00db50..b91332562 100644 --- a/cmd/tusd/cli/flags.go +++ b/cmd/tusd/cli/flags.go @@ -2,11 +2,14 @@ package cli import ( "flag" + "fmt" "path/filepath" + "strconv" "strings" "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" ) @@ -74,11 +77,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 = filestore.DefaultDirPerm + Flags.FilePerms = filestore.DefaultFilePerm + fs := grouped_flags.NewFlagGroupSet(flag.ExitOnError) fs.AddGroup("Listening options", func(f *flag.FlagSet) { @@ -116,6 +144,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..f708b7a3d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -6,4 +6,8 @@ set -o pipefail . /usr/local/share/load-env.sh +if printenv UMASK >/dev/null; then + umask "$UMASK" +fi + exec tusd "$@" diff --git a/docs/_advanced-topics/usage-package.md b/docs/_advanced-topics/usage-package.md index a003a90aa..8417d2a43 100644 --- a/docs/_advanced-topics/usage-package.md +++ b/docs/_advanced-topics/usage-package.md @@ -28,6 +28,11 @@ func main() { // a remote FTP server, you can implement your own storage backend // by implementing the tusd.DataStore interface. 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/pkg/filestore/filestore.go b/pkg/filestore/filestore.go index 6f9ab5229..879d68300 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,52 @@ 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. + +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 + + 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) FileStore { - return FileStore{path} + 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 @@ -73,14 +104,16 @@ 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.DirModePerm, store.FileModePerm, nil); err != nil { return nil, err } upload := &fileUpload{ - info: info, - infoPath: infoPath, - binPath: binPath, + info: info, + infoPath: infoPath, + binPath: binPath, + dirModePerm: store.DirModePerm, + fileModePerm: store.FileModePerm, } // writeInfo creates the file by itself if necessary @@ -130,9 +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, + info: info, + binPath: binPath, + infoPath: infoPath, + dirModePerm: store.DirModePerm, + fileModePerm: store.FileModePerm, }, nil } @@ -166,6 +201,9 @@ type fileUpload struct { infoPath string // binPath is the path to the binary file (which has no extension) binPath string + + dirModePerm fs.FileMode + fileModePerm fs.FileMode } func (upload *fileUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) { @@ -173,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, defaultFilePerm) + file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.fileModePerm) if err != nil { return 0, err } @@ -212,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, defaultFilePerm) + file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, upload.fileModePerm) if err != nil { return err } @@ -253,7 +291,7 @@ func (upload *fileUpload) writeInfo() error { if err != nil { return err } - return createFile(upload.infoPath, data) + return createFile(upload.infoPath, upload.dirModePerm, upload.fileModePerm, data) } func (upload *fileUpload) FinishUpload(ctx context.Context) error { @@ -262,19 +300,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..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") + 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 693a8b6ad..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") + store := filestore.NewWithOptions("some-path", &filestore.FileStoreOptions{ + DirPerm: 0775, + FilePerm: 0664, + }) config := handler.Config{ StoreComposer: handler.NewStoreComposer(), }