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