2018-10-27 18:44:07 +02:00
|
|
|
// Package serve deals with serving objects over HTTP
|
|
|
|
package serve
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2018-10-28 15:31:24 +01:00
|
|
|
"path"
|
2018-10-27 18:44:07 +02:00
|
|
|
"strconv"
|
|
|
|
|
2019-07-28 19:47:38 +02:00
|
|
|
"github.com/rclone/rclone/fs"
|
|
|
|
"github.com/rclone/rclone/fs/accounting"
|
2018-10-27 18:44:07 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Object serves an fs.Object via HEAD or GET
|
|
|
|
func Object(w http.ResponseWriter, r *http.Request, o fs.Object) {
|
|
|
|
if r.Method != "HEAD" && r.Method != "GET" {
|
|
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
2018-10-28 15:08:20 +01:00
|
|
|
return
|
2018-10-27 18:44:07 +02:00
|
|
|
}
|
|
|
|
|
2018-10-28 15:08:20 +01:00
|
|
|
// Show that we accept ranges
|
|
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
|
|
|
2018-10-27 18:44:07 +02:00
|
|
|
// Set content length since we know how long the object is
|
|
|
|
if o.Size() >= 0 {
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(o.Size(), 10))
|
|
|
|
}
|
|
|
|
|
2018-10-28 15:31:24 +01:00
|
|
|
// Set content type
|
2019-06-17 10:34:30 +02:00
|
|
|
mimeType := fs.MimeType(r.Context(), o)
|
2018-10-28 15:31:24 +01:00
|
|
|
if mimeType == "application/octet-stream" && path.Ext(o.Remote()) == "" {
|
|
|
|
// Leave header blank so http server guesses
|
|
|
|
} else {
|
|
|
|
w.Header().Set("Content-Type", mimeType)
|
|
|
|
}
|
|
|
|
|
2023-09-22 20:11:18 +02:00
|
|
|
// Set last modified
|
|
|
|
modTime := o.ModTime(r.Context())
|
|
|
|
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
|
|
|
|
2018-10-27 18:44:07 +02:00
|
|
|
if r.Method == "HEAD" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode Range request if present
|
|
|
|
code := http.StatusOK
|
|
|
|
size := o.Size()
|
|
|
|
var options []fs.OpenOption
|
|
|
|
if rangeRequest := r.Header.Get("Range"); rangeRequest != "" {
|
|
|
|
//fs.Debugf(nil, "Range: request %q", rangeRequest)
|
|
|
|
option, err := fs.ParseRangeOption(rangeRequest)
|
|
|
|
if err != nil {
|
|
|
|
fs.Debugf(o, "Get request parse range request error: %v", err)
|
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
options = append(options, option)
|
|
|
|
offset, limit := option.Decode(o.Size())
|
|
|
|
end := o.Size() // exclusive
|
|
|
|
if limit >= 0 {
|
|
|
|
end = offset + limit
|
|
|
|
}
|
|
|
|
if end > o.Size() {
|
|
|
|
end = o.Size()
|
|
|
|
}
|
|
|
|
size = end - offset
|
|
|
|
// fs.Debugf(nil, "Range: offset=%d, limit=%d, end=%d, size=%d (object size %d)", offset, limit, end, size, o.Size())
|
|
|
|
// Content-Range: bytes 0-1023/146515
|
|
|
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, end-1, o.Size()))
|
|
|
|
// fs.Debugf(nil, "Range: Content-Range: %q", w.Header().Get("Content-Range"))
|
|
|
|
code = http.StatusPartialContent
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
|
|
|
|
2019-06-17 10:34:30 +02:00
|
|
|
file, err := o.Open(r.Context(), options...)
|
2018-10-27 18:44:07 +02:00
|
|
|
if err != nil {
|
|
|
|
fs.Debugf(o, "Get request open error: %v", err)
|
|
|
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
2024-01-18 17:44:13 +01:00
|
|
|
tr := accounting.Stats(r.Context()).NewTransfer(o, nil)
|
2018-10-27 18:44:07 +02:00
|
|
|
defer func() {
|
2020-11-05 17:59:59 +01:00
|
|
|
tr.Done(r.Context(), err)
|
2018-10-27 18:44:07 +02:00
|
|
|
}()
|
2020-06-04 16:09:03 +02:00
|
|
|
in := tr.Account(r.Context(), file) // account the transfer (no buffering)
|
2018-10-27 18:44:07 +02:00
|
|
|
|
|
|
|
w.WriteHeader(code)
|
|
|
|
|
|
|
|
n, err := io.Copy(w, in)
|
|
|
|
if err != nil {
|
|
|
|
fs.Errorf(o, "Didn't finish writing GET request (wrote %d/%d bytes): %v", n, size, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|