mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-15 11:00:51 +02:00
* tiering: stop a shared remote object being deleted while replicas still point at it A remote-tiered volume's .dat content lives only in one cloud object that all N replica .vif files point at. Deleting that object while destroying any one replica, or before a downloaded replica is durable, bricks the survivors. - volume.tier.move cleanup now deletes old replicas with keepRemoteData=true so surviving replicas keep the shared object. Document why the alreadyPlaced anchor needs no replica sync (same-object replicas are byte-identical). - VolumeTierMoveDatFromRemote now fsyncs the downloaded .dat, fsyncs the containing directory, trims the .vif (fsynced) and swaps to the local DiskFile BEFORE deleting the remote object, on both the keep-remote and delete paths. Only the final DeleteFile is gated by keep_remote_dat_file, so a keep-remote download leaves the replica served from local disk rather than the shared object, and a crash before delete merely leaks the object. - volume.tier.download keeps the shared object for every replica except the last, which deletes it. - s3 and rclone download paths fsync the .dat before close. * storage: swap the volume data backend under the data lock The tier-download swap closed v.DataBackend and assigned the new local DiskFile without holding dataFileAccessLock, racing concurrent reads/writes (use of a closed file / nil deref). Add an exported Volume.SwapDataBackend that performs the close-and-replace under the lock, and call it from the tier download. * server: skip directory fsync on Windows in the tier download path os.Open(dir).Sync() is unsupported on Windows and returns an error, which would fail VolumeTierMoveDatFromRemote entirely there. Skip the directory fsync on Windows, matching how the storage-side helper tolerates the unsupported case. * shell: make multi-replica tier.download resilient to already-local replicas If a multi-replica download is interrupted and retried, a replica made local in the prior attempt returns "already on local disk", which aborted the whole command and left the remaining remote replicas dangling. Treat that case as a skip-and-continue so a retry completes the rest. * server: assert downloaded .dat content, not just length, in the tier test A length-only check passes even if the bytes are corrupted; compare the full content of the local .dat against the original.
296 lines
7.2 KiB
Go
296 lines
7.2 KiB
Go
//go:build rclone
|
|
|
|
package rclone_backend
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
_ "github.com/rclone/rclone/backend/all"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/accounting"
|
|
"github.com/rclone/rclone/fs/config/configfile"
|
|
"github.com/rclone/rclone/fs/object"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func init() {
|
|
backend.BackendStorageFactories["rclone"] = &RcloneBackendFactory{}
|
|
configfile.Install()
|
|
}
|
|
|
|
type RcloneBackendFactory struct {
|
|
}
|
|
|
|
func (factory *RcloneBackendFactory) StorageType() backend.StorageType {
|
|
return "rclone"
|
|
}
|
|
|
|
func (factory *RcloneBackendFactory) BuildStorage(configuration backend.StringProperties, configPrefix string, id string) (backend.BackendStorage, error) {
|
|
return newRcloneBackendStorage(configuration, configPrefix, id)
|
|
}
|
|
|
|
type RcloneBackendStorage struct {
|
|
id string
|
|
remoteName string
|
|
keyTemplate *template.Template
|
|
keyTemplateText string
|
|
fs fs.Fs
|
|
}
|
|
|
|
func newRcloneBackendStorage(configuration backend.StringProperties, configPrefix string, id string) (s *RcloneBackendStorage, err error) {
|
|
s = &RcloneBackendStorage{}
|
|
s.id = id
|
|
s.remoteName = configuration.GetString(configPrefix + "remote_name")
|
|
s.keyTemplateText = configuration.GetString(configPrefix + "key_template")
|
|
s.keyTemplate, err = template.New("keyTemplate").Parse(s.keyTemplateText)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
ctx := context.TODO()
|
|
accounting.Start(ctx)
|
|
|
|
fsPath := fmt.Sprintf("%s:", s.remoteName)
|
|
s.fs, err = fs.NewFs(ctx, fsPath)
|
|
if err != nil {
|
|
glog.Errorf("failed to instantiate Rclone filesystem: %s", err)
|
|
return
|
|
}
|
|
|
|
glog.V(0).Infof("created backend storage rclone.%s for remote name %s", s.id, s.remoteName)
|
|
return
|
|
}
|
|
|
|
func (s *RcloneBackendStorage) ToProperties() map[string]string {
|
|
m := make(map[string]string)
|
|
m["remote_name"] = s.remoteName
|
|
if len(s.keyTemplateText) > 0 {
|
|
m["key_template"] = s.keyTemplateText
|
|
}
|
|
return m
|
|
}
|
|
|
|
func formatKey(key string, storage RcloneBackendStorage) (fKey string, err error) {
|
|
var b bytes.Buffer
|
|
if len(storage.keyTemplateText) == 0 {
|
|
fKey = key
|
|
} else {
|
|
err = storage.keyTemplate.Execute(&b, key)
|
|
if err == nil {
|
|
fKey = b.String()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *RcloneBackendStorage) NewStorageFile(key string, tierInfo *volume_server_pb.VolumeInfo) backend.BackendStorageFile {
|
|
f := &RcloneBackendStorageFile{
|
|
backendStorage: s,
|
|
key: key,
|
|
tierInfo: tierInfo,
|
|
}
|
|
|
|
return f
|
|
}
|
|
|
|
func (s *RcloneBackendStorage) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
|
|
randomUuid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return key, 0, err
|
|
}
|
|
key = randomUuid.String()
|
|
|
|
key, err = formatKey(key, *s)
|
|
if err != nil {
|
|
return key, 0, err
|
|
}
|
|
|
|
glog.V(1).Infof("copy dat file of %s to remote rclone.%s as %s", f.Name(), s.id, key)
|
|
|
|
util.Retry("upload via Rclone", func() error {
|
|
size, err = uploadViaRclone(s.fs, f.Name(), key, fn)
|
|
return err
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func uploadViaRclone(rfs fs.Fs, filename string, key string, fn func(progressed int64, percentage float32) error) (fileSize int64, err error) {
|
|
ctx := context.TODO()
|
|
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer file.Close()
|
|
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
info := object.NewStaticObjectInfo(key, stat.ModTime(), stat.Size(), true, nil, rfs)
|
|
|
|
tr := accounting.NewStats(ctx).NewTransfer(info, rfs)
|
|
defer tr.Done(ctx, err)
|
|
acc := tr.Account(ctx, file)
|
|
pr := ProgressReader{acc: acc, tr: tr, fn: fn}
|
|
|
|
obj, err := rfs.Put(ctx, &pr, info)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return obj.Size(), err
|
|
}
|
|
|
|
func (s *RcloneBackendStorage) DownloadFile(filename string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
|
|
glog.V(1).Infof("download dat file of %s from remote rclone.%s as %s", filename, s.id, key)
|
|
|
|
util.Retry("download via Rclone", func() error {
|
|
size, err = downloadViaRclone(s.fs, filename, key, fn)
|
|
return err
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
var createRcloneDownloadFile = func(filename string) (io.WriteCloser, error) {
|
|
return os.Create(filename)
|
|
}
|
|
|
|
func downloadViaRclone(fs fs.Fs, filename string, key string, fn func(progressed int64, percentage float32) error) (fileSize int64, err error) {
|
|
ctx := context.TODO()
|
|
|
|
obj, err := fs.NewObject(ctx, key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rc, err := obj.Open(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rc.Close()
|
|
|
|
file, err := createRcloneDownloadFile(filename)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
tr := accounting.NewStats(ctx).NewTransfer(obj, fs)
|
|
defer func() {
|
|
// fsync the .dat before closing so its content is durable before the caller
|
|
// trims the remote reference and deletes the shared remote object.
|
|
if syncer, ok := file.(interface{ Sync() error }); ok {
|
|
if syncErr := syncer.Sync(); err == nil && syncErr != nil {
|
|
err = syncErr
|
|
}
|
|
}
|
|
if closeErr := file.Close(); err == nil && closeErr != nil {
|
|
err = closeErr
|
|
}
|
|
tr.Done(ctx, err)
|
|
}()
|
|
acc := tr.Account(ctx, rc)
|
|
pr := ProgressReader{acc: acc, tr: tr, fn: fn}
|
|
|
|
written, err := io.Copy(file, &pr)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return written, nil
|
|
}
|
|
|
|
func (s *RcloneBackendStorage) DeleteFile(key string) (err error) {
|
|
glog.V(1).Infof("delete dat file %s from remote", key)
|
|
|
|
util.Retry("delete via Rclone", func() error {
|
|
err = deleteViaRclone(s.fs, key)
|
|
return err
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func deleteViaRclone(fs fs.Fs, key string) (err error) {
|
|
ctx := context.TODO()
|
|
|
|
obj, err := fs.NewObject(ctx, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return obj.Remove(ctx)
|
|
}
|
|
|
|
type RcloneBackendStorageFile struct {
|
|
backendStorage *RcloneBackendStorage
|
|
key string
|
|
tierInfo *volume_server_pb.VolumeInfo
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) ReadAt(p []byte, off int64) (n int, err error) {
|
|
ctx := context.TODO()
|
|
|
|
obj, err := rcloneBackendStorageFile.backendStorage.fs.NewObject(ctx, rcloneBackendStorageFile.key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
opt := fs.RangeOption{Start: off, End: off + int64(len(p)) - 1}
|
|
|
|
rc, err := obj.Open(ctx, &opt)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rc.Close()
|
|
|
|
return io.ReadFull(rc, p)
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) WriteAt(p []byte, off int64) (n int, err error) {
|
|
panic("not implemented")
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) Truncate(off int64) error {
|
|
panic("not implemented")
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) GetStat() (datSize int64, modTime time.Time, err error) {
|
|
files := rcloneBackendStorageFile.tierInfo.GetFiles()
|
|
|
|
if len(files) == 0 {
|
|
err = fmt.Errorf("remote file info not found")
|
|
return
|
|
}
|
|
|
|
datSize = int64(files[0].FileSize)
|
|
modTime = time.Unix(int64(files[0].ModifiedTime), 0)
|
|
|
|
return
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) Name() string {
|
|
return rcloneBackendStorageFile.key
|
|
}
|
|
|
|
func (rcloneBackendStorageFile RcloneBackendStorageFile) Sync() error {
|
|
return nil
|
|
}
|