Refactoring file locking out of tusd.Handler
* Updated filestore.FileStore implementation to use simple locking scheme. Refactored to make interface use pointers. Removed locks from tusd.Handler * Refactored sample code to use pointer implementation of FileStore. * Moved ErrFileLocked variable to datastore.go
This commit is contained in:
parent
3b4353578d
commit
766dabc238
|
@ -39,9 +39,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var store tusd.DataStore
|
var store tusd.DataStore
|
||||||
store = filestore.FileStore{
|
store = filestore.NewFileStore(dir)
|
||||||
Path: dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
if storeSize > 0 {
|
if storeSize > 0 {
|
||||||
store = limitedstore.New(storeSize, store)
|
store = limitedstore.New(storeSize, store)
|
||||||
|
|
|
@ -2,8 +2,15 @@ package tusd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
package filestore
|
package filestore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -23,10 +24,20 @@ var defaultFilePerm = os.FileMode(0775)
|
||||||
type FileStore struct {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store FileStore) NewUpload(info tusd.FileInfo) (id string, err error) {
|
// NewFileStore creates a new FileStore instance.
|
||||||
|
func NewFileStore(path string) (store *FileStore) {
|
||||||
|
store = &FileStore{
|
||||||
|
Path: path,
|
||||||
|
locks: make(map[string]bool),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *FileStore) NewUpload(info tusd.FileInfo) (id string, err error) {
|
||||||
id = uid.Uid()
|
id = uid.Uid()
|
||||||
info.ID = id
|
info.ID = id
|
||||||
|
|
||||||
|
@ -42,8 +53,13 @@ func (store FileStore) NewUpload(info tusd.FileInfo) (id string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
file, err := os.OpenFile(store.binPath(id), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -58,21 +74,28 @@ func (store FileStore) WriteChunk(id string, offset int64, src io.Reader) (int64
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store FileStore) GetInfo(id string) (tusd.FileInfo, error) {
|
func (store *FileStore) GetInfo(id string) (info tusd.FileInfo, err error) {
|
||||||
info := tusd.FileInfo{}
|
data, err := ioutil.ReadFile(store.infoPath(id))
|
||||||
data, err := ioutil.ReadFile(store.infoPath(id))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(data, &info)
|
err = json.Unmarshal(data, &info)
|
||||||
return info, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store FileStore) GetReader(id string) (io.Reader, error) {
|
func (store *FileStore) GetReader(id string) (io.Reader, error) {
|
||||||
return os.Open(store.binPath(id))
|
if !store.getLock(id) {
|
||||||
|
return bytes.NewReader(make([]byte, 0)), tusd.ErrFileLocked
|
||||||
|
}
|
||||||
|
defer store.clearLock(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
|
||||||
}
|
}
|
||||||
|
@ -83,17 +106,17 @@ func (store FileStore) Terminate(id string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the path to the .info file storing the file's info
|
// Return the path to the .info file storing the file's info
|
||||||
func (store FileStore) infoPath(id string) string {
|
func (store *FileStore) infoPath(id string) string {
|
||||||
return store.Path + "/" + id + ".info"
|
return store.Path + "/" + id + ".info"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the entire information. Everything will be overwritten.
|
// Update the entire information. Everything will be overwritten.
|
||||||
func (store FileStore) writeInfo(id string, info tusd.FileInfo) error {
|
func (store *FileStore) writeInfo(id string, info tusd.FileInfo) error {
|
||||||
data, err := json.Marshal(info)
|
data, err := json.Marshal(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -102,7 +125,7 @@ func (store FileStore) writeInfo(id string, info tusd.FileInfo) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the .info file using the new upload.
|
// Update the .info file using the new upload.
|
||||||
func (store FileStore) setOffset(id string, offset int64) error {
|
func (store *FileStore) setOffset(id string, offset int64) error {
|
||||||
info, err := store.GetInfo(id)
|
info, err := store.GetInfo(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -116,3 +139,19 @@ 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)
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test interface implementation of Filestore
|
// Test interface implementation of Filestore
|
||||||
var _ tusd.DataStore = FileStore{}
|
var _ tusd.DataStore = NewFileStore("")
|
||||||
|
|
||||||
func TestFilestore(t *testing.T) {
|
func TestFilestore(t *testing.T) {
|
||||||
tmp, err := ioutil.TempDir("", "tusd-filestore-")
|
tmp, err := ioutil.TempDir("", "tusd-filestore-")
|
||||||
|
@ -19,7 +19,7 @@ func TestFilestore(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
store := FileStore{tmp}
|
store := NewFileStore(tmp)
|
||||||
|
|
||||||
// Create new upload
|
// Create new upload
|
||||||
id, err := store.NewUpload(tusd.FileInfo{
|
id, err := store.NewUpload(tusd.FileInfo{
|
||||||
|
|
45
handler.go
45
handler.go
|
@ -25,7 +25,6 @@ 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")
|
||||||
|
@ -72,7 +71,6 @@ type Handler struct {
|
||||||
isBasePathAbs bool
|
isBasePathAbs bool
|
||||||
basePath string
|
basePath string
|
||||||
routeHandler http.Handler
|
routeHandler http.Handler
|
||||||
locks map[string]bool
|
|
||||||
|
|
||||||
// For each finished upload the corresponding info object will be sent using
|
// For each finished upload the corresponding info object will be sent using
|
||||||
// this unbuffered channel. The NotifyCompleteUploads property in the Config
|
// this unbuffered channel. The NotifyCompleteUploads property in the Config
|
||||||
|
@ -106,7 +104,6 @@ func NewHandler(config Config) (*Handler, error) {
|
||||||
basePath: base,
|
basePath: base,
|
||||||
isBasePathAbs: uri.IsAbs(),
|
isBasePathAbs: uri.IsAbs(),
|
||||||
routeHandler: mux,
|
routeHandler: mux,
|
||||||
locks: make(map[string]bool),
|
|
||||||
CompleteUploads: make(chan FileInfo),
|
CompleteUploads: make(chan FileInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,20 +273,6 @@ func (handler *Handler) headFile(w http.ResponseWriter, r *http.Request) {
|
||||||
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")
|
||||||
|
|
||||||
// Ensure file is not locked
|
|
||||||
if _, ok := handler.locks[id]; ok {
|
|
||||||
handler.sendError(w, ErrFileLocked)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock file for further writes (heads are allowed)
|
|
||||||
handler.locks[id] = true
|
|
||||||
|
|
||||||
// File will be unlocked regardless of an error or success
|
|
||||||
defer func() {
|
|
||||||
delete(handler.locks, id)
|
|
||||||
}()
|
|
||||||
|
|
||||||
info, err := handler.dataStore.GetInfo(id)
|
info, err := handler.dataStore.GetInfo(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handler.sendError(w, err)
|
handler.sendError(w, err)
|
||||||
|
@ -354,20 +337,6 @@ func (handler *Handler) patchFile(w http.ResponseWriter, r *http.Request) {
|
||||||
func (handler *Handler) getFile(w http.ResponseWriter, r *http.Request) {
|
func (handler *Handler) getFile(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get(":id")
|
id := r.URL.Query().Get(":id")
|
||||||
|
|
||||||
// Ensure file is not locked
|
|
||||||
if _, ok := handler.locks[id]; ok {
|
|
||||||
handler.sendError(w, ErrFileLocked)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock file for further writes (heads are allowed)
|
|
||||||
handler.locks[id] = true
|
|
||||||
|
|
||||||
// File will be unlocked regardless of an error or success
|
|
||||||
defer func() {
|
|
||||||
delete(handler.locks, id)
|
|
||||||
}()
|
|
||||||
|
|
||||||
info, err := handler.dataStore.GetInfo(id)
|
info, err := handler.dataStore.GetInfo(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handler.sendError(w, err)
|
handler.sendError(w, err)
|
||||||
|
@ -401,20 +370,6 @@ func (handler *Handler) getFile(w http.ResponseWriter, r *http.Request) {
|
||||||
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")
|
||||||
|
|
||||||
// Ensure file is not locked
|
|
||||||
if _, ok := handler.locks[id]; ok {
|
|
||||||
handler.sendError(w, ErrFileLocked)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock file for further writes (heads are allowed)
|
|
||||||
handler.locks[id] = true
|
|
||||||
|
|
||||||
// File will be unlocked regardless of an error or success
|
|
||||||
defer func() {
|
|
||||||
delete(handler.locks, 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)
|
||||||
|
|
Loading…
Reference in New Issue