Remove old tusd code
This commit is contained in:
parent
4c10bea894
commit
9f29ced4ec
|
@ -1,74 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInvalidRange = errors.New("invalid Content-Range")
|
|
||||||
|
|
||||||
type contentRange struct {
|
|
||||||
Start int64
|
|
||||||
End int64
|
|
||||||
Size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseContentRange parse a Content-Range string like "5-10/100".
|
|
||||||
// see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 .
|
|
||||||
// "*" values causes End/Size to be set to -1, see test case for more details.
|
|
||||||
func parseContentRange(s string) (*contentRange, error) {
|
|
||||||
const prefix = "bytes "
|
|
||||||
offset := strings.Index(s, prefix)
|
|
||||||
if offset != 0 {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
s = s[len(prefix):]
|
|
||||||
|
|
||||||
parts := strings.Split(s, "/")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
r := new(contentRange)
|
|
||||||
|
|
||||||
if parts[0] == "*" {
|
|
||||||
r.Start = 0
|
|
||||||
r.End = -1
|
|
||||||
} else {
|
|
||||||
offsets := strings.Split(parts[0], "-")
|
|
||||||
if len(offsets) != 2 {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset, err := strconv.ParseInt(offsets[0], 10, 64); err == nil {
|
|
||||||
r.Start = offset
|
|
||||||
} else {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset, err := strconv.ParseInt(offsets[1], 10, 64); err == nil {
|
|
||||||
r.End = offset
|
|
||||||
} else {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// A byte-content-range-spec with a byte-range-resp-spec whose last-
|
|
||||||
// byte-pos value is less than its first-byte-pos value, or whose
|
|
||||||
// instance-length value is less than or equal to its last-byte-pos value,
|
|
||||||
// is invalid. The recipient of an invalid byte-content-range- spec MUST
|
|
||||||
// ignore it and any content transferred along with it.
|
|
||||||
if r.End <= r.Start {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts[1] == "*" {
|
|
||||||
r.Size = -1
|
|
||||||
return r, nil
|
|
||||||
} else if size, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
|
|
||||||
r.Size = size
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ContentRangeTests = []struct {
|
|
||||||
s string
|
|
||||||
want contentRange
|
|
||||||
err string
|
|
||||||
}{
|
|
||||||
{s: "bytes 0-5/100", want: contentRange{Start: 0, End: 5, Size: 100}},
|
|
||||||
{s: "bytes 5-20/30", want: contentRange{Start: 5, End: 20, Size: 30}},
|
|
||||||
{s: "bytes */100", want: contentRange{Start: 0, End: -1, Size: 100}},
|
|
||||||
{s: "bytes 5-20/*", want: contentRange{Start: 5, End: 20, Size: -1}},
|
|
||||||
{s: "bytes */*", want: contentRange{Start: 0, End: -1, Size: -1}},
|
|
||||||
{s: "bytes 0-2147483647/2147483648", want: contentRange{Start: 0, End: 2147483647, Size: 2147483648}},
|
|
||||||
{s: "bytes 5-20", err: "invalid"},
|
|
||||||
{s: "bytes 5-5/100", err: "invalid"},
|
|
||||||
{s: "bytes 5-4/100", err: "invalid"},
|
|
||||||
{s: "bytes ", err: "invalid"},
|
|
||||||
{s: "", err: "invalid"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseContentRange(t *testing.T) {
|
|
||||||
for _, test := range ContentRangeTests {
|
|
||||||
t.Logf("testing: %s", test.s)
|
|
||||||
|
|
||||||
r, err := parseContentRange(test.s)
|
|
||||||
if test.err != "" {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("got no error, but expected: %s", test.err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
errMatch := regexp.MustCompile(test.err)
|
|
||||||
if !errMatch.MatchString(err.Error()) {
|
|
||||||
t.Errorf("unexpected error: %s, wanted: %s", err, test.err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
t.Errorf("unexpected error: %s, wanted: %+v", err, test.want)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if *r != test.want {
|
|
||||||
t.Errorf("got: %+v, wanted: %+v", r, test.want)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,257 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fileRoute matches /files/<id>. Go seems to use \r to terminate header
|
|
||||||
// values, so to ease bash scripting, the route ignores a trailing \r in the
|
|
||||||
// route. Better ideas are welcome.
|
|
||||||
var fileRoute = regexp.MustCompile("^/files/([^/\r\n]+)\r?$")
|
|
||||||
|
|
||||||
var filesRoute = regexp.MustCompile("^/files/?$")
|
|
||||||
var dataStore *DataStore
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataDir := path.Join(wd, "tus_data")
|
|
||||||
if configDir := os.Getenv("TUSD_DATA_DIR"); configDir != "" {
|
|
||||||
dataDir = configDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// dataStoreSize limits the storage used by the data store. If exceeded, the
|
|
||||||
// data store will start garbage collection old files until enough storage is
|
|
||||||
// available again.
|
|
||||||
var dataStoreSize int64
|
|
||||||
dataStoreSize = 1024 * 1024 * 1024
|
|
||||||
if configStoreSize := os.Getenv("TUSD_DATA_STORE_MAXSIZE"); configStoreSize != "" {
|
|
||||||
parsed, err := strconv.ParseInt(configStoreSize, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.New("Invalid data store max size configured"))
|
|
||||||
}
|
|
||||||
dataStoreSize = parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Print("Datastore directory: ", dataDir)
|
|
||||||
log.Print("Datastore max size: ", dataStoreSize)
|
|
||||||
|
|
||||||
if err := os.MkdirAll(dataDir, 0777); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
dataStore = NewDataStore(dataDir, dataStoreSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveHttp() error {
|
|
||||||
http.HandleFunc("/", route)
|
|
||||||
|
|
||||||
addr := ":1080"
|
|
||||||
if port := os.Getenv("TUSD_PORT"); port != "" {
|
|
||||||
addr = ":" + port
|
|
||||||
}
|
|
||||||
log.Printf("serving clients at %s", addr)
|
|
||||||
|
|
||||||
return http.ListenAndServe(addr, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func route(w http.ResponseWriter, r *http.Request) {
|
|
||||||
start := time.Now()
|
|
||||||
log.Printf("request: %s %s", r.Method, r.URL.RequestURI())
|
|
||||||
|
|
||||||
w.Header().Set("Server", "tusd")
|
|
||||||
|
|
||||||
// Allow CORS for almost everything. This needs to be revisted / limited to
|
|
||||||
// routes and methods that need it.
|
|
||||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Add("Access-Control-Allow-Methods", "HEAD,GET,PUT,POST,DELETE")
|
|
||||||
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Content-Range, Content-Disposition")
|
|
||||||
w.Header().Add("Access-Control-Expose-Headers", "Location, Range, Content-Disposition")
|
|
||||||
|
|
||||||
if r.Method == "OPTIONS" {
|
|
||||||
reply(w, http.StatusOK, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == "POST" && filesRoute.Match([]byte(r.URL.Path)) {
|
|
||||||
postFiles(w, r)
|
|
||||||
} else if match := fileRoute.FindStringSubmatch(r.URL.Path); match != nil {
|
|
||||||
id := match[1]
|
|
||||||
switch r.Method {
|
|
||||||
case "HEAD":
|
|
||||||
headFile(w, r, id)
|
|
||||||
case "GET":
|
|
||||||
getFile(w, r, id)
|
|
||||||
case "PUT":
|
|
||||||
putFile(w, r, id)
|
|
||||||
default:
|
|
||||||
reply(w, http.StatusMethodNotAllowed, "Invalid http method")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reply(w, http.StatusNotFound, "No matching route")
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := time.Since(start)
|
|
||||||
log.Printf("finished: %s %s (took %s)", r.Method, r.URL.RequestURI(), duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func reply(w http.ResponseWriter, code int, message string) {
|
|
||||||
w.WriteHeader(code)
|
|
||||||
fmt.Fprintf(w, "%d - %s: %s\n", code, http.StatusText(code), message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func postFiles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
contentRange, err := parseContentRange(r.Header.Get("Content-Range"))
|
|
||||||
if err != nil {
|
|
||||||
reply(w, http.StatusBadRequest, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentRange.Size == -1 {
|
|
||||||
reply(w, http.StatusBadRequest, "Content-Range must indicate total file size.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := r.Header.Get("Content-Type")
|
|
||||||
if contentType == "" {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
contentDisposition := r.Header.Get("Content-Disposition")
|
|
||||||
|
|
||||||
id := uid()
|
|
||||||
if err := dataStore.CreateFile(id, contentRange.Size, contentType, contentDisposition); err != nil {
|
|
||||||
reply(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentRange.End != -1 {
|
|
||||||
err := dataStore.WriteFileChunk(id, contentRange.Start, contentRange.End, r.Body)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
reply(w, http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
reply(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Location", "http://"+r.Host+"/files/"+id)
|
|
||||||
setFileHeaders(w, id)
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func headFile(w http.ResponseWriter, r *http.Request, fileId string) {
|
|
||||||
// Work around a bug in Go that would cause HEAD responses to hang. Should be
|
|
||||||
// fixed in future release, see:
|
|
||||||
// http://code.google.com/p/go/issues/detail?id=4126
|
|
||||||
w.Header().Set("Content-Length", "0")
|
|
||||||
setFileHeaders(w, fileId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFile(w http.ResponseWriter, r *http.Request, fileId string) {
|
|
||||||
meta, err := dataStore.GetFileMeta(fileId)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
reply(w, http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
reply(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dataStore.ReadFile(fileId)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
reply(w, http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
reply(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer data.Close()
|
|
||||||
|
|
||||||
setFileHeaders(w, fileId)
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10))
|
|
||||||
|
|
||||||
if _, err := io.CopyN(w, data, meta.Size); err != nil {
|
|
||||||
log.Printf("getFile: CopyN of fileId %s failed with: %s. Is the upload complete yet?", fileId, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func putFile(w http.ResponseWriter, r *http.Request, fileId string) {
|
|
||||||
var start int64 = 0
|
|
||||||
var end int64 = 0
|
|
||||||
|
|
||||||
contentRange, err := parseContentRange(r.Header.Get("Content-Range"))
|
|
||||||
if err != nil {
|
|
||||||
contentLength := r.Header.Get("Content-Length")
|
|
||||||
end, err = strconv.ParseInt(contentLength, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
reply(w, http.StatusBadRequest, "Invalid content length provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// we are zero-indexed
|
|
||||||
end = end - 1
|
|
||||||
|
|
||||||
// @TODO: Make sure contentLength matches the content length of the initial
|
|
||||||
// POST request
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// @TODO: Make sure contentRange.Size matches file size
|
|
||||||
|
|
||||||
start = contentRange.Start
|
|
||||||
end = contentRange.End
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: Check that file exists
|
|
||||||
|
|
||||||
err = dataStore.WriteFileChunk(fileId, start, end, r.Body)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
reply(w, http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
reply(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setFileHeaders(w, fileId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setFileHeaders(w http.ResponseWriter, fileId string) {
|
|
||||||
meta, err := dataStore.GetFileMeta(fileId)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
reply(w, http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
reply(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rangeHeader := ""
|
|
||||||
for i, chunk := range meta.Chunks {
|
|
||||||
rangeHeader += fmt.Sprintf("%d-%d", chunk.Start, chunk.End)
|
|
||||||
if i+1 < len(meta.Chunks) {
|
|
||||||
rangeHeader += ","
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rangeHeader != "" {
|
|
||||||
w.Header().Set("Range", "bytes="+rangeHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", meta.ContentType)
|
|
||||||
w.Header().Set("Content-Disposition", meta.ContentDisposition)
|
|
||||||
}
|
|
Loading…
Reference in New Issue