diff --git a/scripts/demo-alphabet.sh b/scripts/demo-alphabet.sh index ccb129f..cbbdbd8 100755 --- a/scripts/demo-alphabet.sh +++ b/scripts/demo-alphabet.sh @@ -67,8 +67,3 @@ echo -ne "GET '${SERVICE}${location}' \t\t" has_content=$(curl -s ${SERVICE}${location}) echo "<-- ${has_content}" -# get 404 with GET -echo -ne "GET '${SERVICE}${location}a' \t\t" -has_content=$(curl -s ${SERVICE}${location}) -echo "<-- ${has_content}" - diff --git a/src/cmd/tusd/http.go b/src/cmd/tusd/http.go index 0717f10..5e08373 100644 --- a/src/cmd/tusd/http.go +++ b/src/cmd/tusd/http.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io" "log" @@ -11,7 +12,11 @@ import ( "strconv" ) +// fileRoute matches /files/. 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 @@ -22,16 +27,39 @@ func init() { } 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) + 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) @@ -41,9 +69,13 @@ func route(w http.ResponseWriter, r *http.Request) { 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") + w.Header().Add("Access-Control-Expose-Headers", "Location, Range, Content-Disposition") if r.Method == "OPTIONS" { reply(w, http.StatusOK, "") @@ -59,8 +91,6 @@ func route(w http.ResponseWriter, r *http.Request) { headFile(w, r, id) case "GET": getFile(w, r, id) - case "POST": - putFile(w, r, id) case "PUT": putFile(w, r, id) default: @@ -79,13 +109,11 @@ func reply(w http.ResponseWriter, code int, message string) { func postFiles(w http.ResponseWriter, r *http.Request) { contentRange, err := parseContentRange(r.Header.Get("Content-Range")) if err != nil { - log.Print("FOO") reply(w, http.StatusBadRequest, err.Error()) return } if contentRange.Size == -1 { - log.Print("FOO2") reply(w, http.StatusBadRequest, "Content-Range must indicate total file size.") return } @@ -95,47 +123,55 @@ func postFiles(w http.ResponseWriter, r *http.Request) { contentType = "application/octet-stream" } + contentDisposition := r.Header.Get("Content-Disposition") + id := uid() - if err := dataStore.CreateFile(id, contentRange.Size, contentType); err != nil { + 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 { + if err := dataStore.WriteFileChunk(id, contentRange.Start, contentRange.End, r.Body); err != nil { + // @TODO: Could be a 404 as well reply(w, http.StatusInternalServerError, err.Error()) return } } w.Header().Set("Location", "/files/"+id) - setFileRangeHeader(w, id) + setFileHeaders(w, id) w.WriteHeader(http.StatusCreated) } func headFile(w http.ResponseWriter, r *http.Request, fileId string) { - setFileRangeHeader(w, fileId) + // 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) { - data, size, err := dataStore.ReadFile(fileId) - if os.IsNotExist(err) { - reply(w, http.StatusNotFound, err.Error()) + meta, err := dataStore.GetFileMeta(fileId) + if err != nil { + // @TODO: Could be a 404 as well + reply(w, http.StatusInternalServerError, err.Error()) return - } else if err != nil { + } + + data, err := dataStore.ReadFile(fileId) + if err != nil { + // @TODO: Could be a 404 as well reply(w, http.StatusInternalServerError, err.Error()) return } defer data.Close() - setFileRangeHeader(w, fileId) - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + setFileHeaders(w, fileId) + w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10)) - if _, err := io.CopyN(w, data, size); err != nil { + if _, err := io.CopyN(w, data, meta.Size); err != nil { log.Printf("getFile: CopyN failed with: %s", err.Error()) return } @@ -168,7 +204,17 @@ func putFile(w http.ResponseWriter, r *http.Request, fileId string) { // @TODO: Check that file exists - err = dataStore.WriteFileChunk(fileId, start, end, r.Body) + if err := dataStore.WriteFileChunk(fileId, start, end, r.Body); err != nil { + // @TODO: Could be a 404 as well + 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 @@ -177,20 +223,10 @@ func putFile(w http.ResponseWriter, r *http.Request, fileId string) { return } - setFileRangeHeader(w, fileId) -} - -func setFileRangeHeader(w http.ResponseWriter, fileId string) { - chunks, err := dataStore.GetFileChunks(fileId) - if err != nil { - reply(w, http.StatusInternalServerError, err.Error()) - return - } - rangeHeader := "" - for i, chunk := range chunks { + for i, chunk := range meta.Chunks { rangeHeader += fmt.Sprintf("%d-%d", chunk.Start, chunk.End) - if i+1 < len(chunks) { + if i+1 < len(meta.Chunks) { rangeHeader += "," } } @@ -198,4 +234,7 @@ func setFileRangeHeader(w http.ResponseWriter, fileId string) { if rangeHeader != "" { w.Header().Set("Range", "bytes="+rangeHeader) } + + w.Header().Set("Content-Type", meta.ContentType) + w.Header().Set("Content-Disposition", meta.ContentDisposition) }