Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

filestore: add dirs(0775 def) and files(0664 def) chmod(2) perms cmdline options #1155

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cmd/tusd/cli/composer.go
Original file line number Diff line number Diff line change
@@ -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)
30 changes: 30 additions & 0 deletions cmd/tusd/cli/flags.go
Original file line number Diff line number Diff line change
@@ -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) {
4 changes: 4 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -6,4 +6,8 @@ set -o pipefail

. /usr/local/share/load-env.sh

if printenv UMASK >/dev/null; then
umask "$UMASK"
fi

exec tusd "$@"
5 changes: 5 additions & 0 deletions docs/_advanced-topics/usage-package.md
Original file line number Diff line number Diff line change
@@ -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
74 changes: 56 additions & 18 deletions pkg/filestore/filestore.go
Original file line number Diff line number Diff line change
@@ -18,29 +18,60 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"

"github.com/tus/tusd/v2/internal/uid"
"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,14 +201,17 @@ 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) {
return upload.info, nil
}

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
5 changes: 4 additions & 1 deletion pkg/handler/composer_test.go
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 4 additions & 1 deletion pkg/hooks/hooks_test.go
Original file line number Diff line number Diff line change
@@ -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(),
}