2015-02-11 01:25:40 +00:00
|
|
|
package handlers
|
2014-11-11 02:57:38 +00:00
|
|
|
|
|
|
|
import (
|
2014-11-21 03:57:01 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2014-11-11 02:57:38 +00:00
|
|
|
"net/http"
|
2015-01-05 07:59:29 +00:00
|
|
|
"net/url"
|
2015-01-08 23:04:00 +00:00
|
|
|
"os"
|
2014-11-11 02:57:38 +00:00
|
|
|
|
2015-02-12 00:49:49 +00:00
|
|
|
"github.com/docker/distribution"
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu "github.com/docker/distribution/context"
|
2014-12-24 00:01:38 +00:00
|
|
|
"github.com/docker/distribution/digest"
|
2015-02-11 02:18:45 +00:00
|
|
|
"github.com/docker/distribution/registry/api/v2"
|
2014-11-11 02:57:38 +00:00
|
|
|
"github.com/gorilla/handlers"
|
|
|
|
)
|
|
|
|
|
|
|
|
// layerUploadDispatcher constructs and returns the layer upload handler for
|
|
|
|
// the given request context.
|
|
|
|
func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
|
2014-11-21 03:57:01 +00:00
|
|
|
luh := &layerUploadHandler{
|
2014-11-11 02:57:38 +00:00
|
|
|
Context: ctx,
|
2015-02-07 00:19:19 +00:00
|
|
|
UUID: getUploadUUID(ctx),
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
2014-11-21 03:57:01 +00:00
|
|
|
handler := http.Handler(handlers.MethodHandler{
|
2015-05-04 15:56:37 +00:00
|
|
|
"POST": http.HandlerFunc(luh.StartLayerUpload),
|
|
|
|
"GET": http.HandlerFunc(luh.GetUploadStatus),
|
|
|
|
"HEAD": http.HandlerFunc(luh.GetUploadStatus),
|
|
|
|
"PATCH": http.HandlerFunc(luh.PatchLayerData),
|
2015-01-30 05:26:35 +00:00
|
|
|
"PUT": http.HandlerFunc(luh.PutLayerUploadComplete),
|
2014-11-21 03:57:01 +00:00
|
|
|
"DELETE": http.HandlerFunc(luh.CancelLayerUpload),
|
|
|
|
})
|
|
|
|
|
|
|
|
if luh.UUID != "" {
|
2015-01-08 23:04:00 +00:00
|
|
|
state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
|
2015-01-05 07:59:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err)
|
2015-01-08 23:04:00 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
luh.State = state
|
|
|
|
|
2015-01-17 02:32:27 +00:00
|
|
|
if state.Name != ctx.Repository.Name() {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, luh.Repository.Name())
|
2015-01-17 02:32:27 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2015-01-08 23:04:00 +00:00
|
|
|
if state.UUID != luh.UUID {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, luh.UUID)
|
2015-01-08 23:04:00 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
|
2015-01-05 07:59:29 +00:00
|
|
|
})
|
|
|
|
}
|
2014-11-21 03:57:01 +00:00
|
|
|
|
2015-01-17 02:32:27 +00:00
|
|
|
layers := ctx.Repository.Layers()
|
|
|
|
upload, err := layers.Resume(luh.UUID)
|
2015-01-10 00:09:45 +00:00
|
|
|
if err != nil {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err)
|
2015-02-12 00:49:49 +00:00
|
|
|
if err == distribution.ErrLayerUploadUnknown {
|
2015-01-10 00:09:45 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-11-21 03:57:01 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2015-01-10 00:09:45 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2014-11-21 03:57:01 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
luh.Upload = upload
|
2015-01-08 23:04:00 +00:00
|
|
|
|
|
|
|
if state.Offset > 0 {
|
|
|
|
// Seek the layer upload to the correct spot if it's non-zero.
|
|
|
|
// These error conditions should be rare and demonstrate really
|
|
|
|
// problems. We basically cancel the upload and tell the client to
|
|
|
|
// start over.
|
|
|
|
if nn, err := upload.Seek(luh.State.Offset, os.SEEK_SET); err != nil {
|
2015-01-10 00:09:45 +00:00
|
|
|
defer upload.Close()
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(ctx).Infof("error seeking layer upload: %v", err)
|
2015-01-08 23:04:00 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
|
|
|
|
upload.Cancel()
|
|
|
|
})
|
|
|
|
} else if nn != luh.State.Offset {
|
2015-01-10 00:09:45 +00:00
|
|
|
defer upload.Close()
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(ctx).Infof("seek to wrong offest: %d != %d", nn, luh.State.Offset)
|
2015-01-08 23:04:00 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
|
|
|
|
upload.Cancel()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-21 03:57:01 +00:00
|
|
|
handler = closeResources(handler, luh.Upload)
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
2014-11-21 03:57:01 +00:00
|
|
|
return handler
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// layerUploadHandler handles the http layer upload process.
|
|
|
|
type layerUploadHandler struct {
|
|
|
|
*Context
|
|
|
|
|
|
|
|
// UUID identifies the upload instance for the current request.
|
|
|
|
UUID string
|
2014-11-21 03:57:01 +00:00
|
|
|
|
2015-02-12 00:49:49 +00:00
|
|
|
Upload distribution.LayerUpload
|
2015-01-08 23:04:00 +00:00
|
|
|
|
|
|
|
State layerUploadState
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// StartLayerUpload begins the layer upload process and allocates a server-
|
|
|
|
// side upload session.
|
|
|
|
func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.Request) {
|
2015-01-17 02:32:27 +00:00
|
|
|
layers := luh.Repository.Layers()
|
|
|
|
upload, err := layers.Upload()
|
2014-11-21 03:57:01 +00:00
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2014-11-21 03:57:01 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
luh.Upload = upload
|
|
|
|
defer luh.Upload.Close()
|
2014-11-11 02:57:38 +00:00
|
|
|
|
2015-05-04 15:56:37 +00:00
|
|
|
if err := luh.layerUploadResponse(w, r, true); err != nil {
|
2014-11-21 03:57:01 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2014-11-21 03:57:01 +00:00
|
|
|
return
|
|
|
|
}
|
2015-02-27 00:43:47 +00:00
|
|
|
|
|
|
|
w.Header().Set("Docker-Upload-UUID", luh.Upload.UUID())
|
2014-11-21 03:57:01 +00:00
|
|
|
w.WriteHeader(http.StatusAccepted)
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetUploadStatus returns the status of a given upload, identified by uuid.
|
|
|
|
func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
|
2014-11-21 03:57:01 +00:00
|
|
|
if luh.Upload == nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
|
2015-01-10 00:09:45 +00:00
|
|
|
return
|
2014-11-21 03:57:01 +00:00
|
|
|
}
|
|
|
|
|
2015-05-04 15:56:37 +00:00
|
|
|
// TODO(dmcgowan): Set last argument to false in layerUploadResponse when
|
|
|
|
// resumable upload is supported. This will enable returning a non-zero
|
|
|
|
// range for clients to begin uploading at an offset.
|
|
|
|
if err := luh.layerUploadResponse(w, r, true); err != nil {
|
2014-11-21 03:57:01 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2014-11-21 03:57:01 +00:00
|
|
|
return
|
|
|
|
}
|
2014-11-11 02:57:38 +00:00
|
|
|
|
2015-02-27 00:43:47 +00:00
|
|
|
w.Header().Set("Docker-Upload-UUID", luh.UUID)
|
2014-11-21 03:57:01 +00:00
|
|
|
w.WriteHeader(http.StatusNoContent)
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
2015-05-04 15:56:37 +00:00
|
|
|
// PatchLayerData writes data to an upload.
|
|
|
|
func (luh *layerUploadHandler) PatchLayerData(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if luh.Upload == nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ct := r.Header.Get("Content-Type")
|
|
|
|
if ct != "" && ct != "application/octet-stream" {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
// TODO(dmcgowan): encode error
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(dmcgowan): support Content-Range header to seek and write range
|
|
|
|
|
|
|
|
// Copy the data
|
|
|
|
if _, err := io.Copy(luh.Upload, r.Body); err != nil {
|
|
|
|
ctxu.GetLogger(luh).Errorf("unknown error copying into upload: %v", err)
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := luh.layerUploadResponse(w, r, false); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PutLayerUploadComplete takes the final request of a layer upload. The
|
|
|
|
// request may include all the layer data or no layer data. Any data
|
|
|
|
// provided is received and verified. If successful, the layer is linked
|
|
|
|
// into the blob store and 201 Created is returned with the canonical
|
|
|
|
// url of the layer.
|
2015-01-30 05:26:35 +00:00
|
|
|
func (luh *layerUploadHandler) PutLayerUploadComplete(w http.ResponseWriter, r *http.Request) {
|
2014-11-21 03:57:01 +00:00
|
|
|
if luh.Upload == nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
|
2015-01-30 05:26:35 +00:00
|
|
|
return
|
2014-11-21 03:57:01 +00:00
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
|
|
|
|
|
|
|
|
if dgstStr == "" {
|
|
|
|
// no digest? return error, but allow retry.
|
2015-02-20 01:14:25 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2015-01-30 05:26:35 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeDigestInvalid, "digest missing")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dgst, err := digest.ParseDigest(dgstStr)
|
|
|
|
if err != nil {
|
|
|
|
// no digest? return error, but allow retry.
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeDigestInvalid, "digest parsing failed")
|
|
|
|
return
|
|
|
|
}
|
2014-11-11 02:57:38 +00:00
|
|
|
|
2015-02-26 23:47:04 +00:00
|
|
|
// TODO(stevvooe): Consider checking the error on this copy.
|
|
|
|
// Theoretically, problems should be detected during verification but we
|
|
|
|
// may miss a root cause.
|
|
|
|
|
2015-05-04 15:56:37 +00:00
|
|
|
// Read in the data, if any.
|
2015-04-22 19:12:59 +00:00
|
|
|
if _, err := io.Copy(luh.Upload, r.Body); err != nil {
|
|
|
|
ctxu.GetLogger(luh).Errorf("unknown error copying into upload: %v", err)
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2015-04-23 20:13:13 +00:00
|
|
|
return
|
2015-04-22 19:12:59 +00:00
|
|
|
}
|
2014-11-21 03:57:01 +00:00
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
layer, err := luh.Upload.Finish(dgst)
|
|
|
|
if err != nil {
|
|
|
|
switch err := err.(type) {
|
2015-02-12 00:49:49 +00:00
|
|
|
case distribution.ErrLayerInvalidDigest:
|
2015-01-30 05:26:35 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeDigestInvalid, err)
|
|
|
|
default:
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(luh).Errorf("unknown error completing upload: %#v", err)
|
2015-01-30 05:26:35 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2014-11-21 03:57:01 +00:00
|
|
|
}
|
2015-01-30 05:26:35 +00:00
|
|
|
|
|
|
|
// Clean up the backend layer data if there was an error.
|
|
|
|
if err := luh.Upload.Cancel(); err != nil {
|
|
|
|
// If the cleanup fails, all we can do is observe and report.
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(luh).Errorf("error canceling upload after error: %v", err)
|
2015-01-30 05:26:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
2014-11-21 03:57:01 +00:00
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
// Build our canonical layer url
|
2015-02-12 01:00:42 +00:00
|
|
|
layerURL, err := luh.urlBuilder.BuildBlobURL(luh.Repository.Name(), layer.Digest())
|
2015-01-30 05:26:35 +00:00
|
|
|
if err != nil {
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
2015-01-30 05:26:35 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2014-11-21 03:57:01 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
w.Header().Set("Location", layerURL)
|
|
|
|
w.Header().Set("Content-Length", "0")
|
2015-02-26 23:47:04 +00:00
|
|
|
w.Header().Set("Docker-Content-Digest", layer.Digest().String())
|
2015-01-30 05:26:35 +00:00
|
|
|
w.WriteHeader(http.StatusCreated)
|
2014-11-11 02:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CancelLayerUpload cancels an in-progress upload of a layer.
|
|
|
|
func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) {
|
2014-11-21 03:57:01 +00:00
|
|
|
if luh.Upload == nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
2014-12-12 06:24:25 +00:00
|
|
|
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
|
2015-01-30 05:26:35 +00:00
|
|
|
return
|
2014-11-21 03:57:01 +00:00
|
|
|
}
|
|
|
|
|
2015-02-27 00:43:47 +00:00
|
|
|
w.Header().Set("Docker-Upload-UUID", luh.UUID)
|
2015-01-30 05:26:35 +00:00
|
|
|
if err := luh.Upload.Cancel(); err != nil {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(luh).Errorf("error encountered canceling upload: %v", err)
|
2015-01-30 05:26:35 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
luh.Errors.PushErr(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
2014-11-21 03:57:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// layerUploadResponse provides a standard request for uploading layers and
|
|
|
|
// chunk responses. This sets the correct headers but the response status is
|
2015-05-04 15:56:37 +00:00
|
|
|
// left to the caller. The fresh argument is used to ensure that new layer
|
|
|
|
// uploads always start at a 0 offset. This allows disabling resumable push
|
|
|
|
// by always returning a 0 offset on check status.
|
|
|
|
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error {
|
|
|
|
|
|
|
|
var offset int64
|
|
|
|
if !fresh {
|
|
|
|
var err error
|
|
|
|
offset, err = luh.Upload.Seek(0, os.SEEK_CUR)
|
|
|
|
if err != nil {
|
|
|
|
ctxu.GetLogger(luh).Errorf("unable get current offset of layer upload: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
2015-01-08 23:04:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(stevvooe): Need a better way to manage the upload state automatically.
|
2015-01-17 02:32:27 +00:00
|
|
|
luh.State.Name = luh.Repository.Name()
|
2015-01-08 23:04:00 +00:00
|
|
|
luh.State.UUID = luh.Upload.UUID()
|
|
|
|
luh.State.Offset = offset
|
|
|
|
luh.State.StartedAt = luh.Upload.StartedAt()
|
|
|
|
|
|
|
|
token, err := hmacKey(luh.Config.HTTP.Secret).packUploadState(luh.State)
|
2015-01-05 07:59:29 +00:00
|
|
|
if err != nil {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(luh).Infof("error building upload state token: %s", err)
|
2015-01-05 07:59:29 +00:00
|
|
|
return err
|
|
|
|
}
|
2015-01-08 23:04:00 +00:00
|
|
|
|
|
|
|
uploadURL, err := luh.urlBuilder.BuildBlobUploadChunkURL(
|
2015-02-12 01:00:42 +00:00
|
|
|
luh.Repository.Name(), luh.Upload.UUID(),
|
2015-01-08 23:04:00 +00:00
|
|
|
url.Values{
|
|
|
|
"_state": []string{token},
|
|
|
|
})
|
2014-11-21 03:57:01 +00:00
|
|
|
if err != nil {
|
2015-02-07 00:19:19 +00:00
|
|
|
ctxu.GetLogger(luh).Infof("error building upload url: %s", err)
|
2014-11-21 03:57:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-05-04 15:56:37 +00:00
|
|
|
endRange := offset
|
|
|
|
if endRange > 0 {
|
|
|
|
endRange = endRange - 1
|
|
|
|
}
|
|
|
|
|
2015-02-27 00:43:47 +00:00
|
|
|
w.Header().Set("Docker-Upload-UUID", luh.UUID)
|
2014-11-21 03:57:01 +00:00
|
|
|
w.Header().Set("Location", uploadURL)
|
|
|
|
w.Header().Set("Content-Length", "0")
|
2015-05-04 15:56:37 +00:00
|
|
|
w.Header().Set("Range", fmt.Sprintf("0-%d", endRange))
|
2014-11-21 03:57:01 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|