tusd/handler.go

624 lines
17 KiB
Go
Raw Normal View History

package tusd
import (
2015-02-03 18:01:35 +00:00
"encoding/base64"
"errors"
"io"
"log"
"net/http"
"net/url"
"os"
2015-02-17 13:19:56 +00:00
"regexp"
"strconv"
2015-02-03 18:01:35 +00:00
"strings"
)
2015-03-23 16:58:13 +00:00
var reExtractFileID = regexp.MustCompile(`([^/]+)\/?$`)
2015-02-17 13:19:56 +00:00
var (
ErrUnsupportedVersion = errors.New("unsupported version")
ErrMaxSizeExceeded = errors.New("maximum size exceeded")
ErrInvalidContentType = errors.New("missing or invalid Content-Type header")
2015-03-23 17:15:05 +00:00
ErrInvalidUploadLength = errors.New("missing or invalid Upload-Length header")
ErrInvalidOffset = errors.New("missing or invalid Upload-Offset header")
ErrNotFound = errors.New("upload not found")
ErrFileLocked = errors.New("file currently locked")
2015-11-04 09:37:37 +00:00
ErrMismatchOffset = errors.New("mismatched offset")
ErrSizeExceeded = errors.New("resource's size exceeded")
2015-02-06 21:05:33 +00:00
ErrNotImplemented = errors.New("feature not implemented")
2015-02-17 13:19:56 +00:00
ErrUploadNotFinished = errors.New("one of the partial uploads is not finished")
2015-03-23 17:15:05 +00:00
ErrInvalidConcat = errors.New("invalid Upload-Concat header")
2015-02-17 13:19:56 +00:00
ErrModifyFinal = errors.New("modifying a final upload is not allowed")
)
2015-02-01 15:17:56 +00:00
// HTTP status codes sent in the response when the specific error is returned.
var ErrStatusCodes = map[error]int{
ErrUnsupportedVersion: http.StatusPreconditionFailed,
ErrMaxSizeExceeded: http.StatusRequestEntityTooLarge,
ErrInvalidContentType: http.StatusBadRequest,
2015-03-23 17:15:05 +00:00
ErrInvalidUploadLength: http.StatusBadRequest,
ErrInvalidOffset: http.StatusBadRequest,
ErrNotFound: http.StatusNotFound,
ErrFileLocked: 423, // Locked (WebDAV) (RFC 4918)
2015-11-04 22:15:23 +00:00
ErrMismatchOffset: http.StatusConflict,
ErrSizeExceeded: http.StatusRequestEntityTooLarge,
2015-02-06 21:05:33 +00:00
ErrNotImplemented: http.StatusNotImplemented,
2015-02-17 13:19:56 +00:00
ErrUploadNotFinished: http.StatusBadRequest,
ErrInvalidConcat: http.StatusBadRequest,
ErrModifyFinal: http.StatusForbidden,
}
2015-11-05 00:34:23 +00:00
// Config provides a way to configure the Handler depending on your needs.
type Config struct {
2015-02-01 15:17:56 +00:00
// DataStore implementation used to store and retrieve the single uploads.
// Must no be nil.
DataStore DataStore
// MaxSize defines how many bytes may be stored in one single upload. If its
// value is is 0 or smaller no limit will be enforced.
MaxSize int64
// BasePath defines the URL path used for handling uploads, e.g. "/files/".
// If no trailing slash is presented it will be added. You may specify an
// absolute URL containing a scheme, e.g. "http://tus.io"
BasePath string
2015-04-25 22:46:53 +00:00
// Initiate the CompleteUploads channel in the Handler struct in order to
// be notified about complete uploads
NotifyCompleteUploads bool
2015-11-04 21:42:39 +00:00
// Logger the logger to use internally
Logger *log.Logger
}
2015-11-05 00:34:23 +00:00
// Handler exposes methods to handle requests as part of the tus protocol.
// These are PostFile and PatchFile. It also includes GetFile and DelFile
// however these are part of the spec. They are provided for convenience.
type Handler struct {
config Config
dataStore DataStore
isBasePathAbs bool
basePath string
locks map[string]bool
2015-11-04 21:42:39 +00:00
logger *log.Logger
2015-04-18 18:56:24 +00:00
2015-04-25 22:46:53 +00:00
// For each finished upload the corresponding info object will be sent using
// this unbuffered channel. The NotifyCompleteUploads property in the Config
// struct must be set to true in order to work.
CompleteUploads chan FileInfo
}
2015-11-05 00:15:22 +00:00
// NewHandler creates a new handler without routing using the given
// configuration. It exposes the http handlers which need to be combined with
// a router (aka mux) of your choice. If you are looking for preconfigured
// handler see NewRoutedHandler.
func NewHandler(config Config) (*Handler, error) {
2015-11-04 21:42:39 +00:00
logger := config.Logger
if logger == nil {
logger = log.New(os.Stdout, "[tusd] ", 0)
}
base := config.BasePath
uri, err := url.Parse(base)
if err != nil {
return nil, err
}
2015-03-23 16:58:13 +00:00
// Ensure base path ends with slash to remove logic from absFileURL
if base != "" && string(base[len(base)-1]) != "/" {
base += "/"
}
// Ensure base path begins with slash if not absolute (starts with scheme)
if !uri.IsAbs() && len(base) > 0 && string(base[0]) != "/" {
base = "/" + base
}
handler := &Handler{
2015-04-25 22:46:53 +00:00
config: config,
dataStore: config.DataStore,
basePath: base,
isBasePathAbs: uri.IsAbs(),
locks: make(map[string]bool),
CompleteUploads: make(chan FileInfo),
2015-11-04 21:42:39 +00:00
logger: logger,
}
return handler, nil
}
2015-11-05 00:34:23 +00:00
// TusMiddleware checks various aspects of the request and ensures that it
// conforms with the spec. Also handles method overriding for clients which
// cannot make PATCH AND DELETE requests. If you are using the tusd handlers
// directly you will need to wrap at least the POST and PATCH endpoints in
// this middleware.
2015-11-04 22:15:23 +00:00
func (handler *Handler) TusMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow overriding the HTTP method. The reason for this is
// that some libraries/environments to not support PATCH and
// DELETE requests, e.g. Flash in a browser and parts of Java
if newMethod := r.Header.Get("X-HTTP-Method-Override"); newMethod != "" {
r.Method = newMethod
}
2015-11-04 22:15:23 +00:00
go handler.logger.Println(r.Method, r.URL.Path)
2015-11-04 22:15:23 +00:00
header := w.Header()
2015-11-04 22:15:23 +00:00
if origin := r.Header.Get("Origin"); origin != "" {
header.Set("Access-Control-Allow-Origin", origin)
2015-11-04 22:15:23 +00:00
if r.Method == "OPTIONS" {
// Preflight request
header.Set("Access-Control-Allow-Methods", "POST, HEAD, PATCH, OPTIONS")
header.Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata")
header.Set("Access-Control-Max-Age", "86400")
2015-11-04 22:15:23 +00:00
} else {
// Actual request
header.Set("Access-Control-Expose-Headers", "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata")
}
}
2015-11-04 22:15:23 +00:00
// Set current version used by the server
header.Set("Tus-Resumable", "1.0.0")
2015-11-04 22:15:23 +00:00
// Set appropriated headers in case of OPTIONS method allowing protocol
// discovery and end with an 204 No Content
if r.Method == "OPTIONS" {
if handler.config.MaxSize > 0 {
header.Set("Tus-Max-Size", strconv.FormatInt(handler.config.MaxSize, 10))
}
2015-11-04 22:15:23 +00:00
header.Set("Tus-Version", "1.0.0")
header.Set("Tus-Extension", "creation,concatenation,termination")
2015-11-04 22:15:23 +00:00
w.WriteHeader(http.StatusNoContent)
return
}
// Test if the version sent by the client is supported
// GET methods are not checked since a browser may visit this URL and does
// not include this header. This request is not part of the specification.
if r.Method != "GET" && r.Header.Get("Tus-Resumable") != "1.0.0" {
handler.sendError(w, r, ErrUnsupportedVersion)
return
}
// Proceed with routing the request
h.ServeHTTP(w, r)
})
}
2015-11-05 00:34:23 +00:00
// PostFile creates a new file upload using the datastore after validating the
// length and parsing the metadata.
2015-11-05 00:15:22 +00:00
func (handler *Handler) PostFile(w http.ResponseWriter, r *http.Request) {
2015-03-23 17:15:05 +00:00
// Parse Upload-Concat header
isPartial, isFinal, partialUploads, err := parseConcat(r.Header.Get("Upload-Concat"))
2015-02-17 13:19:56 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
2015-02-17 13:19:56 +00:00
// If the upload is a final upload created by concatenation multiple partial
// uploads the size is sum of all sizes of these files (no need for
2015-03-23 17:15:05 +00:00
// Upload-Length header)
2015-02-17 13:19:56 +00:00
var size int64
if isFinal {
size, err = handler.sizeOfUploads(partialUploads)
if err != nil {
handler.sendError(w, r, err)
2015-02-17 13:19:56 +00:00
return
}
} else {
2015-03-23 17:15:05 +00:00
size, err = strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
2015-02-17 13:19:56 +00:00
if err != nil || size < 0 {
handler.sendError(w, r, ErrInvalidUploadLength)
2015-02-17 13:19:56 +00:00
return
}
}
// Test whether the size is still allowed
2015-02-05 17:25:38 +00:00
if handler.config.MaxSize > 0 && size > handler.config.MaxSize {
handler.sendError(w, r, ErrMaxSizeExceeded)
return
}
2015-02-03 18:01:35 +00:00
// Parse metadata
2015-03-23 17:15:05 +00:00
meta := parseMeta(r.Header.Get("Upload-Metadata"))
info := FileInfo{
2015-02-17 13:19:56 +00:00
Size: size,
MetaData: meta,
IsPartial: isPartial,
IsFinal: isFinal,
PartialUploads: partialUploads,
}
id, err := handler.dataStore.NewUpload(info)
if err != nil {
handler.sendError(w, r, err)
return
}
2015-02-17 13:19:56 +00:00
if isFinal {
if err := handler.fillFinalUpload(id, partialUploads); err != nil {
handler.sendError(w, r, err)
2015-02-17 13:19:56 +00:00
return
}
}
2015-03-23 16:58:13 +00:00
url := handler.absFileURL(r, id)
w.Header().Set("Location", url)
w.WriteHeader(http.StatusCreated)
}
2015-11-05 00:34:23 +00:00
// HeadFile returns the length and offset for the HEAD request
2015-11-05 00:15:22 +00:00
func (handler *Handler) HeadFile(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get(":id")
info, err := handler.dataStore.GetInfo(id)
if err != nil {
handler.sendError(w, r, err)
return
}
2015-03-23 17:15:05 +00:00
// Add Upload-Concat header if possible
2015-02-17 13:19:56 +00:00
if info.IsPartial {
2015-03-23 17:15:05 +00:00
w.Header().Set("Upload-Concat", "partial")
2015-02-17 13:19:56 +00:00
}
if info.IsFinal {
v := "final;"
2015-03-23 16:58:13 +00:00
for _, uploadID := range info.PartialUploads {
v += " " + handler.absFileURL(r, uploadID)
2015-02-17 13:19:56 +00:00
}
2015-03-23 17:15:05 +00:00
w.Header().Set("Upload-Concat", v)
2015-02-17 13:19:56 +00:00
}
if len(info.MetaData) != 0 {
2015-03-23 17:15:05 +00:00
w.Header().Set("Upload-Metadata", serializeMeta(info.MetaData))
}
2015-05-26 14:17:35 +00:00
w.Header().Set("Cache-Control", "no-store")
2015-03-23 17:15:05 +00:00
w.Header().Set("Upload-Length", strconv.FormatInt(info.Size, 10))
w.Header().Set("Upload-Offset", strconv.FormatInt(info.Offset, 10))
w.WriteHeader(http.StatusNoContent)
}
2015-11-05 00:34:23 +00:00
// PatchFile adds a chunk to an upload. Only allowed if the upload is not
// locked and enough space is left.
2015-11-05 00:15:22 +00:00
func (handler *Handler) PatchFile(w http.ResponseWriter, r *http.Request) {
//Check for presence of application/offset+octet-stream
if r.Header.Get("Content-Type") != "application/offset+octet-stream" {
handler.sendError(w, r, ErrInvalidContentType)
return
}
//Check for presence of a valid Upload-Offset Header
offset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64)
if err != nil || offset < 0 {
handler.sendError(w, r, ErrInvalidOffset)
return
}
id := extractIDFromPath(r.URL.Path)
// Ensure file is not locked
if _, ok := handler.locks[id]; ok {
handler.sendError(w, r, 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)
if err != nil {
handler.sendError(w, r, err)
return
}
2015-02-17 13:19:56 +00:00
// Modifying a final upload is not allowed
if info.IsFinal {
handler.sendError(w, r, ErrModifyFinal)
2015-02-17 13:19:56 +00:00
return
}
if offset != info.Offset {
2015-11-04 09:37:37 +00:00
handler.sendError(w, r, ErrMismatchOffset)
return
}
// Get Content-Length if possible
length := r.ContentLength
// Test if this upload fits into the file's size
if offset+length > info.Size {
handler.sendError(w, r, ErrSizeExceeded)
return
}
maxSize := info.Size - offset
if length > 0 {
maxSize = length
}
// Limit the
reader := io.LimitReader(r.Body, maxSize)
bytesWritten, err := handler.dataStore.WriteChunk(id, offset, reader)
if err != nil {
handler.sendError(w, r, err)
return
}
// Send new offset to client
2015-04-18 18:56:24 +00:00
newOffset := offset + bytesWritten
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))
// If the upload is completed, send the info out to the channel
2015-04-25 22:46:53 +00:00
if handler.config.NotifyCompleteUploads && newOffset == info.Size {
2015-04-18 18:56:24 +00:00
info.Size = newOffset
2015-04-25 22:46:53 +00:00
handler.CompleteUploads <- info
2015-04-18 18:56:24 +00:00
}
w.WriteHeader(http.StatusNoContent)
}
2015-11-05 00:34:23 +00:00
// GetFile handles requests to download a file using a GET request. This is not
// part of the specification.
2015-11-05 00:15:22 +00:00
func (handler *Handler) GetFile(w http.ResponseWriter, r *http.Request) {
id := extractIDFromPath(r.URL.Path)
2015-02-06 21:05:33 +00:00
// Ensure file is not locked
if _, ok := handler.locks[id]; ok {
handler.sendError(w, r, ErrFileLocked)
2015-02-06 21:05:33 +00:00
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)
if err != nil {
handler.sendError(w, r, err)
2015-02-06 21:05:33 +00:00
return
}
// Do not do anything if no data is stored yet.
if info.Offset == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
// Get reader
src, err := handler.dataStore.GetReader(id)
if err != nil {
handler.sendError(w, r, err)
2015-02-06 21:05:33 +00:00
return
}
w.Header().Set("Content-Length", strconv.FormatInt(info.Offset, 10))
w.WriteHeader(http.StatusOK)
io.Copy(w, src)
2015-07-28 12:58:52 +00:00
// Try to close the reader if the io.Closer interface is implemented
if closer, ok := src.(io.Closer); ok {
closer.Close()
}
2015-02-06 21:05:33 +00:00
}
2015-11-05 00:34:23 +00:00
// DelFile terminates an upload permanently.
2015-11-05 00:15:22 +00:00
func (handler *Handler) DelFile(w http.ResponseWriter, r *http.Request) {
id := extractIDFromPath(r.URL.Path)
2015-02-28 13:47:39 +00:00
// Ensure file is not locked
if _, ok := handler.locks[id]; ok {
handler.sendError(w, r, ErrFileLocked)
2015-02-28 13:47:39 +00:00
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)
if err != nil {
handler.sendError(w, r, err)
2015-02-28 13:47:39 +00:00
return
}
w.WriteHeader(http.StatusNoContent)
}
2015-02-01 15:17:56 +00:00
// Send the error in the response body. The status code will be looked up in
// ErrStatusCodes. If none is found 500 Internal Error will be used.
func (handler *Handler) sendError(w http.ResponseWriter, r *http.Request, err error) {
2015-02-28 13:53:01 +00:00
// Interpret os.ErrNotExist as 404 Not Found
if os.IsNotExist(err) {
err = ErrNotFound
}
status, ok := ErrStatusCodes[err]
if !ok {
status = 500
}
reason := err.Error()
if r.Method == "HEAD" {
reason = ""
}
2015-11-06 00:59:24 +00:00
reason += "\n"
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", strconv.Itoa(len(reason)))
w.WriteHeader(status)
2015-11-06 00:59:24 +00:00
w.Write([]byte(reason))
}
2015-02-01 15:17:56 +00:00
// Make an absolute URLs to the given upload id. If the base path is absolute
// it will be prepended else the host and protocol from the request is used.
2015-03-23 16:58:13 +00:00
func (handler *Handler) absFileURL(r *http.Request, id string) string {
if handler.isBasePathAbs {
return handler.basePath + id
}
// Read origin and protocol from request
url := "http://"
if r.TLS != nil {
url = "https://"
}
url += r.Host + handler.basePath + id
return url
}
2015-02-03 18:01:35 +00:00
2015-02-17 13:19:56 +00:00
// The get sum of all sizes for a list of upload ids while checking whether
// all of these uploads are finished yet. This is used to calculate the size
// of a final resource.
func (handler *Handler) sizeOfUploads(ids []string) (size int64, err error) {
for _, id := range ids {
info, err := handler.dataStore.GetInfo(id)
if err != nil {
return size, err
}
if info.Offset != info.Size {
err = ErrUploadNotFinished
return size, err
}
size += info.Size
}
return
}
// Fill an empty upload with the content of the uploads by their ids. The data
// will be written in the order as they appear in the slice
func (handler *Handler) fillFinalUpload(id string, uploads []string) error {
readers := make([]io.Reader, len(uploads))
2015-03-23 16:58:13 +00:00
for index, uploadID := range uploads {
reader, err := handler.dataStore.GetReader(uploadID)
2015-02-17 13:19:56 +00:00
if err != nil {
return err
}
readers[index] = reader
}
reader := io.MultiReader(readers...)
_, err := handler.dataStore.WriteChunk(id, 0, reader)
2015-02-17 13:19:56 +00:00
return err
2015-02-17 13:19:56 +00:00
}
2015-03-23 17:15:05 +00:00
// Parse the Upload-Metadata header as defined in the File Creation extension.
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
2015-02-03 18:01:35 +00:00
func parseMeta(header string) map[string]string {
meta := make(map[string]string)
for _, element := range strings.Split(header, ",") {
element := strings.TrimSpace(element)
parts := strings.Split(element, " ")
// Do not continue with this element if no key and value or presented
if len(parts) != 2 {
continue
}
// Ignore corrent element if the value is no valid base64
key := parts[0]
value, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
continue
}
meta[key] = string(value)
}
return meta
}
2015-02-17 13:19:56 +00:00
2015-03-23 17:15:05 +00:00
// Serialize a map of strings into the Upload-Metadata header format used in the
// response for HEAD requests.
2015-03-23 17:15:05 +00:00
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
func serializeMeta(meta map[string]string) string {
header := ""
for key, value := range meta {
valueBase64 := base64.StdEncoding.EncodeToString([]byte(value))
header += key + " " + valueBase64 + ","
}
// Remove trailing comma
if len(header) > 0 {
header = header[:len(header)-1]
}
return header
}
2015-03-23 17:15:05 +00:00
// Parse the Upload-Concat header, e.g.
// Upload-Concat: partial
// Upload-Concat: final; http://tus.io/files/a /files/b/
2015-02-17 13:19:56 +00:00
func parseConcat(header string) (isPartial bool, isFinal bool, partialUploads []string, err error) {
if len(header) == 0 {
return
}
if header == "partial" {
isPartial = true
return
}
l := len("final; ")
if strings.HasPrefix(header, "final; ") && len(header) > l {
isFinal = true
list := strings.Split(header[l:], " ")
for _, value := range list {
value := strings.TrimSpace(value)
if value == "" {
continue
}
id := extractIDFromPath(value)
if id == "" {
2015-02-17 13:19:56 +00:00
err = ErrInvalidConcat
return
}
partialUploads = append(partialUploads, id)
2015-02-17 13:19:56 +00:00
}
}
// If no valid partial upload ids are extracted this is not a final upload.
if len(partialUploads) == 0 {
isFinal = false
err = ErrInvalidConcat
}
return
}
// extractIDFromPath pulls the last segment from the url provided
func extractIDFromPath(url string) string {
result := reExtractFileID.FindStringSubmatch(url)
if len(result) != 2 {
return ""
}
return result[1]
}