github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/lib/http/serve/serve.go (about)

     1  // Package serve deals with serving objects over HTTP
     2  package serve
     3  
     4  import (
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"path"
     9  	"strconv"
    10  
    11  	"github.com/rclone/rclone/fs"
    12  	"github.com/rclone/rclone/fs/accounting"
    13  )
    14  
    15  // Object serves an fs.Object via HEAD or GET
    16  func Object(w http.ResponseWriter, r *http.Request, o fs.Object) {
    17  	if r.Method != "HEAD" && r.Method != "GET" {
    18  		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
    19  		return
    20  	}
    21  
    22  	// Show that we accept ranges
    23  	w.Header().Set("Accept-Ranges", "bytes")
    24  
    25  	// Set content length since we know how long the object is
    26  	if o.Size() >= 0 {
    27  		w.Header().Set("Content-Length", strconv.FormatInt(o.Size(), 10))
    28  	}
    29  
    30  	// Set content type
    31  	mimeType := fs.MimeType(r.Context(), o)
    32  	if mimeType == "application/octet-stream" && path.Ext(o.Remote()) == "" {
    33  		// Leave header blank so http server guesses
    34  	} else {
    35  		w.Header().Set("Content-Type", mimeType)
    36  	}
    37  
    38  	// Set last modified
    39  	modTime := o.ModTime(r.Context())
    40  	w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
    41  
    42  	if r.Method == "HEAD" {
    43  		return
    44  	}
    45  
    46  	// Decode Range request if present
    47  	code := http.StatusOK
    48  	size := o.Size()
    49  	var options []fs.OpenOption
    50  	if rangeRequest := r.Header.Get("Range"); rangeRequest != "" {
    51  		//fs.Debugf(nil, "Range: request %q", rangeRequest)
    52  		option, err := fs.ParseRangeOption(rangeRequest)
    53  		if err != nil {
    54  			fs.Debugf(o, "Get request parse range request error: %v", err)
    55  			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
    56  			return
    57  		}
    58  		options = append(options, option)
    59  		offset, limit := option.Decode(o.Size())
    60  		end := o.Size() // exclusive
    61  		if limit >= 0 {
    62  			end = offset + limit
    63  		}
    64  		if end > o.Size() {
    65  			end = o.Size()
    66  		}
    67  		size = end - offset
    68  		// fs.Debugf(nil, "Range: offset=%d, limit=%d, end=%d, size=%d (object size %d)", offset, limit, end, size, o.Size())
    69  		// Content-Range: bytes 0-1023/146515
    70  		w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, end-1, o.Size()))
    71  		// fs.Debugf(nil, "Range: Content-Range: %q", w.Header().Get("Content-Range"))
    72  		code = http.StatusPartialContent
    73  	}
    74  	w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
    75  
    76  	file, err := o.Open(r.Context(), options...)
    77  	if err != nil {
    78  		fs.Debugf(o, "Get request open error: %v", err)
    79  		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
    80  		return
    81  	}
    82  	tr := accounting.Stats(r.Context()).NewTransfer(o, nil)
    83  	defer func() {
    84  		tr.Done(r.Context(), err)
    85  	}()
    86  	in := tr.Account(r.Context(), file) // account the transfer (no buffering)
    87  
    88  	w.WriteHeader(code)
    89  
    90  	n, err := io.Copy(w, in)
    91  	if err != nil {
    92  		fs.Errorf(o, "Didn't finish writing GET request (wrote %d/%d bytes): %v", n, size, err)
    93  		return
    94  	}
    95  }