mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-17 04:36:50 +00:00
* remote storage: carry Content-Encoding into mounted entries A RemoteEntry now records the remote object's Content-Encoding, and every path that materializes a local entry from remote metadata (lazy fetch, lazy listing, remote.mount, remote.meta.sync, remote.cache) stamps it into the entry extended attributes, so HTTP and S3 HeadObject/GetObject return the header. GCS and Azure populate it on listing and stat; S3 only exposes it via HeadObject, so listings leave it empty. * remote storage: set Content-Encoding when uploading to the remote An entry carrying Content-Encoding in its extended attributes (a native S3 upload, or a value pulled from the remote) now keeps it when filer.remote.sync or remote.copy.local writes the object to GCS, S3, or Azure, instead of silently dropping it. * gcs: read remote objects without decompressive transcoding GCS transparently decompresses gzip-encoded objects on download, which ignores range requests and returns byte counts that disagree with the tracked RemoteSize. Request the stored bytes instead; chunked reads of gzip-encoded objects then behave like any other object. * remote storage: track Content-Encoding presence so removals propagate A listing that does not report encodings (S3) leaves the field unset and the local header untouched, while an authoritative report of no encoding (GCS, Azure, any stat) now clears a previously stamped header instead of leaving it stale. remote.cache also schedules a metadata update when only the reported encoding changes. * remote storage: propagate Content-Encoding on metadata-only updates filer.remote.sync routes same-content changes through UpdateFileMetadata, which only touched custom metadata (GCS, Azure) or tags (S3), so a Content-Encoding change in the extended attributes never reached the remote object's real header. GCS now patches contentEncoding alongside the metadata, and Azure reissues the blob's HTTP headers with the new value, carrying the others over since the call replaces the full set. S3 stays tags-only: changing the header there means rewriting the object, which the sync already does whenever content changes. * remote.meta.sync: optional per-file stat for listing-omitted metadata S3 listings carry no Content-Encoding, so entries synced from them never learn it and the lazy-stat path never runs once an entry exists. With -statFiles, each new or changed file whose listing left the encoding unreported is stat-ed before reconciling, and the stat-derived value is persisted so the next run only stats files that changed. Off by default: it costs one remote request per file, and GCS and Azure listings already carry the encoding. * s3: apply metadata-only Content-Encoding changes with an in-place copy Content-Encoding is S3 system metadata, so the tags-only metadata update silently left the object's real header untouched. When the encoding differs, reissue the object as a self-copy with replaced metadata, carrying the content type and configured storage class like a fresh write does. CopyObject caps at 5 GiB; beyond that the change is logged and applies on the next content write. * azure: skip the metadata call when user metadata is unchanged An encoding-only change reissues the blob's HTTP headers; sending the unchanged user metadata alongside it wastes a round trip and bumps the blob's ETag once more than needed. * s3: carry existing object metadata through the encoding copy The replace directive drops everything not resent, and a mounted entry usually has no local mime or user metadata, so the in-place copy wiped the object's Content-Type, Cache-Control, user metadata, encryption settings, and storage class. Read them back with a HeadObject first and carry them over, overriding only what SeaweedFS manages: the encoding, a locally set mime, and the configured storage class. S3 reports Expires as a string while the copy input wants a time, so it is parsed and skipped when malformed.
179 lines
5.7 KiB
Go
179 lines
5.7 KiB
Go
package remote_storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/remote_pb"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
const slash = "/"
|
|
|
|
func ParseLocationName(remote string) (locationName string) {
|
|
remote = strings.TrimSuffix(remote, slash)
|
|
parts := strings.SplitN(remote, slash, 2)
|
|
if len(parts) >= 1 {
|
|
return parts[0]
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseBucketLocation(remote string) (loc *remote_pb.RemoteStorageLocation) {
|
|
loc = &remote_pb.RemoteStorageLocation{}
|
|
remote = strings.TrimSuffix(remote, slash)
|
|
parts := strings.SplitN(remote, slash, 3)
|
|
if len(parts) >= 1 {
|
|
loc.Name = parts[0]
|
|
}
|
|
if len(parts) >= 2 {
|
|
loc.Bucket = parts[1]
|
|
}
|
|
loc.Path = remote[len(loc.Name)+1+len(loc.Bucket):]
|
|
if loc.Path == "" {
|
|
loc.Path = slash
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseNoBucketLocation(remote string) (loc *remote_pb.RemoteStorageLocation) {
|
|
loc = &remote_pb.RemoteStorageLocation{}
|
|
remote = strings.TrimSuffix(remote, slash)
|
|
parts := strings.SplitN(remote, slash, 2)
|
|
if len(parts) >= 1 {
|
|
loc.Name = parts[0]
|
|
}
|
|
loc.Path = remote[len(loc.Name):]
|
|
if loc.Path == "" {
|
|
loc.Path = slash
|
|
}
|
|
return
|
|
}
|
|
|
|
func FormatLocation(loc *remote_pb.RemoteStorageLocation) string {
|
|
if loc.Bucket == "" {
|
|
return fmt.Sprintf("%s%s", loc.Name, loc.Path)
|
|
}
|
|
return fmt.Sprintf("%s/%s%s", loc.Name, loc.Bucket, loc.Path)
|
|
}
|
|
|
|
type VisitFunc func(dir string, name string, isDirectory bool, remoteEntry *filer_pb.RemoteEntry) error
|
|
|
|
// EntryContentEncoding returns the Content-Encoding stored in the entry
|
|
// extended attributes, for clients to set on uploaded remote objects.
|
|
func EntryContentEncoding(entry *filer_pb.Entry) string {
|
|
if entry == nil {
|
|
return ""
|
|
}
|
|
return string(entry.Extended["Content-Encoding"])
|
|
}
|
|
|
|
type Bucket struct {
|
|
Name string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// ErrRemoteObjectNotFound is returned by StatFile when the object does not exist in the remote storage backend.
|
|
var ErrRemoteObjectNotFound = errors.New("remote object not found")
|
|
|
|
type RemoteStorageClient interface {
|
|
Traverse(loc *remote_pb.RemoteStorageLocation, visitFn VisitFunc) error
|
|
ListDirectory(ctx context.Context, loc *remote_pb.RemoteStorageLocation, visitFn VisitFunc) error
|
|
StatFile(loc *remote_pb.RemoteStorageLocation) (remoteEntry *filer_pb.RemoteEntry, err error)
|
|
ReadFile(loc *remote_pb.RemoteStorageLocation, offset int64, size int64) (data []byte, err error)
|
|
WriteDirectory(loc *remote_pb.RemoteStorageLocation, entry *filer_pb.Entry) (err error)
|
|
RemoveDirectory(loc *remote_pb.RemoteStorageLocation) (err error)
|
|
WriteFile(loc *remote_pb.RemoteStorageLocation, entry *filer_pb.Entry, reader io.Reader) (remoteEntry *filer_pb.RemoteEntry, err error)
|
|
UpdateFileMetadata(loc *remote_pb.RemoteStorageLocation, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) (err error)
|
|
DeleteFile(loc *remote_pb.RemoteStorageLocation) (err error)
|
|
ListBuckets() ([]*Bucket, error)
|
|
CreateBucket(name string) (err error)
|
|
DeleteBucket(name string) (err error)
|
|
}
|
|
|
|
// RemoteStorageConcurrentReader is an optional interface for remote storage clients
|
|
// that support configurable download concurrency for multipart downloads.
|
|
type RemoteStorageConcurrentReader interface {
|
|
ReadFileWithConcurrency(loc *remote_pb.RemoteStorageLocation, offset int64, size int64, concurrency int) (data []byte, err error)
|
|
}
|
|
|
|
// RemoteStorageStreamReader is an optional interface for remote storage clients
|
|
// that support streaming reads with io.Reader for efficient memory usage.
|
|
type RemoteStorageStreamReader interface {
|
|
ReadFileAsStream(ctx context.Context, loc *remote_pb.RemoteStorageLocation, offset int64, size int64) (reader io.ReadCloser, err error)
|
|
}
|
|
|
|
type RemoteStorageClientMaker interface {
|
|
Make(remoteConf *remote_pb.RemoteConf) (RemoteStorageClient, error)
|
|
HasBucket() bool
|
|
}
|
|
|
|
type CachedRemoteStorageClient struct {
|
|
*remote_pb.RemoteConf
|
|
RemoteStorageClient
|
|
}
|
|
|
|
var (
|
|
RemoteStorageClientMakers = make(map[string]RemoteStorageClientMaker)
|
|
remoteStorageClients = make(map[string]CachedRemoteStorageClient)
|
|
remoteStorageClientsLock sync.Mutex
|
|
)
|
|
|
|
func GetAllRemoteStorageNames() string {
|
|
var storageNames []string
|
|
for k := range RemoteStorageClientMakers {
|
|
storageNames = append(storageNames, k)
|
|
}
|
|
sort.Strings(storageNames)
|
|
return strings.Join(storageNames, "|")
|
|
}
|
|
|
|
func ParseRemoteLocation(remoteConfType string, remote string) (remoteStorageLocation *remote_pb.RemoteStorageLocation, err error) {
|
|
maker, found := RemoteStorageClientMakers[remoteConfType]
|
|
if !found {
|
|
return nil, fmt.Errorf("remote storage type %s not found", remoteConfType)
|
|
}
|
|
|
|
if !maker.HasBucket() {
|
|
return parseNoBucketLocation(remote), nil
|
|
}
|
|
return parseBucketLocation(remote), nil
|
|
}
|
|
|
|
func makeRemoteStorageClient(remoteConf *remote_pb.RemoteConf) (RemoteStorageClient, error) {
|
|
maker, found := RemoteStorageClientMakers[remoteConf.Type]
|
|
if !found {
|
|
return nil, fmt.Errorf("remote storage type %s not found", remoteConf.Type)
|
|
}
|
|
return maker.Make(remoteConf)
|
|
}
|
|
|
|
func GetRemoteStorage(remoteConf *remote_pb.RemoteConf) (RemoteStorageClient, error) {
|
|
remoteStorageClientsLock.Lock()
|
|
defer remoteStorageClientsLock.Unlock()
|
|
|
|
existingRemoteStorageClient, found := remoteStorageClients[remoteConf.Name]
|
|
if found && proto.Equal(existingRemoteStorageClient.RemoteConf, remoteConf) {
|
|
return existingRemoteStorageClient.RemoteStorageClient, nil
|
|
}
|
|
|
|
newRemoteStorageClient, err := makeRemoteStorageClient(remoteConf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("make remote storage client %s: %v", remoteConf.Name, err)
|
|
}
|
|
|
|
remoteStorageClients[remoteConf.Name] = CachedRemoteStorageClient{
|
|
RemoteConf: remoteConf,
|
|
RemoteStorageClient: newRemoteStorageClient,
|
|
}
|
|
|
|
return newRemoteStorageClient, nil
|
|
}
|