Compare commits

...

4 Commits

Author SHA1 Message Date
Marius 80ff08a50c Correctly use sf-boolean syntax for Upload-Incomplete 2021-11-20 16:57:20 +01:00
Marius 947141b180 Merge branch 'master' of github.com:tus/tusd into feat/new-tus-protocol 2021-11-20 16:52:23 +01:00
Marius b7da32553d Implement basic functionality 2021-11-15 22:46:07 +01:00
Marius c941e5ef9a Add flag to enable support for tus v2 2021-11-15 13:38:48 +01:00
6 changed files with 168 additions and 12 deletions

View File

@ -56,6 +56,7 @@ var Flags struct {
TLSCertFile string TLSCertFile string
TLSKeyFile string TLSKeyFile string
TLSMode string TLSMode string
TusV2 bool
CPUProfile string CPUProfile string
} }
@ -102,6 +103,7 @@ func ParseFlags() {
flag.StringVar(&Flags.TLSCertFile, "tls-certificate", "", "Path to the file containing the x509 TLS certificate to be used. The file should also contain any intermediate certificates and the CA certificate.") flag.StringVar(&Flags.TLSCertFile, "tls-certificate", "", "Path to the file containing the x509 TLS certificate to be used. The file should also contain any intermediate certificates and the CA certificate.")
flag.StringVar(&Flags.TLSKeyFile, "tls-key", "", "Path to the file containing the key for the TLS certificate.") flag.StringVar(&Flags.TLSKeyFile, "tls-key", "", "Path to the file containing the key for the TLS certificate.")
flag.StringVar(&Flags.TLSMode, "tls-mode", "tls12", "Specify which TLS mode to use; valid modes are tls13, tls12, and tls12-strong.") flag.StringVar(&Flags.TLSMode, "tls-mode", "tls12", "Specify which TLS mode to use; valid modes are tls13, tls12, and tls12-strong.")
flag.BoolVar(&Flags.TusV2, "enable-tus-v2", false, "Enable support for the tus v2 protocol, next to support for v1 (experimental and may be removed/changed in the future)")
flag.StringVar(&Flags.CPUProfile, "cpuprofile", "", "write cpu profile to file") flag.StringVar(&Flags.CPUProfile, "cpuprofile", "", "write cpu profile to file")
flag.Parse() flag.Parse()

View File

@ -27,6 +27,7 @@ func Serve() {
MaxSize: Flags.MaxSize, MaxSize: Flags.MaxSize,
BasePath: Flags.Basepath, BasePath: Flags.Basepath,
RespectForwardedHeaders: Flags.BehindProxy, RespectForwardedHeaders: Flags.BehindProxy,
EnableTusV2: Flags.TusV2,
StoreComposer: Composer, StoreComposer: Composer,
NotifyCompleteUploads: true, NotifyCompleteUploads: true,
NotifyTerminatedUploads: true, NotifyTerminatedUploads: true,

View File

@ -49,9 +49,11 @@ func (store FileStore) UseIn(composer *handler.StoreComposer) {
} }
func (store FileStore) NewUpload(ctx context.Context, info handler.FileInfo) (handler.Upload, error) { func (store FileStore) NewUpload(ctx context.Context, info handler.FileInfo) (handler.Upload, error) {
id := uid.Uid() if info.ID == "" {
info.ID = uid.Uid()
}
id := info.ID
binPath := store.binPath(id) binPath := store.binPath(id)
info.ID = id
info.Storage = map[string]string{ info.Storage = map[string]string{
"Type": "filestore", "Type": "filestore",
"Path": binPath, "Path": binPath,

View File

@ -22,6 +22,9 @@ type Config struct {
// absolute URL containing a scheme, e.g. "http://tus.io" // absolute URL containing a scheme, e.g. "http://tus.io"
BasePath string BasePath string
isAbs bool isAbs bool
// EnableTusV2 controls whether the new and experimental tus v2 protocol is
// accepted, next to the current tus v1 protocol.
EnableTusV2 bool
// NotifyCompleteUploads indicates whether sending notifications about // NotifyCompleteUploads indicates whether sending notifications about
// completed uploads using the CompleteUploads channel should be enabled. // completed uploads using the CompleteUploads channel should be enabled.
NotifyCompleteUploads bool NotifyCompleteUploads bool

View File

@ -47,5 +47,14 @@ func NewHandler(config Config) (*Handler, error) {
mux.Del(":id", http.HandlerFunc(handler.DelFile)) mux.Del(":id", http.HandlerFunc(handler.DelFile))
} }
if config.EnableTusV2 {
mux.Head("", http.HandlerFunc(handler.HeadFile))
// Only attach the DELETE handler if the Terminate() method is provided
if config.StoreComposer.UsesTerminater {
mux.Del("", http.HandlerFunc(handler.DelFile))
}
}
return routedHandler, nil return routedHandler, nil
} }

View File

@ -262,7 +262,7 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
// Test if the version sent by the client is supported // Test if the version sent by the client is supported
// GET and HEAD methods are not checked since a browser may visit this URL and does // GET and HEAD methods are not checked since a browser may visit this URL and does
// not include this header. GET requests are not part of the specification. // not include this header. GET requests are not part of the specification.
if r.Method != "GET" && r.Method != "HEAD" && r.Header.Get("Tus-Resumable") != "1.0.0" { if r.Method != "GET" && r.Method != "HEAD" && r.Header.Get("Tus-Resumable") != "1.0.0" && !handler.config.EnableTusV2 {
handler.sendError(w, r, ErrUnsupportedVersion) handler.sendError(w, r, ErrUnsupportedVersion)
return return
} }
@ -275,6 +275,11 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
// PostFile creates a new file upload using the datastore after validating the // PostFile creates a new file upload using the datastore after validating the
// length and parsing the metadata. // length and parsing the metadata.
func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) { func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) {
if isTusV2Request(r) {
handler.PostFileV2(w, r)
return
}
ctx := context.Background() ctx := context.Background()
// Check for presence of application/offset+octet-stream. If another content // Check for presence of application/offset+octet-stream. If another content
@ -416,11 +421,123 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
handler.sendResp(w, r, http.StatusCreated) handler.sendResp(w, r, http.StatusCreated)
} }
// PostFile creates a new file upload using the datastore after validating the
// length and parsing the metadata.
func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// TODO: Check that upload length deferring is supported
// Parse headers
// TODO: Make parsing of Upload-Offset optional
// TODO: What is the correct valud for Upload-Incomplete
// TODO: Also consider Content-Type and Content-Disposition (using https://play.golang.org/p/AjWbJB8vUk)
token := r.Header.Get("Upload-Token")
offset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64)
if err != nil || offset < 0 {
handler.sendError(w, r, ErrInvalidOffset)
return
}
isIncomplete := r.Header.Get("Upload-Incomplete") == "?1"
// 1. Get or create upload resource
// TODO: Create consistent ID from token? e.g. using SHA256
id := token
upload, err := handler.composer.Core.GetUpload(ctx, id)
if err == ErrNotFound {
info := FileInfo{
ID: id,
SizeIsDeferred: true,
// TODO: Set metadata?
// MetaData: meta,
}
if handler.config.PreUploadCreateCallback != nil {
if err := handler.config.PreUploadCreateCallback(newHookEvent(info, r)); err != nil {
handler.sendError(w, r, err)
return
}
}
upload, err = handler.composer.Core.NewUpload(ctx, info)
if err != nil {
handler.sendError(w, r, err)
return
}
handler.Metrics.incUploadsCreated()
handler.log("UploadCreated", "id", id, "size", "n/a", "url", "n/a")
if handler.config.NotifyCreatedUploads {
handler.CreatedUploads <- newHookEvent(info, r)
}
} else if err != nil {
handler.sendError(w, r, err)
return
}
// 2. Verify offset
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
info, err := upload.GetInfo(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
if offset != info.Offset {
handler.sendError(w, r, ErrMismatchOffset)
return
}
// 3. Write chunk
if err := handler.writeChunk(ctx, upload, info, w, r); err != nil {
handler.sendError(w, r, err)
return
}
// 4. Finish upload, if necessary
if !isIncomplete {
info, err = upload.GetInfo(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
uploadLength := info.Offset
lengthDeclarableUpload := handler.composer.LengthDeferrer.AsLengthDeclarableUpload(upload)
if err := lengthDeclarableUpload.DeclareLength(ctx, uploadLength); err != nil {
handler.sendError(w, r, err)
return
}
info.Size = uploadLength
info.SizeIsDeferred = false
if err := handler.finishUploadIfComplete(ctx, upload, info, r); err != nil {
handler.sendError(w, r, err)
return
}
}
handler.sendResp(w, r, http.StatusCreated)
}
// HeadFile returns the length and offset for the HEAD request // HeadFile returns the length and offset for the HEAD request
func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) { func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx := context.Background()
id, err := extractIDFromPath(r.URL.Path) id, err := handler.extractUploadID(r)
if err != nil { if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
@ -464,15 +581,23 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request)
w.Header().Set("Upload-Concat", v) w.Header().Set("Upload-Concat", v)
} }
if len(info.MetaData) != 0 { if !isTusV2Request(r) {
w.Header().Set("Upload-Metadata", SerializeMetadataHeader(info.MetaData)) if len(info.MetaData) != 0 {
} w.Header().Set("Upload-Metadata", SerializeMetadataHeader(info.MetaData))
}
if info.SizeIsDeferred { if info.SizeIsDeferred {
w.Header().Set("Upload-Defer-Length", UploadLengthDeferred) w.Header().Set("Upload-Defer-Length", UploadLengthDeferred)
} else {
w.Header().Set("Upload-Length", strconv.FormatInt(info.Size, 10))
w.Header().Set("Content-Length", strconv.FormatInt(info.Size, 10))
}
} else { } else {
w.Header().Set("Upload-Length", strconv.FormatInt(info.Size, 10)) if info.SizeIsDeferred {
w.Header().Set("Content-Length", strconv.FormatInt(info.Size, 10)) w.Header().Set("Upload-Incomplete", "?1")
} else {
w.Header().Set("Upload-Incomplete", "?0")
}
} }
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
@ -840,7 +965,7 @@ func (handler *UnroutedHandler) DelFile(w http.ResponseWriter, r *http.Request)
return return
} }
id, err := extractIDFromPath(r.URL.Path) id, err := handler.extractUploadID(r)
if err != nil { if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
@ -1013,6 +1138,14 @@ func (handler *UnroutedHandler) sendProgressMessages(hook HookEvent, reader *bod
return stop return stop
} }
func (handler *UnroutedHandler) extractUploadID(r *http.Request) (string, error) {
if isTusV2Request(r) {
return r.Header.Get("Upload-Token"), nil
}
return extractIDFromPath(r.URL.Path)
}
// getHostAndProtocol extracts the host and used protocol (either HTTP or HTTPS) // getHostAndProtocol extracts the host and used protocol (either HTTP or HTTPS)
// from the given request. If `allowForwarded` is set, the X-Forwarded-Host, // from the given request. If `allowForwarded` is set, the X-Forwarded-Host,
// X-Forwarded-Proto and Forwarded headers will also be checked to // X-Forwarded-Proto and Forwarded headers will also be checked to
@ -1247,3 +1380,9 @@ func getRequestId(r *http.Request) string {
return reqId return reqId
} }
// isTusV2Request returns whether a HTTP request includes a sign that it is
// related to tus v2 (instead of tus v1)
func isTusV2Request(r *http.Request) bool {
return r.Header.Get("Upload-Token") != ""
}