mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-20 04:54:24 +02:00
* filer: free shared chunks when a folder delete drops the last hard link doBatchDeleteFolderMetaAndData skipped chunk collection for every hard-linked child, relying on maybeDeleteHardLinks to retire the shared inode record. When the delete removes the last link the record is gone and nobody else will ever free those chunks. After the link callback runs, check whether the record survived; if it did not, queue the entry's chunks for deletion. * filer: free orphaned hard-link chunks in the replayed delete sweep The subtree sweep on a replayed directory delete skipped every hard-linked child, so a link that was the inode's last owner leaked its shared chunks. Check the inode record after the store delete; when it is gone, queue the child's chunks like any other swept entry. * filer: mirror remote-object cleanup in the replayed delete sweep Children a lagging peer alone still holds under a replayed directory delete were dropped with plain store deletes: their remote objects leaked and lazy remote fetches could resurrect them. The sweep now records event-stamped tombstones and deletes swept non-directory children through maybeDeleteFromRemoteIfUnchanged, which carries the same stat guard the write-back daemon's processDeleteEvent applies plus a modification-time check — a delayed replay cannot erase an object another writer replaced, even a same-size one. A failed remote delete fails the replay so the event is retried instead of orphaning the object. The directory's own tombstone comes from the event pipeline at the event timestamp and covers the subtree. Metadata-only deletes and cross-cluster events keep the origin's semantics. * filerstore: return real errors from KvGet on MongoDB, ArangoDB, Elastic These stores mapped every lookup failure to ErrKvNotFound, so a database error looked identical to a missing key: hard-link records could appear gone while links still owned their chunks, and path timestamp reads could treat committed state as absent. Return the underlying error and keep ErrKvNotFound for genuinely missing keys. * filer: bind remote-object deletes to the observed object version A guarded stat-then-delete still lets a replacement landing between the two requests go down with the deleted object. Add an optional conditional-delete operation to remote storage clients — S3 If-Match on the live ETag, GCS generation match, Azure IfMatch — and use it everywhere a delete is guarded by a stored stamp: the write-back daemon's delete, rename-source, and pending-rename paths, and the replayed-delete sweep. Backends without it keep the guarded stat-then-delete. * filer: restamp the remote entry after a metadata-only remote update Changing Content-Encoding rewrites the object in place on S3 and advances its LastModified, while the recorded stamp keeps the old mtime. A version-checked delete would then mistake the object for a replacement and keep it forever. Re-stat after UpdateFileMetadata so the saved stamp describes the live object. * filer: mirror the remote-delete ownership checks in the replayed sweep The sweep deleted swept children's remote objects on the listing's snapshot alone. Two gaps: a remote-only entry that took the path since — one a commit record may not cover, such as a lazy fetch materialized from the remote — reads the object as its only copy, and an entry removed while its rename copy was pending leaves the object at the source key orphaned. The per-child delete now runs under the child's entry lock with the commit-time check repeated, keeps a remote-only entry that holds the path, and releases the pending rename's source under the same ownership and stamp conditions the write-back daemon's releasePendingSource applies. * remote.sync: restamp only the object the metadata update touched The live stat after UpdateFileMetadata was saved unconditionally, so a replacement written between the update and the stat stamped the entry with an object it never described — and a later delete would then pass the freshness guard on the replacement's data. The restamp now requires the live object to still match the stored stamp on size and ETag; a different object keeps the old stamp, which the delete guard reads as replaced and keeps. * filer: skip unbudgeted reader-cache test on 32-bit TestReaderCacheUnboundedWithoutBudget proves the reader cache has no implicit memory budget by keeping 96 concurrent 4 MiB downloads in flight (>256 MiB). On linux/386 the weed/filer test binary already runs near the 4 GiB address-space limit, so the burst reliably crashes it with "out of memory" mid-suite. * filer: bail out of a swept child lock instead of waiting on it Replay already holds the event's path locks when the sweep reaches a descendant. A local rename can hold that descendant while waiting on one of the replayed paths, so blocking on the child lock deadlocks both. The sweep now tries the lock and reports ErrLockContended — transient, so the replication retry replays the event once the local write settles. * remote: report the stamp a metadata update produced A metadata write can rewrite the object it touches — S3 changes Content-Encoding through an in-place copy, which can mint a new ETag — so restamping from a fresh stat can reject the very object the update produced, or describe a replacement that landed in between. UpdateFileMetadata now returns the RemoteEntry of the object it wrote (S3 from the copy result, GCS from the update attributes, Azure from a post-update stat); nil means the object was untouched and callers keep the stored stamp. * remote: restamp Azure from the metadata write's own etag
435 lines
17 KiB
Go
435 lines
17 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/remote_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/remote_storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
type lazyFetchContextKey struct{}
|
|
|
|
// lazyParentsContextKey marks a FindEntry issued while materializing missing
|
|
// parents: that fetch's own parent walk would only re-stat the same gaps.
|
|
type lazyParentsContextKey struct{}
|
|
|
|
// maybeLazyFetchFromRemote is called by FindEntry when the store returns no
|
|
// entry for p. If p is under a remote-storage mount, it stats the remote
|
|
// object, builds a filer Entry from the result, and persists it via
|
|
// CreateEntry together with its missing parent directories: remote.uncache
|
|
// and cache eviction find entries by walking down from the mount directory.
|
|
//
|
|
// On a CreateEntry failure after a successful StatFile the in-memory entry is
|
|
// still returned (availability over consistency); the singleflight key is
|
|
// forgotten so the next lookup retries the filer write.
|
|
//
|
|
// Returns nil without error when: p is not under a remote mount; the remote
|
|
// reports the object does not exist; or any other remote error occurs.
|
|
func (f *Filer) maybeLazyFetchFromRemote(ctx context.Context, p util.FullPath) (*Entry, error) {
|
|
// Prevent recursive invocation: CreateEntry calls FindEntry, which would
|
|
// re-enter this function and deadlock on the singleflight key.
|
|
if ctx.Value(lazyFetchContextKey{}) != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
if f.RemoteStorage == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(p)
|
|
if remoteLoc == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// A startup tombstone rebuild may still be replaying the meta log; wait
|
|
// for it so a pending delete cannot resurrect here.
|
|
if done := f.remoteTombstonesDone.Load(); done != nil {
|
|
select {
|
|
case <-*done:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
if f.isRemoteDeletionPending(ctx, p, mountDir) {
|
|
glog.V(2).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s deleted locally, remote delete pending", p)
|
|
return nil, nil
|
|
}
|
|
|
|
remoteConf, found := f.RemoteStorage.FindRemoteStorageConf(p)
|
|
if !found {
|
|
return nil, nil
|
|
}
|
|
|
|
relPath := strings.TrimPrefix(string(p), string(mountDir))
|
|
if relPath != "" && !strings.HasPrefix(relPath, "/") {
|
|
relPath = "/" + relPath
|
|
}
|
|
base := strings.TrimSuffix(remoteLoc.Path, "/")
|
|
remotePath := "/" + strings.TrimLeft(base+relPath, "/")
|
|
|
|
objectLoc := &remote_pb.RemoteStorageLocation{
|
|
Name: remoteLoc.Name,
|
|
Bucket: remoteLoc.Bucket,
|
|
Path: remotePath,
|
|
}
|
|
|
|
type lazyFetchResult struct {
|
|
entry *Entry
|
|
}
|
|
|
|
key := string(p)
|
|
val, err, _ := f.lazyFetchGroup.Do(key, func() (interface{}, error) {
|
|
buildCtx := context.WithoutCancel(ctx)
|
|
client, clientErr := f.buildRemoteStorageClient(buildCtx, remoteConf)
|
|
if clientErr != nil {
|
|
glog.V(1).InfofCtx(ctx, "maybeLazyFetchFromRemote: reject %s: %v", p, clientErr)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
remoteEntry, statErr := client.StatFile(objectLoc)
|
|
if statErr != nil {
|
|
if errors.Is(statErr, remote_storage.ErrRemoteObjectNotFound) {
|
|
glog.V(3).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s not found in remote", p)
|
|
} else {
|
|
glog.Warningf("maybeLazyFetchFromRemote: stat %s failed: %v", p, statErr)
|
|
}
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
if remoteEntry == nil {
|
|
glog.V(3).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s StatFile returned nil entry", p)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
|
|
mtime := time.Unix(remoteEntry.RemoteMtime, 0)
|
|
entry := &Entry{
|
|
FullPath: p,
|
|
Attr: Attr{
|
|
Mtime: mtime,
|
|
Crtime: mtime,
|
|
Mode: 0644,
|
|
FileSize: uint64(remoteEntry.RemoteSize),
|
|
},
|
|
Extended: MergeRemoteContentEncoding(remoteEntry, nil),
|
|
Remote: remoteEntry,
|
|
}
|
|
|
|
persistBaseCtx, cancelPersist := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancelPersist()
|
|
persistCtx := context.WithValue(persistBaseCtx, lazyFetchContextKey{}, true)
|
|
// A delete may have landed while StatFile was in flight; re-check so
|
|
// the fetched object cannot resurrect a path whose delete is pending.
|
|
if f.isRemoteDeletionPending(persistCtx, p, mountDir) {
|
|
glog.V(2).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s deleted during remote stat", p)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
if ctx.Value(lazyParentsContextKey{}) == nil {
|
|
f.fetchMissingRemoteParents(persistBaseCtx, mountDir, p)
|
|
}
|
|
saveErr := f.CreateEntry(persistCtx, entry, nil, false, false, nil, false, f.MaxFilenameLength)
|
|
if saveErr != nil {
|
|
glog.Warningf("maybeLazyFetchFromRemote: failed to persist filer entry for %s: %v", p, saveErr)
|
|
f.lazyFetchGroup.Forget(key)
|
|
return lazyFetchResult{entry}, nil
|
|
}
|
|
|
|
// A delete records its tombstone before removing the entry, so a
|
|
// tombstone visible now means the insert raced a delete that already
|
|
// ran: retract the persisted entry so the path stays deleted.
|
|
if f.isRemoteDeletionPending(persistCtx, p, mountDir) {
|
|
glog.V(2).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s deleted while persisting", p)
|
|
f.lazyFetchGroup.Forget(key)
|
|
f.retractLazyRemoteEntry(persistCtx, entry)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
|
|
return lazyFetchResult{entry}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, ok := val.(lazyFetchResult)
|
|
if !ok {
|
|
return nil, fmt.Errorf("maybeLazyFetchFromRemote: unexpected singleflight result type %T for %s", val, p)
|
|
}
|
|
return result.entry, nil
|
|
}
|
|
|
|
// fetchMissingRemoteParents looks up p's missing parents under mountDir, top
|
|
// down, through lazy fetch. A remote object stored at a parent's key is then
|
|
// materialized first, so creating p promotes it to a prefix object instead of
|
|
// hiding it behind a plain directory. Each missing parent costs one remote
|
|
// stat, once per directory.
|
|
func (f *Filer) fetchMissingRemoteParents(ctx context.Context, mountDir, p util.FullPath) {
|
|
var missing []util.FullPath
|
|
for dir, _ := p.DirAndName(); len(dir) > len(mountDir); dir, _ = util.FullPath(dir).DirAndName() {
|
|
if _, err := f.Store.FindEntry(ctx, util.FullPath(dir)); !errors.Is(err, filer_pb.ErrNotFound) {
|
|
break
|
|
}
|
|
missing = append(missing, util.FullPath(dir))
|
|
}
|
|
parentCtx := context.WithValue(ctx, lazyParentsContextKey{}, true)
|
|
for i := len(missing) - 1; i >= 0; i-- {
|
|
f.FindEntry(parentCtx, missing[i])
|
|
}
|
|
}
|
|
|
|
// retractLazyRemoteEntry deletes the entry at entry.FullPath only when it is
|
|
// still the entry a lazy remote read just materialized — a concurrent write
|
|
// may have replaced it, and deleting by path alone would take that write down.
|
|
func (f *Filer) retractLazyRemoteEntry(ctx context.Context, entry *Entry) {
|
|
existing, findErr := f.FindEntry(ctx, entry.FullPath)
|
|
if findErr != nil || existing == nil {
|
|
return
|
|
}
|
|
// The stored entry must still be exactly what this read materialized —
|
|
// an intervening write (appended chunks, touched attributes) means a
|
|
// real update owns the path now.
|
|
if !proto.Equal(existing.ToProtoEntry(), entry.ToProtoEntry()) {
|
|
return
|
|
}
|
|
if err := f.doDeleteEntryMetaAndData(ctx, existing, false, false, nil); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
|
glog.Warningf("retractLazyRemoteEntry %s: %v", entry.FullPath, err)
|
|
}
|
|
}
|
|
|
|
// remoteStorageObject resolves the client and object location entry maps to
|
|
// on its mounted remote storage; a nil client means the path is not mounted.
|
|
func (f *Filer) remoteStorageObject(ctx context.Context, entry *Entry) (remote_storage.RemoteStorageClient, *remote_pb.RemoteStorageLocation, error) {
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(entry.FullPath)
|
|
if remoteLoc == nil {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
remoteConf, found := f.RemoteStorage.GetRemoteStorageConf(remoteLoc.Name)
|
|
if !found {
|
|
return nil, nil, fmt.Errorf("resolve remote storage client for %s: not found", entry.FullPath)
|
|
}
|
|
client, clientErr := f.buildRemoteStorageClient(ctx, remoteConf)
|
|
if clientErr != nil {
|
|
return nil, nil, fmt.Errorf("resolve remote storage client for %s: %w", entry.FullPath, clientErr)
|
|
}
|
|
if client == nil {
|
|
return nil, nil, fmt.Errorf("resolve remote storage client for %s: initialization failed", entry.FullPath)
|
|
}
|
|
|
|
return client, MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, entry.FullPath), nil
|
|
}
|
|
|
|
func (f *Filer) maybeDeleteFromRemote(ctx context.Context, entry *Entry) (bool, error) {
|
|
if entry == nil || f.RemoteStorage == nil {
|
|
return false, nil
|
|
}
|
|
|
|
if !entry.IsDirectory() && entry.Remote == nil {
|
|
return false, nil
|
|
}
|
|
|
|
client, objectLoc, err := f.remoteStorageObject(ctx, entry)
|
|
if err != nil || client == nil {
|
|
return false, err
|
|
}
|
|
|
|
if entry.IsDirectory() {
|
|
if err := client.RemoveDirectory(objectLoc); err != nil {
|
|
if errors.Is(err, remote_storage.ErrRemoteObjectNotFound) {
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("remove remote directory %s: %w", entry.FullPath, err)
|
|
}
|
|
glog.V(3).InfofCtx(ctx, "maybeDeleteFromRemote: deleted directory %s from remote", entry.FullPath)
|
|
return true, nil
|
|
}
|
|
|
|
if err := client.DeleteFile(objectLoc); err != nil {
|
|
if errors.Is(err, remote_storage.ErrRemoteObjectNotFound) {
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("delete remote file %s: %w", entry.FullPath, err)
|
|
}
|
|
|
|
glog.V(3).InfofCtx(ctx, "maybeDeleteFromRemote: deleted %s from remote", entry.FullPath)
|
|
return true, nil
|
|
}
|
|
|
|
// deleteSweptEntryRemote frees the remote objects a swept entry still owns,
|
|
// mirroring the write-back daemon's delete guards (processDeleteEvent): the
|
|
// source a pending rename marker still reads goes first — nothing else
|
|
// releases it — then the object at the entry's own key, each only while it
|
|
// is still the object the entry's stamp describes.
|
|
func (f *Filer) deleteSweptEntryRemote(ctx context.Context, entry *Entry) error {
|
|
if entry == nil || f.RemoteStorage == nil || entry.Remote == nil {
|
|
return nil
|
|
}
|
|
client, objectLoc, err := f.remoteStorageObject(ctx, entry)
|
|
if err != nil || client == nil {
|
|
return err
|
|
}
|
|
if err := f.releasePendingRenameSource(ctx, client, entry); err != nil {
|
|
return err
|
|
}
|
|
return deleteRemoteFileIfUnchanged(ctx, client, objectLoc, entry.Remote)
|
|
}
|
|
|
|
// releasePendingRenameSource deletes the object a pending-rename marker on
|
|
// entry still reads: the destination copy never landed, so the source key is
|
|
// the content's only location and dropping the entry would orphan it. The
|
|
// source survives while a remote-only entry occupies its path — that entry's
|
|
// content is the same object. The delete binds to entry's stamp, which is
|
|
// the stamp of the source the marker reads. Mirrors the write-back daemon's
|
|
// releasePendingSource.
|
|
func (f *Filer) releasePendingRenameSource(ctx context.Context, client remote_storage.RemoteStorageClient, entry *Entry) error {
|
|
src := f.RemoteSourcePath(entry, entry.FullPath)
|
|
if src == entry.FullPath {
|
|
return nil
|
|
}
|
|
srcEntry, err := f.Store.FindEntry(ctx, src)
|
|
if err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
|
return fmt.Errorf("find rename source %s: %w", src, err)
|
|
}
|
|
if srcEntry != nil && len(srcEntry.Content) == 0 && srcEntry.IsInRemoteOnly() {
|
|
glog.V(0).InfofCtx(ctx, "keep remote source %s of pending rename of %s: a remote-only entry holds its path", src, entry.FullPath)
|
|
return nil
|
|
}
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(src)
|
|
if remoteLoc == nil {
|
|
return nil
|
|
}
|
|
return deleteRemoteFileIfUnchanged(ctx, client, MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, src), entry.Remote)
|
|
}
|
|
|
|
// deleteRemoteFileIfUnchanged deletes the remote object only while it is
|
|
// still the one stamp describes. Backends offering a conditional delete bind
|
|
// the check and the delete into one preconditioned request; the rest fall
|
|
// back to a guarded stat-then-delete.
|
|
func deleteRemoteFileIfUnchanged(ctx context.Context, client remote_storage.RemoteStorageClient, objectLoc *remote_pb.RemoteStorageLocation, stamp *filer_pb.RemoteEntry) error {
|
|
if deleter, ok := client.(remote_storage.RemoteStorageConditionalDeleter); ok {
|
|
deleted, delErr := deleter.DeleteFileIfUnchanged(objectLoc, stamp)
|
|
if delErr != nil {
|
|
return fmt.Errorf("delete remote file %s: %w", remote_storage.FormatLocation(objectLoc), delErr)
|
|
}
|
|
if !deleted {
|
|
glog.V(0).InfofCtx(ctx, "keep %s: the remote object changed since the delete", remote_storage.FormatLocation(objectLoc))
|
|
}
|
|
return nil
|
|
}
|
|
live, statErr := client.StatFile(objectLoc)
|
|
if statErr != nil {
|
|
if errors.Is(statErr, remote_storage.ErrRemoteObjectNotFound) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("stat remote file %s: %w", remote_storage.FormatLocation(objectLoc), statErr)
|
|
}
|
|
if !remote_storage.StillCurrent(stamp, live) {
|
|
glog.V(0).InfofCtx(ctx, "keep %s: the remote object changed since the delete", remote_storage.FormatLocation(objectLoc))
|
|
return nil
|
|
}
|
|
if err := client.DeleteFile(objectLoc); err != nil && !errors.Is(err, remote_storage.ErrRemoteObjectNotFound) {
|
|
return fmt.Errorf("delete remote file %s: %w", remote_storage.FormatLocation(objectLoc), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteFromRemote deletes the object, or for a directory every object, the
|
|
// entry maps to on its mounted remote storage.
|
|
func (f *Filer) DeleteFromRemote(ctx context.Context, entry *Entry) error {
|
|
_, err := f.maybeDeleteFromRemote(ctx, entry)
|
|
return err
|
|
}
|
|
|
|
// ExtRemoteRenameSourceKey marks a remote-only entry whose rename committed
|
|
// while its copy to the new key was still pending: it holds the path whose
|
|
// object remote-only reads must serve until the copy stamps the entry.
|
|
const ExtRemoteRenameSourceKey = "Seaweed-X-Remote-Rename-Source"
|
|
|
|
// RemoteSourcePath returns the path whose remote object holds the content of
|
|
// the remote-only entry at p: the renamed-from path a pending deferred copy
|
|
// still reads, or p itself. A marker leading out of p's mount is ignored, so
|
|
// whoever can set extended attributes cannot point reads at another mount.
|
|
func (f *Filer) RemoteSourcePath(entry *Entry, p util.FullPath) util.FullPath {
|
|
src := util.FullPath(entry.Extended[ExtRemoteRenameSourceKey])
|
|
if src == "" || f.RemoteStorage == nil {
|
|
return p
|
|
}
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(p)
|
|
if remoteLoc == nil {
|
|
return p
|
|
}
|
|
if srcMountDir, _ := f.RemoteStorage.FindMountDirectory(src); srcMountDir != mountDir {
|
|
return p
|
|
}
|
|
return src
|
|
}
|
|
|
|
// RemoteReadPath returns the dir and name whose object a remote-only read of
|
|
// the entry at dir/name serves.
|
|
func (f *Filer) RemoteReadPath(entry *Entry, dir, name string) (string, string) {
|
|
return f.RemoteSourcePath(entry, util.NewFullPath(dir, name)).DirAndName()
|
|
}
|
|
|
|
// CopyRemoteOnlyEntry copies the remote object of a renamed entry whose
|
|
// content exists only on the remote to the key of newPath, and returns the
|
|
// RemoteEntry of the copy. The rename deletes the old key along with the old
|
|
// entry, and with no local data nothing would write the new key. It returns
|
|
// nil when no copy is needed: the entry has local data, which the remote sync
|
|
// uploads to the new key, it is empty, which the sync writes as an empty
|
|
// object, or it is not under a remote mount. A rename that
|
|
// moves such an entry out of its mount, or onto a remote that cannot copy, is
|
|
// refused, since it would delete the only copy of the content.
|
|
func (f *Filer) CopyRemoteOnlyEntry(ctx context.Context, entry *Entry, oldPath, newPath util.FullPath) (*filer_pb.RemoteEntry, error) {
|
|
src, dst, copier, err := f.RemoteCopyLocations(ctx, entry, oldPath, newPath)
|
|
if err != nil || copier == nil {
|
|
return nil, err
|
|
}
|
|
remoteEntry, err := copier.CopyFile(src, dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("copy remote %s to %s: %w", remote_storage.FormatLocation(src), remote_storage.FormatLocation(dst), err)
|
|
}
|
|
glog.V(3).InfofCtx(ctx, "CopyRemoteOnlyEntry: copied %s to %s", remote_storage.FormatLocation(src), remote_storage.FormatLocation(dst))
|
|
return remoteEntry, nil
|
|
}
|
|
|
|
// RemoteCopyLocations resolves the object locations a remote-only rename copy
|
|
// of entry between oldPath and newPath would use, and the client able to copy
|
|
// it. A nil copier means the entry needs no copy; an error means it needs one
|
|
// that cannot be served, which a rename must refuse before committing.
|
|
func (f *Filer) RemoteCopyLocations(ctx context.Context, entry *Entry, oldPath, newPath util.FullPath) (src, dst *remote_pb.RemoteStorageLocation, copier remote_storage.RemoteStorageObjectCopier, err error) {
|
|
if f.RemoteStorage == nil || entry.IsDirectory() || !entry.IsInRemoteOnly() || len(entry.Content) > 0 {
|
|
return nil, nil, nil, nil
|
|
}
|
|
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(oldPath)
|
|
if remoteLoc == nil {
|
|
return nil, nil, nil, nil
|
|
}
|
|
if newMountDir, _ := f.RemoteStorage.FindMountDirectory(newPath); newMountDir != mountDir {
|
|
return nil, nil, nil, fmt.Errorf("%s exists only on the remote of mount %s and cannot be renamed out of it; cache it first", oldPath, mountDir)
|
|
}
|
|
|
|
remoteConf, found := f.RemoteStorage.GetRemoteStorageConf(remoteLoc.Name)
|
|
if !found {
|
|
return nil, nil, nil, fmt.Errorf("resolve remote storage client for %s: not found", oldPath)
|
|
}
|
|
client, clientErr := f.buildRemoteStorageClient(ctx, remoteConf)
|
|
if clientErr != nil {
|
|
return nil, nil, nil, fmt.Errorf("resolve remote storage client for %s: %w", oldPath, clientErr)
|
|
}
|
|
copier, ok := client.(remote_storage.RemoteStorageObjectCopier)
|
|
if !ok {
|
|
return nil, nil, nil, fmt.Errorf("%s exists only on the remote and remote storage type %s cannot copy it to the new name; cache it first", oldPath, remoteConf.Type)
|
|
}
|
|
|
|
src = MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, f.RemoteSourcePath(entry, oldPath))
|
|
dst = MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, newPath)
|
|
return src, dst, copier, nil
|
|
}
|