Files
seaweedfs/weed/server/volume_server_handlers_write.go
T
acb44a68a9 fix(volume): skip unnecessary needle data reads on delete (#11711)
* fix(volume): do not read a needle's data to delete it

A delete read the needle's whole payload from disk only to discard it.
The delete handler and BatchDelete need the needle's cookie and its
chunk-manifest flag, which live in the header and trailer, but fetched
them with a full needle read, the same call a GET uses.

readNeedle now reads the header and trailer only when MustMetaOnly is
set, whatever the needle's size, and the delete paths ask for that. The
Rust volume server honours must_meta_only the same way for its delete
paths. A compressed needle or a chunk manifest still gets the full read,
as before, because their callers need the bytes.

HEAD is unchanged. It used to set MustMetaOnly, which nothing read; it
now asks for AttemptMetaOnly alone, so it keeps reading a needle up to
PagedReadLimit whole. On a spinning disk that one read is cheaper than a
header read followed by a trailer read.

Version 1 needles are always read in full. They have no data size field
and no metadata after the data, which ReadNeedleMeta relies on, so a
metadata-only read of one failed with "invalid needle meta size". That
already happened for version 1 needles above PagedReadLimit read with
AttemptMetaOnly.

A delete therefore no longer verifies the data CRC of the needle it
removes. The cookie check is unchanged.

* fix(volume): read transformed images fully on HEAD too

A HEAD carrying resize or crop params reports the transformed length,
which a metadata read cannot produce. Go and Rust both answered such
HEADs from the source header, returning the stored size instead of the
cropped or resized one.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-12 18:08:19 +08:00

195 lines
5.9 KiB
Go

package weed_server
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/topology"
"github.com/seaweedfs/seaweedfs/weed/util/buffer_pool"
)
func (vs *VolumeServer) PostHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if e := r.ParseForm(); e != nil {
glog.V(0).InfolnCtx(ctx, "form parse error:", e)
writeJsonError(w, r, http.StatusBadRequest, e)
return
}
vid, fid, _, _, _ := parseURLPath(r.URL.Path)
volumeId, ve := needle.NewVolumeId(vid)
if ve != nil {
glog.V(0).InfolnCtx(ctx, "NewVolumeId error:", ve)
stats.VolumeServerFileWriteFailures.Inc()
writeJsonError(w, r, http.StatusBadRequest, ve)
return
}
if !vs.maybeCheckJwtAuthorization(r, vid, fid, true) {
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
return
}
bytesBuffer := buffer_pool.SyncPoolGetBuffer()
defer buffer_pool.SyncPoolPutBuffer(bytesBuffer)
reqNeedle, originalSize, contentMd5, ne := needle.CreateNeedleFromRequest(r, vs.FixJpgOrientation, vs.fileSizeLimitBytes, bytesBuffer)
if ne != nil {
stats.VolumeServerFileWriteFailures.Inc()
writeJsonError(w, r, http.StatusBadRequest, ne)
return
}
ret := operation.UploadResult{}
// use context.WithoutCancel to avoid context cancellation when the client connection is closed
isUnchanged, writeError := topology.ReplicatedWrite(context.WithoutCancel(ctx), vs.GetMaster, vs.grpcDialOption, vs.store, volumeId, reqNeedle, r, contentMd5)
if writeError != nil {
stats.VolumeServerFileWriteFailures.Inc()
writeJsonError(w, r, http.StatusInternalServerError, writeError)
return
}
// http 204 status code does not allow body
if writeError == nil && isUnchanged {
SetEtag(w, reqNeedle.Etag())
w.Header().Set("Content-MD5", contentMd5)
w.WriteHeader(http.StatusNoContent)
return
}
httpStatus := http.StatusCreated
if reqNeedle.HasName() {
ret.Name = string(reqNeedle.Name)
}
ret.Size = uint32(originalSize)
ret.ETag = reqNeedle.Etag()
ret.Mime = string(reqNeedle.Mime)
ret.ContentMd5 = contentMd5
SetEtag(w, ret.ETag)
w.Header().Set("Content-MD5", contentMd5)
writeJsonQuiet(w, r, httpStatus, ret)
}
func (vs *VolumeServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
n := new(needle.Needle)
vid, fid, _, _, _ := parseURLPath(r.URL.Path)
volumeId, _ := needle.NewVolumeId(vid)
if err := n.ParsePath(fid); err != nil {
writeJsonError(w, r, http.StatusBadRequest, err)
return
}
if !vs.maybeCheckJwtAuthorization(r, vid, fid, true) {
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
return
}
// glog.V(2).Infof("volume %s deleting %s", vid, n)
cookie := n.Cookie
ecVolume, hasEcVolume := vs.store.FindEcVolume(volumeId)
if hasEcVolume {
count, err := vs.store.DeleteEcShardNeedle(ecVolume, n, cookie)
if errors.Is(err, storage.ErrorDeleted) {
// Already gone. The non-EC path below answers 404 from its
// ReadVolumeNeedle pre-check rather than counting a write failure,
// and callers fold that 404 into success.
m := make(map[string]uint32)
m["size"] = 0
writeJsonQuiet(w, r, http.StatusNotFound, m)
return
}
writeDeleteResult(err, count, w, r)
return
}
_, ok := vs.store.ReadVolumeNeedle(volumeId, n, deleteReadOption(), nil)
if ok != nil {
m := make(map[string]uint32)
m["size"] = 0
writeJsonQuiet(w, r, http.StatusNotFound, m)
return
}
if n.Cookie != cookie {
glog.V(0).Infoln("delete", r.URL.Path, "with unmaching cookie from ", r.RemoteAddr, "agent", r.UserAgent())
writeJsonError(w, r, http.StatusBadRequest, errors.New("File Random Cookie does not match."))
return
}
count := int64(n.Size)
if n.IsChunkedManifest() {
chunkManifest, e := operation.LoadChunkManifest(n.Data, n.IsCompressed())
if e != nil {
stats.VolumeServerFileWriteFailures.Inc()
writeJsonError(w, r, http.StatusInternalServerError, fmt.Errorf("Load chunks manifest error: %v", e))
return
}
// make sure all chunks had deleted before delete manifest
if e := chunkManifest.DeleteChunks(vs.GetMaster, false, vs.grpcDialOption); e != nil {
stats.VolumeServerFileWriteFailures.Inc()
writeJsonError(w, r, http.StatusInternalServerError, fmt.Errorf("Delete chunks error: %v", e))
return
}
count = chunkManifest.Size
}
n.LastModified = uint64(time.Now().Unix())
if len(r.FormValue("ts")) > 0 {
modifiedTime, err := strconv.ParseInt(r.FormValue("ts"), 10, 64)
if err == nil {
n.LastModified = uint64(modifiedTime)
}
}
_, err := topology.ReplicatedDelete(vs.GetMaster, vs.grpcDialOption, vs.store, volumeId, n, r)
writeDeleteResult(err, count, w, r)
}
// deleteReadOption returns the read option used by deletes: read only the
// needle's header and trailer.
//
// A delete needs to know that the needle exists, what its cookie is, and
// whether it is a chunk manifest. None of that requires the payload. If the
// needle turns out to be a chunk manifest, the read loads its data as well,
// because the delete needs the manifest to delete the chunks.
func deleteReadOption() *storage.ReadOption {
return &storage.ReadOption{AttemptMetaOnly: true, MustMetaOnly: true}
}
func writeDeleteResult(err error, count int64, w http.ResponseWriter, r *http.Request) {
if err == nil {
m := make(map[string]int64)
m["size"] = count
writeJsonQuiet(w, r, http.StatusAccepted, m)
} else {
stats.VolumeServerFileWriteFailures.Inc()
writeJsonError(w, r, http.StatusInternalServerError, fmt.Errorf("Deletion Failed: %w", err))
}
}
func SetEtag(w http.ResponseWriter, etag string) {
if etag != "" {
if strings.HasPrefix(etag, "\"") {
w.Header().Set("ETag", etag)
} else {
w.Header().Set("ETag", "\""+etag+"\"")
}
}
}