Bump github.com/tus/tusd from 1.10.1 to 1.12.1

Bumps [github.com/tus/tusd](https://github.com/tus/tusd) from 1.10.1 to 1.12.1.
- [Release notes](https://github.com/tus/tusd/releases)
- [Commits](https://github.com/tus/tusd/compare/v1.10.1...v1.12.1)

---
updated-dependencies:
- dependency-name: github.com/tus/tusd
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2023-08-09 12:32:35 +02:00
committed by Ralf Haferkamp
parent cbe945af4a
commit 97d3aec987
29 changed files with 5744 additions and 1214 deletions
+7
View File
@@ -22,12 +22,19 @@ type Config struct {
// absolute URL containing a scheme, e.g. "http://tus.io"
BasePath string
isAbs bool
// EnableExperimentalProtocol controls whether the new resumable upload protocol draft
// from the IETF's HTTP working group is accepted next to the current tus v1 protocol.
// See https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/
EnableExperimentalProtocol bool
// DisableDownload indicates whether the server will refuse downloads of the
// uploaded file, by not mounting the GET handler.
DisableDownload bool
// DisableTermination indicates whether the server will refuse termination
// requests of the uploaded file, by not mounting the DELETE handler.
DisableTermination bool
// Disable cors headers. If set to true, tusd will not send any CORS related header.
// This is useful if you have a proxy sitting in front of tusd that handles CORS.
DisableCors bool
// NotifyCompleteUploads indicates whether sending notifications about
// completed uploads using the CompleteUploads channel should be enabled.
NotifyCompleteUploads bool
+241 -35
View File
@@ -7,6 +7,7 @@ import (
"io"
"log"
"math"
"mime"
"net"
"net/http"
"regexp"
@@ -16,6 +17,7 @@ import (
)
const UploadLengthDeferred = "1"
const currentUploadDraftInteropVersion = "3"
var (
reExtractFileID = regexp.MustCompile(`([^/]+)\/?$`)
@@ -98,6 +100,12 @@ type HookEvent struct {
}
func newHookEvent(info FileInfo, r *http.Request) HookEvent {
// The Host header field is not present in the header map, see https://pkg.go.dev/net/http#Request:
// > For incoming requests, the Host header is promoted to the
// > Request.Host field and removed from the Header map.
// That's why we add it back manually.
r.Header.Set("Host", r.Host)
return HookEvent{
Upload: info,
HTTPRequest: HTTPRequest{
@@ -217,7 +225,7 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
header := w.Header()
if origin := r.Header.Get("Origin"); origin != "" {
if origin := r.Header.Get("Origin"); !handler.config.DisableCors && origin != "" {
header.Set("Access-Control-Allow-Origin", origin)
if r.Method == "OPTIONS" {
@@ -232,17 +240,22 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
// Preflight request
header.Add("Access-Control-Allow-Methods", allowedMethods)
header.Add("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat")
header.Add("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version")
header.Set("Access-Control-Max-Age", "86400")
} else {
// Actual request
header.Add("Access-Control-Expose-Headers", "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat")
header.Add("Access-Control-Expose-Headers", "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version")
}
}
// Set current version used by the server
header.Set("Tus-Resumable", "1.0.0")
// Detect requests with tus v1 protocol vs the IETF resumable upload draft
isTusV1 := !handler.isResumableUploadDraftRequest(r)
if isTusV1 {
// Set current version used by the server
header.Set("Tus-Resumable", "1.0.0")
}
// Add nosniff to all responses https://golang.org/src/net/http/server.go#L1429
header.Set("X-Content-Type-Options", "nosniff")
@@ -271,7 +284,7 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
// 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
// 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" && isTusV1 {
handler.sendError(w, r, ErrUnsupportedVersion)
return
}
@@ -284,6 +297,11 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
// PostFile creates a new file upload using the datastore after validating the
// length and parsing the metadata.
func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) {
if handler.isResumableUploadDraftRequest(r) {
handler.PostFileV2(w, r)
return
}
ctx := context.Background()
// Check for presence of application/offset+octet-stream. If another content
@@ -425,6 +443,140 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
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()
// Parse headers
contentType := r.Header.Get("Content-Type")
contentDisposition := r.Header.Get("Content-Disposition")
isComplete := r.Header.Get("Upload-Incomplete") == "?0"
info := FileInfo{
MetaData: make(MetaData),
}
if isComplete && r.ContentLength != -1 {
// If the client wants to perform the upload in one request with Content-Length, we know the final upload size.
info.Size = r.ContentLength
} else {
// Error out if the storage does not support upload length deferring, but we need it.
if !handler.composer.UsesLengthDeferrer {
handler.sendError(w, r, ErrNotImplemented)
return
}
info.SizeIsDeferred = true
}
// Parse Content-Type and Content-Disposition to get file type or file name
if contentType != "" {
fileType, _, err := mime.ParseMediaType(contentType)
if err != nil {
handler.sendError(w, r, err)
return
}
info.MetaData["filetype"] = fileType
}
if contentDisposition != "" {
_, values, err := mime.ParseMediaType(contentDisposition)
if err != nil {
handler.sendError(w, r, err)
return
}
if values["filename"] != "" {
info.MetaData["filename"] = values["filename"]
}
}
// 1. Create upload resource
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
}
info, err = upload.GetInfo(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
id := info.ID
// Add the Location header directly after creating the new resource to even
// include it in cases of failure when an error is returned
url := handler.absFileURL(r, id)
w.Header().Set("Location", url)
// Send 104 response
w.Header().Set("Upload-Draft-Interop-Version", currentUploadDraftInteropVersion)
w.WriteHeader(104)
handler.Metrics.incUploadsCreated()
handler.log("UploadCreated", "id", id, "size", i64toa(info.Size), "url", url)
if handler.config.NotifyCreatedUploads {
handler.CreatedUploads <- newHookEvent(info, r)
}
// 2. Lock upload
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
// 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 isComplete && info.SizeIsDeferred {
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
func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
@@ -457,36 +609,52 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request)
return
}
// Add Upload-Concat header if possible
if info.IsPartial {
w.Header().Set("Upload-Concat", "partial")
}
if info.IsFinal {
v := "final;"
for _, uploadID := range info.PartialUploads {
v += handler.absFileURL(r, uploadID) + " "
}
// Remove trailing space
v = v[:len(v)-1]
w.Header().Set("Upload-Concat", v)
}
if len(info.MetaData) != 0 {
w.Header().Set("Upload-Metadata", SerializeMetadataHeader(info.MetaData))
}
if info.SizeIsDeferred {
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))
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Upload-Offset", strconv.FormatInt(info.Offset, 10))
handler.sendResp(w, r, http.StatusOK)
if !handler.isResumableUploadDraftRequest(r) {
// Add Upload-Concat header if possible
if info.IsPartial {
w.Header().Set("Upload-Concat", "partial")
}
if info.IsFinal {
v := "final;"
for _, uploadID := range info.PartialUploads {
v += handler.absFileURL(r, uploadID) + " "
}
// Remove trailing space
v = v[:len(v)-1]
w.Header().Set("Upload-Concat", v)
}
if len(info.MetaData) != 0 {
w.Header().Set("Upload-Metadata", SerializeMetadataHeader(info.MetaData))
}
if info.SizeIsDeferred {
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))
}
// TODO: We send a 200 OK here by default. Can we switch this to 204?
handler.sendResp(w, r, http.StatusOK)
} else {
if !info.SizeIsDeferred && info.Offset == info.Size {
// Upload is complete if we know the size and it matches the offset.
w.Header().Set("Upload-Incomplete", "?0")
} else {
w.Header().Set("Upload-Incomplete", "?1")
}
w.Header().Set("Upload-Draft-Interop-Version", currentUploadDraftInteropVersion)
// Draft requires a 204 No Content response
handler.sendResp(w, r, http.StatusNoContent)
}
}
// PatchFile adds a chunk to an upload. This operation is only allowed
@@ -494,8 +662,10 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request)
func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
isTusV1 := !handler.isResumableUploadDraftRequest(r)
// Check for presence of application/offset+octet-stream
if r.Header.Get("Content-Type") != "application/offset+octet-stream" {
if isTusV1 && r.Header.Get("Content-Type") != "application/offset+octet-stream" {
handler.sendError(w, r, ErrInvalidContentType)
return
}
@@ -546,6 +716,10 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request
return
}
// TODO: If Upload-Incomplete: ?0 and Content-Length is set, we can
// - declare the length already here
// - validate that the length from this request matches info.Size if !info.SizeIsDeferred
// Do not proxy the call to the data store if the upload is already completed
if !info.SizeIsDeferred && info.Offset == info.Size {
w.Header().Set("Upload-Offset", strconv.FormatInt(offset, 10))
@@ -583,6 +757,32 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request
return
}
isComplete := r.Header.Get("Upload-Incomplete") == "?0"
if isComplete && info.SizeIsDeferred {
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.StatusNoContent)
}
@@ -1130,6 +1330,12 @@ func (handler *UnroutedHandler) lockUpload(id string) (Lock, error) {
return lock, nil
}
// isResumableUploadDraftRequest returns whether a HTTP request includes a sign that it is
// related to resumable upload draft from IETF (instead of tus v1)
func (handler UnroutedHandler) isResumableUploadDraftRequest(r *http.Request) bool {
return handler.config.EnableExperimentalProtocol && r.Header.Get("Upload-Draft-Interop-Version") == currentUploadDraftInteropVersion
}
// ParseMetadataHeader parses the Upload-Metadata header as defined in the
// File Creation extension.
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n