Adding LockFile and UnlockFile to server API (per comments on issue #22)
* Added Locked member to tusd.FileInfo object * Pushed locking mechanisms down to the JSON files themselves.
This commit is contained in:
parent
4c1f2b99a2
commit
4f8dba9d9d
21
datastore.go
21
datastore.go
|
@ -1,16 +1,9 @@
|
||||||
package tusd
|
package tusd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error indicating that the data store has locked the file for further edits.
|
|
||||||
// This error is not a part of the official tus specification. Implementers of
|
|
||||||
// tusd.DataStore have the option to return this error to signal a file is
|
|
||||||
// locked for writing, and cannot be written to by another HTTP request.
|
|
||||||
var ErrFileLocked = errors.New("file currently locked")
|
|
||||||
|
|
||||||
type MetaData map[string]string
|
type MetaData map[string]string
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
|
@ -30,6 +23,11 @@ type FileInfo struct {
|
||||||
// ordered slice containing the ids of the uploads of which the final upload
|
// ordered slice containing the ids of the uploads of which the final upload
|
||||||
// will consist after concatenation.
|
// will consist after concatenation.
|
||||||
PartialUploads []string
|
PartialUploads []string
|
||||||
|
// Indicates that a write operation is currently pending for this file.
|
||||||
|
// This field is not needed to implement the tus specification.
|
||||||
|
// Implementations of tusd.DataStore may use this to prevent write
|
||||||
|
// collisions or race conditions.
|
||||||
|
Locked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataStore interface {
|
type DataStore interface {
|
||||||
|
@ -61,4 +59,13 @@ type DataStore interface {
|
||||||
// Terminate an upload so any further requests to the resource, both reading
|
// Terminate an upload so any further requests to the resource, both reading
|
||||||
// and writing, must return os.ErrNotExist or similar.
|
// and writing, must return os.ErrNotExist or similar.
|
||||||
Terminate(id string) error
|
Terminate(id string) error
|
||||||
|
// Lock the upload file for writing. This feature is not part of the
|
||||||
|
// official TUS specification. Handlers should call this prior to writing
|
||||||
|
// chunks or terminating files. Returns true if the file lock has been
|
||||||
|
// acquired.
|
||||||
|
LockFile(id string) (bool, error)
|
||||||
|
// Unlock the upload file for writing. This feature is not part of the
|
||||||
|
// official TUS specification. Handlers should defer calls to this after
|
||||||
|
// acquiring a lock.
|
||||||
|
UnlockFile(id string) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,14 +25,12 @@ type FileStore struct {
|
||||||
// Relative or absolute path to store files in. FileStore does not check
|
// Relative or absolute path to store files in. FileStore does not check
|
||||||
// whether the path exists, you os.MkdirAll in this case on your own.
|
// whether the path exists, you os.MkdirAll in this case on your own.
|
||||||
Path string
|
Path string
|
||||||
locks map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileStore creates a new FileStore instance.
|
// NewFileStore creates a new FileStore instance.
|
||||||
func NewFileStore(path string) (store *FileStore) {
|
func NewFileStore(path string) (store *FileStore) {
|
||||||
store = &FileStore{
|
store = &FileStore{
|
||||||
Path: path,
|
Path: path,
|
||||||
locks: make(map[string]bool),
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -54,11 +52,6 @@ func (store *FileStore) NewUpload(info tusd.FileInfo) (id string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *FileStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
|
func (store *FileStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
|
||||||
if !store.getLock(id) {
|
|
||||||
return 0, tusd.ErrFileLocked
|
|
||||||
}
|
|
||||||
defer store.clearLock(id)
|
|
||||||
|
|
||||||
file, err := os.OpenFile(store.binPath(id), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
|
file, err := os.OpenFile(store.binPath(id), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -84,18 +77,18 @@ func (store *FileStore) GetInfo(id string) (info tusd.FileInfo, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *FileStore) GetReader(id string) (io.Reader, error) {
|
func (store *FileStore) GetReader(id string) (io.Reader, error) {
|
||||||
if !store.getLock(id) {
|
hasLock, err := store.LockFile(id)
|
||||||
|
if err != nil {
|
||||||
|
return bytes.NewReader(make([]byte, 0)), err
|
||||||
|
}
|
||||||
|
if !hasLock {
|
||||||
return bytes.NewReader(make([]byte, 0)), tusd.ErrFileLocked
|
return bytes.NewReader(make([]byte, 0)), tusd.ErrFileLocked
|
||||||
}
|
}
|
||||||
defer store.clearLock(id)
|
defer store.UnlockFile(id)
|
||||||
return os.Open(store.binPath(id))
|
return os.Open(store.binPath(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *FileStore) Terminate(id string) error {
|
func (store *FileStore) Terminate(id string) error {
|
||||||
if !store.getLock(id) {
|
|
||||||
return tusd.ErrFileLocked
|
|
||||||
}
|
|
||||||
defer store.clearLock(id)
|
|
||||||
if err := os.Remove(store.infoPath(id)); err != nil {
|
if err := os.Remove(store.infoPath(id)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -105,6 +98,37 @@ func (store *FileStore) Terminate(id string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store *FileStore) LockFile(id string) (hasLock bool, err) {
|
||||||
|
info, err := store.GetInfo(id)
|
||||||
|
if err != nil {
|
||||||
|
hasLock = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.Locked {
|
||||||
|
// Cannot acquire lock if something else has the lock.
|
||||||
|
hasLock = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info.Locked = true
|
||||||
|
err = writeInfo(id, info)
|
||||||
|
if err != nil {
|
||||||
|
hasLock = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasLock = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *FileStore) UnlockFile(id string) (err error) {
|
||||||
|
info, err := store.GetInfo(id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info.Locked = false
|
||||||
|
err = writeInfo(info)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Return the path to the .bin storing the binary data
|
// Return the path to the .bin storing the binary data
|
||||||
func (store *FileStore) binPath(id string) string {
|
func (store *FileStore) binPath(id string) string {
|
||||||
return store.Path + "/" + id + ".bin"
|
return store.Path + "/" + id + ".bin"
|
||||||
|
@ -139,19 +163,3 @@ func (store *FileStore) setOffset(id string, offset int64) error {
|
||||||
info.Offset = offset
|
info.Offset = offset
|
||||||
return store.writeInfo(id, info)
|
return store.writeInfo(id, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLock obtains a lock on reading/writing data for the given file ID.
|
|
||||||
func (store *FileStore) getLock(id string) (hasLock bool) {
|
|
||||||
if _, locked := store.locks[id]; locked {
|
|
||||||
hasLock = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.locks[id] = true
|
|
||||||
hasLock = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearLock removes the lock for the given file ID.
|
|
||||||
func (store *FileStore) clearLock(id string) {
|
|
||||||
delete(store.locks, id)
|
|
||||||
}
|
|
||||||
|
|
32
handler.go
32
handler.go
|
@ -25,6 +25,7 @@ var (
|
||||||
ErrInvalidUploadLength = errors.New("missing or invalid Upload-Length header")
|
ErrInvalidUploadLength = errors.New("missing or invalid Upload-Length header")
|
||||||
ErrInvalidOffset = errors.New("missing or invalid Upload-Offset header")
|
ErrInvalidOffset = errors.New("missing or invalid Upload-Offset header")
|
||||||
ErrNotFound = errors.New("upload not found")
|
ErrNotFound = errors.New("upload not found")
|
||||||
|
ErrFileLocked = errors.New("file currently locked")
|
||||||
ErrIllegalOffset = errors.New("illegal offset")
|
ErrIllegalOffset = errors.New("illegal offset")
|
||||||
ErrSizeExceeded = errors.New("resource's size exceeded")
|
ErrSizeExceeded = errors.New("resource's size exceeded")
|
||||||
ErrNotImplemented = errors.New("feature not implemented")
|
ErrNotImplemented = errors.New("feature not implemented")
|
||||||
|
@ -272,6 +273,11 @@ func (handler *Handler) headFile(w http.ResponseWriter, r *http.Request) {
|
||||||
// space is left.
|
// space is left.
|
||||||
func (handler *Handler) patchFile(w http.ResponseWriter, r *http.Request) {
|
func (handler *Handler) patchFile(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get(":id")
|
id := r.URL.Query().Get(":id")
|
||||||
|
err := handler.getLock(id)
|
||||||
|
if err != nil {
|
||||||
|
handler.sendError(w, err)
|
||||||
|
}
|
||||||
|
defer handler.releaseLock(id)
|
||||||
|
|
||||||
info, err := handler.dataStore.GetInfo(id)
|
info, err := handler.dataStore.GetInfo(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -369,8 +375,14 @@ func (handler *Handler) getFile(w http.ResponseWriter, r *http.Request) {
|
||||||
// Terminate an upload permanently.
|
// Terminate an upload permanently.
|
||||||
func (handler *Handler) delFile(w http.ResponseWriter, r *http.Request) {
|
func (handler *Handler) delFile(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get(":id")
|
id := r.URL.Query().Get(":id")
|
||||||
|
err := handler.getLock(id)
|
||||||
|
if err != nil {
|
||||||
|
handler.sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer handler.releaseLock(id)
|
||||||
|
|
||||||
err := handler.dataStore.Terminate(id)
|
err = handler.dataStore.Terminate(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handler.sendError(w, err)
|
handler.sendError(w, err)
|
||||||
return
|
return
|
||||||
|
@ -454,6 +466,24 @@ func (handler *Handler) fillFinalUpload(id string, uploads []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the lock from the data store, returning an error if a true error occurred
|
||||||
|
// or if the file could not be locked.
|
||||||
|
func (handler *Handler) getLock(id string) (error) {
|
||||||
|
hasLock, err := handler.dataStore.LockFile(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasLock {
|
||||||
|
return ErrFileLocked
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the lock from the data store
|
||||||
|
func (handler *Handler) releaseLock(id string) (error) {
|
||||||
|
return handler.dataStore.UnlockFile(id)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the Upload-Metadata header as defined in the File Creation extension.
|
// Parse the Upload-Metadata header as defined in the File Creation extension.
|
||||||
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
|
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
|
||||||
func parseMeta(header string) map[string]string {
|
func parseMeta(header string) map[string]string {
|
||||||
|
|
|
@ -28,6 +28,14 @@ func (store zeroStore) Terminate(id string) error {
|
||||||
return ErrNotImplemented
|
return ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store zeroStore) LockFile(id string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store zeroStore) UnlockFile(id string) (error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type httpTest struct {
|
type httpTest struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue