mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Three hooks, all on the entry's real path so JWT scopes, WORM, and read-only rules apply unchanged: - POST /path?format=<name> ingests a multipart index sidecar plus media and cuts storage chunks on the extents the sidecar declares - GET /path?view=<name> serves adapter views; rendered documents and extent streams both ride the normal prefetch path with entry ETag, preconditions, and HEAD support - POST /path?repack=<name> derives the layout from the stored bytes and rewrites the chunks cut on extent boundaries, swapping the entry under the entry lock and queueing the old chunks for deletion The layout is advisory: a stale one 404s its views while plain reads stay untouched. Repack refuses hard-linked, remote, and SSE entries.
354 lines
11 KiB
Go
354 lines
11 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/constants"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
var (
|
|
OS_UID = uint32(os.Getuid())
|
|
OS_GID = uint32(os.Getgid())
|
|
|
|
ErrReadOnly = errors.New("read only")
|
|
)
|
|
|
|
type FilerPostResult struct {
|
|
Name string `json:"name,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func (fs *FilerServer) assignNewFileInfo(ctx context.Context, so *operation.StorageOption, expectedDataSize uint64) (fileId, urlLocation string, auth security.EncodedJwt, err error) {
|
|
|
|
stats.FilerHandlerCounter.WithLabelValues(stats.ChunkAssign).Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerRequestHistogram.WithLabelValues(stats.ChunkAssign).Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
ar, altRequest := so.ToAssignRequests(1)
|
|
ar.ExpectedDataSize = expectedDataSize
|
|
if altRequest != nil {
|
|
altRequest.ExpectedDataSize = expectedDataSize
|
|
}
|
|
|
|
// Use a context that ignores cancellation from the request context
|
|
assignCtx := context.WithoutCancel(ctx)
|
|
|
|
assignResult, ae := operation.Assign(assignCtx, fs.filer.GetMaster, fs.grpcDialOption, ar, altRequest)
|
|
if ae != nil {
|
|
glog.ErrorfCtx(ctx, "failing to assign a file id: %v", ae)
|
|
err = ae
|
|
return
|
|
}
|
|
fileId = assignResult.Fid
|
|
assignUrl := assignResult.Url
|
|
// Prefer same data center
|
|
if fs.option.DataCenter != "" {
|
|
for _, repl := range assignResult.Replicas {
|
|
if repl.DataCenter == fs.option.DataCenter {
|
|
assignUrl = repl.Url
|
|
break
|
|
}
|
|
}
|
|
}
|
|
urlLocation = "http://" + assignUrl + "/" + assignResult.Fid
|
|
if so.Fsync {
|
|
urlLocation += "?fsync=true"
|
|
}
|
|
auth = assignResult.Auth
|
|
return
|
|
}
|
|
|
|
func (fs *FilerServer) PostHandler(w http.ResponseWriter, r *http.Request, contentLength int64) {
|
|
ctx := r.Context()
|
|
|
|
destination := r.RequestURI
|
|
headerDestination := r.Header.Get(s3_constants.SeaweedStorageDestinationHeader)
|
|
if headerDestination != "" {
|
|
destination = headerDestination
|
|
}
|
|
|
|
// The destination header picks storage rules for a logical destination, but
|
|
// the entry is written at r.URL.Path. Enforce the read-only/quota rule on the
|
|
// actual write path too, so the header cannot route a write into a read-only
|
|
// location.
|
|
if headerDestination != "" && fs.filer.FilerConf.MatchStorageRule(r.URL.Path).ReadOnly {
|
|
writeJsonError(w, r, http.StatusInsufficientStorage, ErrReadOnly)
|
|
return
|
|
}
|
|
|
|
query := r.URL.Query()
|
|
so, err := fs.detectStorageOption0(ctx, destination,
|
|
query.Get("collection"),
|
|
query.Get("replication"),
|
|
query.Get("ttl"),
|
|
query.Get("disk"),
|
|
query.Get("fsync"),
|
|
query.Get("dataCenter"),
|
|
query.Get("rack"),
|
|
query.Get("dataNode"),
|
|
query.Get("saveInside"),
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, ErrReadOnly) {
|
|
writeJsonError(w, r, http.StatusInsufficientStorage, err)
|
|
} else {
|
|
glog.V(1).InfolnCtx(ctx, "post", r.RequestURI, ":", err.Error())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if util.FullPath(r.URL.Path).IsLongerFileName(so.MaxFileNameLength) {
|
|
glog.V(1).InfolnCtx(ctx, "post", r.RequestURI, ": ", "entry name too long")
|
|
w.WriteHeader(http.StatusRequestURITooLong)
|
|
return
|
|
}
|
|
|
|
// When DiskType is empty,use filer's -disk
|
|
if so.DiskType == "" {
|
|
so.DiskType = fs.option.DiskType
|
|
}
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/etc") {
|
|
so.SaveInside = true
|
|
}
|
|
|
|
if query.Has("mv.from") {
|
|
fs.move(ctx, w, r, so)
|
|
} else if query.Has("cp.from") {
|
|
fs.copy(ctx, w, r, so)
|
|
} else if query.Get("format") != "" {
|
|
fs.formatIngest(ctx, w, r, so)
|
|
} else if query.Get("repack") != "" {
|
|
fs.formatRepack(ctx, w, r, so)
|
|
} else {
|
|
fs.autoChunk(ctx, w, r, contentLength, so)
|
|
}
|
|
|
|
util_http.CloseRequest(r)
|
|
|
|
}
|
|
|
|
func (fs *FilerServer) move(ctx context.Context, w http.ResponseWriter, r *http.Request, so *operation.StorageOption) {
|
|
src := r.URL.Query().Get("mv.from")
|
|
dst := r.URL.Path
|
|
|
|
glog.V(2).InfofCtx(ctx, "FilerServer.move %v to %v", src, dst)
|
|
|
|
var err error
|
|
if src, err = clearName(src); err != nil {
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if dst, err = clearName(dst); err != nil {
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
src = strings.TrimRight(src, "/")
|
|
if src == "" {
|
|
err = fmt.Errorf("invalid source '/'")
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
srcPath := util.FullPath(src)
|
|
dstPath := util.FullPath(dst)
|
|
if dstPath.IsLongerFileName(so.MaxFileNameLength) {
|
|
err = fmt.Errorf("dst name to long")
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
srcEntry, err := fs.filer.FindEntry(ctx, srcPath)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to get src entry '%s', err: %s", src, err)
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
wormEnforced, err := fs.wormEnforcedForEntry(ctx, src)
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
} else if wormEnforced {
|
|
// you cannot move a worm file or directory
|
|
err = fmt.Errorf("cannot move write-once entry from '%s' to '%s': %s", src, dst, constants.ErrMsgOperationNotPermitted)
|
|
writeJsonError(w, r, http.StatusForbidden, err)
|
|
return
|
|
}
|
|
|
|
oldDir, oldName := srcPath.DirAndName()
|
|
newDir, newName := dstPath.DirAndName()
|
|
newName = util.Nvl(newName, oldName)
|
|
|
|
dstEntry, err := fs.filer.FindEntry(ctx, util.FullPath(strings.TrimRight(dst, "/")))
|
|
if err != nil && err != filer_pb.ErrNotFound {
|
|
err = fmt.Errorf("failed to get dst entry '%s', err: %s", dst, err)
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if err == nil && !dstEntry.IsDirectory() && srcEntry.IsDirectory() {
|
|
err = fmt.Errorf("move: cannot overwrite non-directory '%s' with directory '%s'", dst, src)
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
_, err = fs.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
|
|
OldDirectory: oldDir,
|
|
OldName: oldName,
|
|
NewDirectory: newDir,
|
|
NewName: newName,
|
|
})
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to move entry from '%s' to '%s', err: %s", src, dst, err)
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// curl -X DELETE http://localhost:8888/path/to
|
|
// curl -X DELETE http://localhost:8888/path/to?recursive=true
|
|
// curl -X DELETE http://localhost:8888/path/to?recursive=true&ignoreRecursiveError=true
|
|
// curl -X DELETE http://localhost:8888/path/to?recursive=true&skipChunkDeletion=true
|
|
func (fs *FilerServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
isRecursive := r.FormValue("recursive") == "true"
|
|
if !isRecursive && fs.option.recursiveDelete {
|
|
if r.FormValue("recursive") != "false" {
|
|
isRecursive = true
|
|
}
|
|
}
|
|
ignoreRecursiveError := r.FormValue("ignoreRecursiveError") == "true"
|
|
skipChunkDeletion := r.FormValue("skipChunkDeletion") == "true"
|
|
|
|
objectPath := r.URL.Path
|
|
if len(r.URL.Path) > 1 && strings.HasSuffix(objectPath, "/") {
|
|
objectPath = objectPath[0 : len(objectPath)-1]
|
|
}
|
|
|
|
wormEnforced, err := fs.wormEnforcedForEntry(context.TODO(), objectPath)
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
} else if wormEnforced {
|
|
writeJsonError(w, r, http.StatusForbidden, errors.New(constants.ErrMsgOperationNotPermitted))
|
|
return
|
|
}
|
|
|
|
err = fs.filer.DeleteEntryMetaAndData(context.Background(), util.FullPath(objectPath), isRecursive, ignoreRecursiveError, !skipChunkDeletion, false, nil, 0)
|
|
if err != nil && err != filer_pb.ErrNotFound {
|
|
glog.V(1).Infoln("deleting", objectPath, ":", err.Error())
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (fs *FilerServer) detectStorageOption(ctx context.Context, requestURI, qCollection, qReplication string, ttlSeconds int32, diskType, dataCenter, rack, dataNode string) (*operation.StorageOption, error) {
|
|
|
|
rule := fs.filer.FilerConf.MatchStorageRule(requestURI)
|
|
|
|
if rule.ReadOnly {
|
|
// Name the read-only prefix so the caller knows which path is locked and why.
|
|
// MatchStorageRule leaves LocationPrefix empty when several rules merge; fall back to the request path.
|
|
prefix := rule.LocationPrefix
|
|
if prefix == "" {
|
|
// requestURI may carry a query string on the HTTP path; keep only the path.
|
|
prefix, _, _ = strings.Cut(requestURI, "?")
|
|
}
|
|
return nil, fmt.Errorf("%w: %s (e.g. bucket over quota)", ErrReadOnly, prefix)
|
|
}
|
|
|
|
// Use local variable instead of mutating shared rule
|
|
maxFileNameLength := rule.MaxFileNameLength
|
|
if maxFileNameLength == 0 {
|
|
maxFileNameLength = fs.filer.MaxFilenameLength
|
|
}
|
|
|
|
// required by buckets folder
|
|
bucketDefaultCollection := ""
|
|
if strings.HasPrefix(requestURI, fs.filer.DirBucketsPath+"/") {
|
|
bucketDefaultCollection = fs.filer.DetectBucket(util.FullPath(requestURI))
|
|
}
|
|
|
|
if ttlSeconds == 0 {
|
|
ttl, err := needle.ReadTTL(rule.GetTtl())
|
|
if err != nil {
|
|
glog.ErrorfCtx(ctx, "fail to parse %s ttl setting %s: %v", rule.LocationPrefix, rule.Ttl, err)
|
|
}
|
|
ttlSeconds = int32(ttl.Minutes()) * 60
|
|
}
|
|
|
|
collection := util.Nvl(qCollection, rule.Collection, bucketDefaultCollection, fs.option.Collection)
|
|
|
|
// A placement overlay steers a bound collection's new volumes onto a tier.
|
|
// It sits between the explicit request and the filer.conf rule: it overrides
|
|
// the rule but yields to a value the caller asked for outright. Only a
|
|
// steered collection contributes values; an unsteered one clears them so it
|
|
// falls straight through to the rule.
|
|
overlayDisk, overlayReplication, overlayDataCenter, overlaySteered := fs.filer.ResolvePlacement(collection)
|
|
if !overlaySteered {
|
|
overlayDisk, overlayReplication, overlayDataCenter = "", "", ""
|
|
} else {
|
|
glog.V(4).InfofCtx(ctx, "placement overlay steers collection %s: disk=%q replication=%q dataCenter=%q",
|
|
collection, overlayDisk, overlayReplication, overlayDataCenter)
|
|
}
|
|
|
|
return &operation.StorageOption{
|
|
Replication: util.Nvl(qReplication, overlayReplication, rule.Replication, fs.option.DefaultReplication),
|
|
Collection: collection,
|
|
DataCenter: util.Nvl(dataCenter, overlayDataCenter, rule.DataCenter, fs.option.DataCenter),
|
|
Rack: util.Nvl(rack, rule.Rack, fs.option.Rack),
|
|
DataNode: util.Nvl(dataNode, rule.DataNode, fs.option.DataNode),
|
|
TtlSeconds: ttlSeconds,
|
|
DiskType: util.Nvl(diskType, overlayDisk, rule.DiskType),
|
|
Fsync: rule.Fsync,
|
|
VolumeGrowthCount: rule.VolumeGrowthCount,
|
|
MaxFileNameLength: maxFileNameLength,
|
|
}, nil
|
|
}
|
|
|
|
func (fs *FilerServer) detectStorageOption0(ctx context.Context, requestURI, qCollection, qReplication string, qTtl string, diskType string, fsync string, dataCenter, rack, dataNode, saveInside string) (*operation.StorageOption, error) {
|
|
|
|
ttl, err := needle.ReadTTL(qTtl)
|
|
if err != nil {
|
|
glog.ErrorfCtx(ctx, "fail to parse ttl %s: %v", qTtl, err)
|
|
}
|
|
|
|
so, err := fs.detectStorageOption(ctx, requestURI, qCollection, qReplication, int32(ttl.Minutes())*60, diskType, dataCenter, rack, dataNode)
|
|
if so != nil {
|
|
if fsync == "false" {
|
|
so.Fsync = false
|
|
} else if fsync == "true" {
|
|
so.Fsync = true
|
|
}
|
|
if saveInside == "true" {
|
|
so.SaveInside = true
|
|
} else {
|
|
so.SaveInside = false
|
|
}
|
|
}
|
|
|
|
return so, err
|
|
}
|