mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-23 22:40:39 +02:00
* volume: drop index entries that resolve to a different needle An index row whose offset lands on a needle with a different id and a different size cannot have been produced by any write path: every appender indexes a key at the offset and size of the record it wrote, and WriteNeedleBlob keeps its indexed size consistent with the blob header. Such rows are corruption (for example an offset replayed with stale high bytes) and until now made the volume reject every write for the key with "mismatching cookie" while reads resolved to the wrong needle. When the duplicate check finds the indexed record belongs to a different key and size, drop the stale entry and let the append re-index the key. Same-key conflicts still fail the cookie check, and entries staying consistent with a re-keyed blob keep their existing semantics. * volume: verify the indexed needle header before streaming reads The buffered read path re-parses the record and rejects a needle whose stored size disagrees with the index, but the streamed path serves data bytes without ever looking at the header, so a stale index row would stream another needle's payload. Probe the header once up front and report a miss when the record is neither the requested key nor the indexed size, the same staleness test the write path applies. * rust volume: drop index entries that resolve to a different needle Mirror of the Go volume change: when the duplicate check finds the indexed record belongs to a different key and size, delete the stale entry and let the append re-index the key. * volume: overwrite a stale index entry on commit, not up front Deleting the stale row during the duplicate check published a tombstone before the replacement append and, for durable writes, before its fsync. A failed append or sync then rolled back the data but left the key deleted. The publish step already overwrites any older offset, so leave the entry in place and let it be replaced once the record commits. * volume: report streaming header-probe failures to the io error tracker A failed header read returned before the tracked body-read loop, so repeated EIO on the probe never reached checkReadWriteError. Skip io.EOF the way the loop does: it is a truncated record, not a disk fault. * rust volume: overwrite a stale index entry on commit, not up front Mirror of the Go volume change: the staged delete published a tombstone before the replacement append and its .dat flush, so a failed append or sync left the key deleted. publish_write already overwrites any older offset once the record commits. * rust volume: report needle meta read failures to the io error tracker read_meta is the header probe ahead of a streamed body: an EIO there escaped check_read_write_error entirely, so a failing disk could not trip the tolerance. Report the outcome the way read_full already does. * volume: retry the wrapped offset before judging an index entry stale A 4-byte index value can index past the 32 GiB wrap: the low offset may hold another needle while the real record sits at +MaxPossibleVolumeSize, which is where reads already retry on a size mismatch. Skipping the cookie check on the low-offset record alone would accept a foreign-cookie write over a live key. Retry the wrapped header read first, the same condition the read path uses; a wrapped read that fails for reasons other than EOF stops the write like the low-offset read does. * volume: probe the streamed needle header under the access lock With HasSlowRead the access lock is dropped after the index lookup, so a compaction commit could swap the backend between the lookup and the header probe and fail a valid download on the old offset. Take the read lock for the probe and resolve the compaction revision first, the way the streaming loop re-resolves it on every page. * rust volume: retry the wrapped offset before judging an index entry stale Mirror of the Go volume change: probe offset + MAX_POSSIBLE_VOLUME_SIZE under the 4-byte-offset build before treating a mismatched record as stale, so a live wrapped entry still gets its cookie checked. * rust volume: do not clear the io error streak on a successful meta read read_meta runs before the payload read in read_needle_for_get, so recording success there reset the streak before every failing download and the volume could never reach quarantine. Report failures only; read_full still records success after a complete read. * volume: record a meta-only read's success against the io error streak A meta read no longer clears the streak because a payload read still follows on GETs, but requests the meta read ends outright — HEAD answers and cookie mismatches — never reached the body path, so their success left earlier failures counted. Note the success on those terminal paths; mirrored in the Rust volume server. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: report streamed reads to the io error streak Streamed GETs never touched the tracker: a failing chunk read went uncounted and a completed download never cleared the streak, unlike readNeedleDataInto which does both. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: reset the io error streak only at terminal meta reads A streamed GET's metadata read runs before its payload read, so clearing the streak there erased the count before every failing download and the volume could never reach quarantine. Record the meta-read success only where the request actually ends: must-meta-only reads, HEAD responses, and cookie mismatches — matching the Rust volume server's placement. * rust volume: record the streamed read's success at the final frame Hyper can complete a Content-Length response without polling the body again, so a success recorded only when the stream is next polled could be skipped, leaving an earlier io error streak in place after a successful download. * volume: record a meta-only read's success on conditional 304 replies * rust volume: record a meta-only read's success on conditional 304 replies * volume: reset the io error streak only after the checksum passes A streamed read that ends in a CRC mismatch still cleared the EIO streak, so corruption could mask progress toward quarantine. The Rust side records success only after the final frame's checksum verifies; move the reset below the CRC check to match. * volume: satisfy codespell in the meta-read comment * rust volume: report range reads to the io error streak * rust volume: cover the range read's io error reporting --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
513 lines
15 KiB
Go
513 lines
15 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand/v2"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"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)
|
|
}
|
|
|
|
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.LookupVolumeId(vs.GetMaster, vs.grpcDialOption, volumeId.String())
|
|
if err != 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 {
|
|
rand.Shuffle(len(lookupResult.Locations), func(i, j int) {
|
|
lookupResult.Locations[i], lookupResult.Locations[j] = lookupResult.Locations[j], lookupResult.Locations[i]
|
|
})
|
|
}
|
|
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.NewRequest(http.MethodGet, 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
|
|
}
|
|
for k, vv := range r.Header {
|
|
for _, v := range vv {
|
|
request.Header.Add(k, v)
|
|
}
|
|
}
|
|
|
|
response, err := util_http.GetGlobalHttpClient().Do(request)
|
|
if err != nil {
|
|
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
|
|
for k, vv := range response.Header {
|
|
if k == "Server" {
|
|
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
|
|
}
|
|
}
|
|
|
|
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" {
|
|
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))
|
|
if r.Header.Get("If-Modified-Since") != "" {
|
|
if t, parseError := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); parseError == nil {
|
|
if t.Unix() >= int64(n.LastModified) {
|
|
if readOption.IsMetaOnly {
|
|
if v := vs.store.GetVolume(volumeId); v != nil {
|
|
v.NoteMetaOnlySuccess()
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if inm := r.Header.Get("If-None-Match"); inm == "\""+n.Etag()+"\"" {
|
|
if readOption.IsMetaOnly {
|
|
if v := vs.store.GetVolume(volumeId); v != nil {
|
|
v.NoteMetaOnlySuccess()
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
SetEtag(w, n.Etag())
|
|
|
|
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
|
|
})
|
|
|
|
}
|