mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-27 15:30:45 +01:00
* fix 12074 * volume: shuffle a copy of cached replica locations for proxy reads LookupVolumeId serves cached location slices shared between lookups; shuffling in place races with concurrent proxied reads of the same volume. * seaweed-volume: forward inbound method and surface lookup failure in proxy reads Mirror the Go handler: proxy_request used to always send GET, turning a client HEAD into a replica download, and a failed master lookup returned 404 like missing data. Forward the inbound method and answer 500 for lookup failures in proxy mode. * volume: keep master lookup error detail out of the proxy error response * volume: bound the proxied-request wait in the method-preservation test * seaweed-volume: answer 404 for missing volumes in proxy-mode lookups Mirror Go's LookupVolumeId: a per-volume error in the master's lookup reply (e.g. a missing volume) means "no locations" and answers 404 downstream, not a lookup failure that proxy mode answers 500. * volume: guard nil lookup result in proxy path A master response that omits the requested vid leaves LookupVolumeId returning a nil *LookupResult with a nil error; dereferencing it panics the read handler. * seaweed-volume: answered lookup errors are missing data, not failures A master reply carrying a per-volume error with HTTP 404 (volume not found) now yields empty locations like Go's LookupResult, so proxy mode answers 404. Other failure statuses such as 503 during master warmup remain lookup failures that proxy mode answers 500. * seaweed-volume: drop hop-by-hop headers from replica proxy reads Go's inbound request drops Host, Content-Length and Transfer-Encoding into dedicated fields, so its forwarded header set never carries them. hyper keeps them; forwarding a client's Content-Length with no body stalls the replica's read, and forwarding its Host misnames the target. * volume: bound the proxy master lookup to the request context * volume: drop hop-by-hop and Connection-nominated headers from proxy reads * seaweed-volume: drop Connection-nominated headers from proxy reads * volume: skip the proxy lookup error when the inbound request is gone * volume: drop proxy-specific headers from replica proxy reads * seaweed-volume: drop proxy-specific headers from replica proxy reads Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: drop hop-by-hop headers from replica proxy responses * seaweed-volume: drop hop-by-hop headers from replica proxy responses * seaweed-volume: a failed lookup status without an error body is still a failure * volume: do not re-proxy an already-proxied read for a missing volume A proxied request that reaches a server which still lacks the volume was looked up and forwarded again, so a stale location answer could bounce the read between two servers until the client gave up. The Rust volume server's proxy_missing_volume already returns 404 for proxied=true; mirror it here. * seaweed-volume: replace stale proxied params in the forwarded query Go builds the proxied URL with query.Set(reqIsProxied), which replaces every proxied param the client sent. Appending after a client-supplied proxied=false left the replica's first-value lookup reading the stale marker, defeating the loop guard. --------- Co-authored-by: chrislusf <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
557 lines
17 KiB
Go
557 lines
17 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand/v2"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/mem"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/images"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const reqIsProxied = "proxied"
|
|
|
|
func NotFound(w http.ResponseWriter) {
|
|
stats.VolumeServerFileReadFailures.Inc()
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorGetNotFound).Inc()
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
|
|
func InternalError(w http.ResponseWriter, r *http.Request, err error) {
|
|
stats.VolumeServerFileReadFailures.Inc()
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorGetInternal).Inc()
|
|
if err == nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if r.Method == http.MethodHead {
|
|
glog.V(0).Infof("response method:%s URL:%s with httpStatus:%d and error:%v",
|
|
r.Method, redactedURL(r), http.StatusInternalServerError, err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
}
|
|
|
|
// hopByHopHeaders names the fields that describe a single hop and must not
|
|
// be relayed: the fixed hop-by-hop set plus every field the header's own
|
|
// Connection values nominate (RFC 9110 7.6.1).
|
|
func hopByHopHeaders(h http.Header, extra ...string) map[string]bool {
|
|
drop := map[string]bool{
|
|
"Connection": true, "Keep-Alive": true, "Te": true, "Trailer": true,
|
|
"Upgrade": true, "Proxy-Connection": true, "Proxy-Authenticate": true, "Proxy-Authorization": true,
|
|
}
|
|
for _, v := range h.Values("Connection") {
|
|
for _, token := range strings.Split(v, ",") {
|
|
if name := http.CanonicalHeaderKey(strings.TrimSpace(token)); name != "" {
|
|
drop[name] = true
|
|
}
|
|
}
|
|
}
|
|
for _, name := range extra {
|
|
drop[http.CanonicalHeaderKey(name)] = true
|
|
}
|
|
return drop
|
|
}
|
|
|
|
func (vs *VolumeServer) proxyReqToTargetServer(w http.ResponseWriter, r *http.Request) {
|
|
vid, fid, _, _, _ := parseURLPath(r.URL.Path)
|
|
volumeId, err := needle.NewVolumeId(vid)
|
|
if err != nil {
|
|
glog.V(2).Infof("parsing vid %s: %v", r.URL.Path, err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
lookupResult, err := operation.LookupVolumeIdWithContext(r.Context(), vs.GetMaster, vs.grpcDialOption, volumeId.String())
|
|
if err != nil && vs.ReadMode == "proxy" {
|
|
if r.Context().Err() != nil {
|
|
return
|
|
}
|
|
glog.V(0).InfofCtx(r.Context(), "lookup error: %v %s", err, r.URL.Path)
|
|
InternalError(w, r, nil)
|
|
return
|
|
}
|
|
if err != nil || lookupResult == nil || len(lookupResult.Locations) <= 0 {
|
|
glog.V(0).Infoln("lookup error:", err, r.URL.Path)
|
|
stats.VolumeServerFileReadInvalidNeedles.Inc()
|
|
NotFound(w)
|
|
return
|
|
}
|
|
if len(lookupResult.Locations) >= 2 {
|
|
// cached locations are shared between lookups, so shuffle a copy
|
|
locations := slices.Clone(lookupResult.Locations)
|
|
rand.Shuffle(len(locations), func(i, j int) {
|
|
locations[i], locations[j] = locations[j], locations[i]
|
|
})
|
|
lookupResult.Locations = locations
|
|
}
|
|
var targetUrl *url.URL
|
|
location := fmt.Sprintf("%s:%d", vs.store.Ip, vs.store.Port)
|
|
for _, loc := range lookupResult.Locations {
|
|
if !strings.Contains(loc.Url, location) {
|
|
rawURL, _ := util_http.NormalizeUrl(loc.Url)
|
|
targetUrl, _ = url.Parse(rawURL)
|
|
break
|
|
}
|
|
}
|
|
if targetUrl == nil {
|
|
stats.VolumeServerFileReadInvalidNeedles.Inc()
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.EmptyReadProxyLoc).Inc()
|
|
glog.Errorf("failed lookup target host is empty locations: %+v, %s", lookupResult.Locations, location)
|
|
NotFound(w)
|
|
return
|
|
}
|
|
if vs.ReadMode == "proxy" {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ReadProxyReq).Inc()
|
|
// proxy client request to target server
|
|
r.URL.Host = targetUrl.Host
|
|
r.URL.Scheme = targetUrl.Scheme
|
|
query := r.URL.Query()
|
|
query.Set(reqIsProxied, "true")
|
|
r.URL.RawQuery = query.Encode()
|
|
request, err := http.NewRequestWithContext(r.Context(), r.Method, r.URL.String(), nil)
|
|
if err != nil {
|
|
glog.V(0).Infof("failed to instance http request of url %s: %v", redactedURL(r), err)
|
|
InternalError(w, r, nil)
|
|
return
|
|
}
|
|
// the proxied request carries no body, so it must not forward this
|
|
// hop's framing either
|
|
hopByHop := hopByHopHeaders(r.Header, "Host", "Content-Length", "Transfer-Encoding")
|
|
for k, vv := range r.Header {
|
|
if hopByHop[http.CanonicalHeaderKey(k)] {
|
|
continue
|
|
}
|
|
for _, v := range vv {
|
|
request.Header.Add(k, v)
|
|
}
|
|
}
|
|
|
|
response, err := util_http.GetGlobalHttpClient().Do(request)
|
|
if err != nil {
|
|
if r.Context().Err() != nil {
|
|
return
|
|
}
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.FailedReadProxyReq).Inc()
|
|
glog.V(0).Infof("request remote url %s: %v", redactedURL(r), err)
|
|
InternalError(w, r, nil)
|
|
return
|
|
}
|
|
defer util_http.CloseResponse(response)
|
|
// proxy target response to client
|
|
respHopByHop := hopByHopHeaders(response.Header)
|
|
for k, vv := range response.Header {
|
|
if k == "Server" || respHopByHop[http.CanonicalHeaderKey(k)] {
|
|
continue
|
|
}
|
|
for _, v := range vv {
|
|
w.Header().Add(k, v)
|
|
}
|
|
}
|
|
w.WriteHeader(response.StatusCode)
|
|
buf := mem.Allocate(128 * 1024)
|
|
defer mem.Free(buf)
|
|
io.CopyBuffer(w, response.Body, buf)
|
|
return
|
|
} else {
|
|
// redirect
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ReadRedirectReq).Inc()
|
|
targetUrl.Path = fmt.Sprintf("%s/%s,%s", targetUrl.Path, vid, fid)
|
|
arg := url.Values{}
|
|
if c := r.FormValue("collection"); c != "" {
|
|
arg.Set("collection", c)
|
|
}
|
|
arg.Set(reqIsProxied, "true")
|
|
targetUrl.RawQuery = arg.Encode()
|
|
http.Redirect(w, r, targetUrl.String(), http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
}
|
|
|
|
// notModified checks If-Modified-Since only when there is no If-None-Match
|
|
// (RFC 9110 13.2.2).
|
|
func notModified(r *http.Request, etag string, lastModified uint64) bool {
|
|
if inm := r.Header.Values("If-None-Match"); len(inm) != 0 {
|
|
return etagListMatches(strings.Join(inm, ","), etag, true)
|
|
}
|
|
ims := r.Header.Get("If-Modified-Since")
|
|
if ims == "" || lastModified == 0 {
|
|
return false
|
|
}
|
|
t, err := time.Parse(http.TimeFormat, ims)
|
|
return err == nil && t.Unix() >= int64(lastModified)
|
|
}
|
|
|
|
func (vs *VolumeServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) {
|
|
n := new(needle.Needle)
|
|
vid, fid, filename, ext, _ := parseURLPath(r.URL.Path)
|
|
|
|
if !vs.maybeCheckJwtAuthorization(r, vid, fid, false) {
|
|
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
|
|
return
|
|
}
|
|
|
|
volumeId, err := needle.NewVolumeId(vid)
|
|
if err != nil {
|
|
glog.V(2).Infof("parsing vid %s: %v", r.URL.Path, err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
err = n.ParsePath(fid)
|
|
if err != nil {
|
|
glog.V(2).Infof("parsing fid %s: %v", r.URL.Path, err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// glog.V(4).Infoln("volume", volumeId, "reading", n)
|
|
hasVolume := vs.store.HasVolume(volumeId)
|
|
_, hasEcVolume := vs.store.FindEcVolume(volumeId)
|
|
if !hasVolume && !hasEcVolume {
|
|
if vs.ReadMode == "local" || r.URL.Query().Get(reqIsProxied) == "true" {
|
|
stats.VolumeServerFileReadInvalidNeedles.Inc()
|
|
glog.V(0).Infoln("volume is not local:", err, r.URL.Path)
|
|
NotFound(w)
|
|
return
|
|
}
|
|
vs.proxyReqToTargetServer(w, r)
|
|
return
|
|
}
|
|
cookie := n.Cookie
|
|
|
|
readOption := &storage.ReadOption{
|
|
ReadDeleted: r.FormValue("readDeleted") == "true",
|
|
HasSlowRead: vs.hasSlowRead,
|
|
ReadBufferSize: vs.readBufferSizeMB * 1024 * 1024,
|
|
}
|
|
|
|
var count int
|
|
var memoryCost types.Size
|
|
readOption.AttemptMetaOnly, readOption.MustMetaOnly = shouldAttemptStreamWrite(hasVolume, ext, r)
|
|
onReadSizeFn := func(size types.Size) {
|
|
memoryCost = size
|
|
atomic.AddInt64(&vs.inFlightDownloadDataSize, int64(memoryCost))
|
|
}
|
|
|
|
if hasVolume {
|
|
count, err = vs.store.ReadVolumeNeedle(volumeId, n, readOption, onReadSizeFn)
|
|
} else if hasEcVolume {
|
|
count, err = vs.store.ReadEcShardNeedle(volumeId, n, onReadSizeFn)
|
|
}
|
|
|
|
defer func() {
|
|
atomic.AddInt64(&vs.inFlightDownloadDataSize, -int64(memoryCost))
|
|
if vs.concurrentDownloadLimit != 0 {
|
|
vs.inFlightDownloadDataLimitCond.Broadcast()
|
|
}
|
|
}()
|
|
|
|
if err != nil && err != storage.ErrorDeleted && hasVolume {
|
|
glog.V(4).Infof("read needle: %v", err)
|
|
// start to fix it from other replicas, if not deleted and hasVolume and is not a replicated request
|
|
}
|
|
// glog.V(4).Infoln("read bytes", count, "error", err)
|
|
if err != nil || count < 0 {
|
|
glog.V(3).Infof("read %s isNormalVolume %v error: %v", r.URL.Path, hasVolume, err)
|
|
invalid_needle := err == storage.ErrorNotFound || errors.Is(err, erasure_coding.NotFoundError)
|
|
if invalid_needle {
|
|
stats.VolumeServerFileReadInvalidNeedles.Inc()
|
|
}
|
|
if invalid_needle || err == storage.ErrorDeleted {
|
|
NotFound(w)
|
|
} else {
|
|
InternalError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
if n.Cookie != cookie {
|
|
// A meta-only read ended the request; record its success.
|
|
if readOption.IsMetaOnly {
|
|
if v := vs.store.GetVolume(volumeId); v != nil {
|
|
v.NoteMetaOnlySuccess()
|
|
}
|
|
}
|
|
glog.V(0).Infof("request %s with cookie:%x expected:%x from %s agent %s", r.URL.Path, cookie, n.Cookie, r.RemoteAddr, r.UserAgent())
|
|
NotFound(w)
|
|
return
|
|
}
|
|
if n.LastModified != 0 {
|
|
w.Header().Set("Last-Modified", time.Unix(int64(n.LastModified), 0).UTC().Format(http.TimeFormat))
|
|
}
|
|
SetEtag(w, n.Etag())
|
|
if notModified(r, n.Etag(), n.LastModified) {
|
|
if readOption.IsMetaOnly {
|
|
if v := vs.store.GetVolume(volumeId); v != nil {
|
|
v.NoteMetaOnlySuccess()
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
|
|
if n.HasPairs() {
|
|
pairMap := make(map[string]string)
|
|
err = json.Unmarshal(n.Pairs, &pairMap)
|
|
if err != nil {
|
|
glog.V(0).Infoln("Unmarshal pairs error:", err)
|
|
}
|
|
for k, v := range pairMap {
|
|
w.Header().Set(k, v)
|
|
}
|
|
}
|
|
|
|
if vs.tryHandleChunkedFile(n, filename, ext, w, r) {
|
|
return
|
|
}
|
|
|
|
if n.NameSize > 0 && filename == "" {
|
|
filename = string(n.Name)
|
|
if ext == "" {
|
|
ext = filepath.Ext(filename)
|
|
}
|
|
}
|
|
mtype := ""
|
|
if n.MimeSize > 0 {
|
|
mt := string(n.Mime)
|
|
if !strings.HasPrefix(mt, "application/octet-stream") {
|
|
mtype = mt
|
|
}
|
|
}
|
|
|
|
if n.IsCompressed() {
|
|
_, _, _, shouldResize := shouldResizeImages(ext, r)
|
|
_, _, _, _, shouldCrop := shouldCropImages(ext, r)
|
|
if shouldResize || shouldCrop {
|
|
if n.Data, err = util.DecompressData(n.Data); err != nil {
|
|
glog.V(0).Infoln("ungzip error:", err, r.URL.Path)
|
|
}
|
|
// } else if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") && util.IsZstdContent(n.Data) {
|
|
// w.Header().Set("Content-Encoding", "zstd")
|
|
} else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && util.IsGzippedContent(n.Data) {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
} else {
|
|
if n.Data, err = util.DecompressData(n.Data); err != nil {
|
|
glog.V(0).Infoln("uncompress error:", err, r.URL.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !readOption.IsMetaOnly {
|
|
rs := conditionallyCropImages(bytes.NewReader(n.Data), ext, r)
|
|
rs = conditionallyResizeImages(rs, ext, r)
|
|
if e := writeResponseContent(filename, mtype, rs, w, r); e != nil {
|
|
glog.V(2).Infoln("response write error:", e)
|
|
}
|
|
} else {
|
|
vs.streamWriteResponseContent(filename, mtype, volumeId, n, w, r, readOption)
|
|
}
|
|
}
|
|
|
|
func shouldAttemptStreamWrite(hasLocalVolume bool, ext string, r *http.Request) (shouldAttempt bool, mustMetaOnly bool) {
|
|
if !hasLocalVolume {
|
|
return false, false
|
|
}
|
|
if len(ext) > 0 {
|
|
ext = strings.ToLower(ext)
|
|
}
|
|
_, _, _, shouldResize := shouldResizeImages(ext, r)
|
|
_, _, _, _, shouldCrop := shouldCropImages(ext, r)
|
|
if shouldResize || shouldCrop {
|
|
// A transformed image's length is not the needle's; the payload is
|
|
// needed to answer — also true for HEAD, which reports it.
|
|
return false, false
|
|
}
|
|
if r.Method == http.MethodHead {
|
|
// Attempt only, so a needle up to PagedReadLimit is still read whole:
|
|
// on a spinning disk one read is cheaper than a header read followed
|
|
// by a trailer read.
|
|
return true, false
|
|
}
|
|
return true, false
|
|
}
|
|
|
|
func (vs *VolumeServer) tryHandleChunkedFile(n *needle.Needle, fileName string, ext string, w http.ResponseWriter, r *http.Request) (processed bool) {
|
|
if !n.IsChunkedManifest() || r.URL.Query().Get("cm") == "false" {
|
|
return false
|
|
}
|
|
|
|
chunkManifest, e := operation.LoadChunkManifest(n.Data, n.IsCompressed())
|
|
if e != nil {
|
|
glog.V(0).Infof("load chunked manifest (%s) error: %v", r.URL.Path, e)
|
|
return false
|
|
}
|
|
if fileName == "" && chunkManifest.Name != "" {
|
|
fileName = chunkManifest.Name
|
|
}
|
|
|
|
if ext == "" {
|
|
ext = filepath.Ext(fileName)
|
|
}
|
|
|
|
mType := ""
|
|
if chunkManifest.Mime != "" {
|
|
mt := chunkManifest.Mime
|
|
if !strings.HasPrefix(mt, "application/octet-stream") {
|
|
mType = mt
|
|
}
|
|
}
|
|
|
|
w.Header().Set("X-File-Store", "chunked")
|
|
|
|
chunkedFileReader := operation.NewChunkedFileReader(chunkManifest.Chunks, vs.GetMaster(context.Background()), vs.grpcDialOption)
|
|
defer chunkedFileReader.Close()
|
|
|
|
rs := conditionallyCropImages(chunkedFileReader, ext, r)
|
|
rs = conditionallyResizeImages(rs, ext, r)
|
|
|
|
if e := writeResponseContent(fileName, mType, rs, w, r); e != nil {
|
|
glog.V(2).Infoln("response write error:", e)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func conditionallyResizeImages(originalDataReaderSeeker io.ReadSeeker, ext string, r *http.Request) io.ReadSeeker {
|
|
rs := originalDataReaderSeeker
|
|
if len(ext) > 0 {
|
|
ext = strings.ToLower(ext)
|
|
}
|
|
width, height, mode, shouldResize := shouldResizeImages(ext, r)
|
|
if shouldResize {
|
|
rs, _, _ = images.Resized(ext, originalDataReaderSeeker, width, height, mode)
|
|
}
|
|
return rs
|
|
}
|
|
|
|
func shouldResizeImages(ext string, r *http.Request) (width, height int, mode string, shouldResize bool) {
|
|
if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".webp" {
|
|
if r.FormValue("width") != "" {
|
|
width, _ = strconv.Atoi(r.FormValue("width"))
|
|
}
|
|
if r.FormValue("height") != "" {
|
|
height, _ = strconv.Atoi(r.FormValue("height"))
|
|
}
|
|
}
|
|
mode = r.FormValue("mode")
|
|
shouldResize = width > 0 || height > 0
|
|
return
|
|
}
|
|
|
|
func conditionallyCropImages(originalDataReaderSeeker io.ReadSeeker, ext string, r *http.Request) io.ReadSeeker {
|
|
rs := originalDataReaderSeeker
|
|
if len(ext) > 0 {
|
|
ext = strings.ToLower(ext)
|
|
}
|
|
x1, y1, x2, y2, shouldCrop := shouldCropImages(ext, r)
|
|
if shouldCrop {
|
|
var err error
|
|
rs, err = images.Cropped(ext, rs, x1, y1, x2, y2)
|
|
if err != nil {
|
|
glog.Errorf("Cropping images error: %s", err)
|
|
}
|
|
}
|
|
return rs
|
|
}
|
|
|
|
func shouldCropImages(ext string, r *http.Request) (x1, y1, x2, y2 int, shouldCrop bool) {
|
|
if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" {
|
|
if r.FormValue("crop_x1") != "" {
|
|
x1, _ = strconv.Atoi(r.FormValue("crop_x1"))
|
|
}
|
|
if r.FormValue("crop_y1") != "" {
|
|
y1, _ = strconv.Atoi(r.FormValue("crop_y1"))
|
|
}
|
|
if r.FormValue("crop_x2") != "" {
|
|
x2, _ = strconv.Atoi(r.FormValue("crop_x2"))
|
|
}
|
|
if r.FormValue("crop_y2") != "" {
|
|
y2, _ = strconv.Atoi(r.FormValue("crop_y2"))
|
|
}
|
|
}
|
|
shouldCrop = x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1
|
|
return
|
|
}
|
|
|
|
func writeResponseContent(filename, mimeType string, rs io.ReadSeeker, w http.ResponseWriter, r *http.Request) error {
|
|
totalSize, e := rs.Seek(0, 2)
|
|
if mimeType == "" {
|
|
if ext := filepath.Ext(filename); ext != "" {
|
|
mimeType = mime.TypeByExtension(ext)
|
|
}
|
|
}
|
|
if mimeType != "" {
|
|
w.Header().Set("Content-Type", mimeType)
|
|
}
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
|
|
AdjustPassthroughHeaders(w, r, filename)
|
|
|
|
if r.Method == http.MethodHead {
|
|
w.Header().Set("Content-Length", strconv.FormatInt(totalSize, 10))
|
|
return nil
|
|
}
|
|
|
|
return ProcessRangeRequest(r, w, totalSize, mimeType, func(offset int64, size int64) (filer.DoStreamContent, error) {
|
|
return func(writer io.Writer) error {
|
|
if _, e = rs.Seek(offset, 0); e != nil {
|
|
return e
|
|
}
|
|
_, e = io.CopyN(writer, rs, size)
|
|
return e
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (vs *VolumeServer) streamWriteResponseContent(filename string, mimeType string, volumeId needle.VolumeId, n *needle.Needle, w http.ResponseWriter, r *http.Request, readOption *storage.ReadOption) {
|
|
totalSize := int64(n.DataSize)
|
|
if mimeType == "" {
|
|
if ext := filepath.Ext(filename); ext != "" {
|
|
mimeType = mime.TypeByExtension(ext)
|
|
}
|
|
}
|
|
if mimeType != "" {
|
|
w.Header().Set("Content-Type", mimeType)
|
|
}
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
AdjustPassthroughHeaders(w, r, filename)
|
|
|
|
if r.Method == http.MethodHead {
|
|
// HEAD ends at the metadata read; record its success.
|
|
if v := vs.store.GetVolume(volumeId); v != nil {
|
|
v.NoteMetaOnlySuccess()
|
|
}
|
|
w.Header().Set("Content-Length", strconv.FormatInt(totalSize, 10))
|
|
return
|
|
}
|
|
|
|
ProcessRangeRequest(r, w, totalSize, mimeType, func(offset int64, size int64) (filer.DoStreamContent, error) {
|
|
return func(writer io.Writer) error {
|
|
return vs.store.ReadVolumeNeedleDataInto(volumeId, n, readOption, writer, offset, size)
|
|
}, nil
|
|
})
|
|
|
|
}
|