diff --git a/pkg/filelocker/filelocker.go b/pkg/filelocker/filelocker.go index 3a33f4b..78ae8cb 100644 --- a/pkg/filelocker/filelocker.go +++ b/pkg/filelocker/filelocker.go @@ -1,19 +1,12 @@ -// Package filestore provide a storage backend based on the local file system. +// Package filelocker provide an upload locker based on the local file system. // -// FileStore is a storage backend used as a handler.DataStore in handler.NewHandler. -// It stores the uploads in a directory specified in two different files: The -// `[id].info` files are used to store the fileinfo in JSON format. The -// `[id]` files without an extension contain the raw binary data uploaded. -// No cleanup is performed so you may want to run a cronjob to ensure your disk -// is not filled up with old and finished uploads. -// -// In addition, it provides an exclusive upload locking mechanism using lock files +// It provides an exclusive upload locking mechanism using lock files // which are stored on disk. Each of them stores the PID of the process which // acquired the lock. This allows locks to be automatically freed when a process // is unable to release it on its own because the process is not alive anymore. // For more information, consult the documentation for handler.LockerDataStore -// interface, which is implemented by FileStore -package filestore +// interface, which is implemented by FileLocker. +package filelocker import ( "os" @@ -42,16 +35,16 @@ func New(path string) FileLocker { return FileLocker{path} } -func (locker FileLocker) NewLock(id string) (handler.UploadLock, error) { +func (locker FileLocker) NewLock(id string) (handler.Lock, error) { path, err := filepath.Abs(filepath.Join(locker.Path, id+".lock")) if err != nil { - return lockfile.Lockfile(""), err + return nil, err } // We use Lockfile directly instead of lockfile.New to bypass the unnecessary // check whether the provided path is absolute since we just resolved it // on our own. - return fileUploadLock{ + return &fileUploadLock{ file: lockfile.Lockfile(path), }, nil } @@ -61,7 +54,7 @@ type fileUploadLock struct { } func (lock fileUploadLock) Lock() error { - err = lock.file.TryLock() + err := lock.file.TryLock() if err == lockfile.ErrBusy { return handler.ErrFileLocked } @@ -70,7 +63,7 @@ func (lock fileUploadLock) Lock() error { } func (lock fileUploadLock) Unlock() error { - err = lock.file.Unlock() + err := lock.file.Unlock() // A "no such file or directory" will be returned if no lockfile was found. // Since this means that the file has never been locked, we drop the error diff --git a/pkg/filelocker/filelocker_test.go b/pkg/filelocker/filelocker_test.go new file mode 100644 index 0000000..8403a0a --- /dev/null +++ b/pkg/filelocker/filelocker_test.go @@ -0,0 +1,32 @@ +package filelocker + +import ( + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tus/tusd/pkg/handler" +) + +var _ handler.Locker = &FileLocker{} + +func TestFileLocker(t *testing.T) { + a := assert.New(t) + + dir, err := ioutil.TempDir("", "tusd-file-locker") + a.NoError(err) + + locker := FileLocker{dir} + + lock1, err := locker.NewLock("one") + a.NoError(err) + + a.NoError(lock1.Lock()) + a.Equal(handler.ErrFileLocked, lock1.Lock()) + + lock2, err := locker.NewLock("one") + a.NoError(err) + a.Equal(handler.ErrFileLocked, lock2.Lock()) + + a.NoError(lock1.Unlock()) +} diff --git a/pkg/handler/datastore.go b/pkg/handler/datastore.go index 10f9f98..ab6bd57 100644 --- a/pkg/handler/datastore.go +++ b/pkg/handler/datastore.go @@ -144,3 +144,28 @@ type LengthDeferrerDataStore interface { type LengthDeclarableUpload interface { DeclareLength(length int64) error } + +// Locker is the interface required for custom lock persisting mechanisms. +// Common ways to store this information is in memory, on disk or using an +// external service, such as ZooKeeper. +// When multiple processes are attempting to access an upload, whether it be +// by reading or writing, a synchronization mechanism is required to prevent +// data corruption, especially to ensure correct offset values and the proper +// order of chunks inside a single upload. +type Locker interface { + // NewLock creates a new unlocked lock object for the given upload ID. + NewLock(id string) (Lock, error) +} + +// Lock is the interface for a lock as returned from a Locker. +type Lock interface { + // Lock attempts to obtain an exclusive lock for the upload specified + // by its id. + // If this operation fails because the resource is already locked, the + // tusd.ErrFileLocked must be returned. If no error is returned, the attempt + // is consider to be successful and the upload to be locked until UnlockUpload + // is invoked for the same upload. + Lock() error + // Unlock releases an existing lock for the given upload. + Unlock() error +}