Seafile returns a content-addressed id (a git-like hash of the file
contents) rather than a unique per-object identifier. Two different
files with identical contents are therefore given the same id, even on
different servers.
operations.SameObject assumes IDs uniquely identify an object, so when
syncing between Seafile remotes it treated two distinct but
identical-content files as the same object. The guard in NeedTransfer
if Equal(ctx, src, dst) && !SameObject(src, dst)
then evaluated to false for unchanged files (Equal was true because the
sizes matched, and SameObject was true because the ids matched), so
rclone re-uploaded them on every sync.
Seafile has no modtime and no usable content hash, so dropping the IDer
implementation makes SameObject fall back to path comparison and lets
unchanged files be skipped correctly.
Fixes #8300
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
3.3 KiB
Go
119 lines
3.3 KiB
Go
package seafile
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
)
|
|
|
|
// Object describes a seafile object (also commonly called a file)
|
|
type Object struct {
|
|
fs *Fs // what this object is part of
|
|
remote string // The remote path (full path containing library name if target at root)
|
|
pathInLibrary string // Path of the object without the library name
|
|
size int64 // size of the object
|
|
modTime time.Time // modification time of the object
|
|
libraryID string // Needed to download the file
|
|
}
|
|
|
|
// ==================== Interface fs.DirEntry ====================
|
|
|
|
// Return a string version
|
|
func (o *Object) String() string {
|
|
if o == nil {
|
|
return "<nil>"
|
|
}
|
|
return o.remote
|
|
}
|
|
|
|
// Remote returns the remote string
|
|
func (o *Object) Remote() string {
|
|
return o.remote
|
|
}
|
|
|
|
// ModTime returns last modified time
|
|
func (o *Object) ModTime(context.Context) time.Time {
|
|
return o.modTime
|
|
}
|
|
|
|
// Size returns the size of an object in bytes
|
|
func (o *Object) Size() int64 {
|
|
return o.size
|
|
}
|
|
|
|
// ==================== Interface fs.ObjectInfo ====================
|
|
|
|
// Fs returns the parent Fs
|
|
func (o *Object) Fs() fs.Info {
|
|
return o.fs
|
|
}
|
|
|
|
// Hash returns the selected checksum of the file
|
|
// If no checksum is available it returns ""
|
|
func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) {
|
|
return "", hash.ErrUnsupported
|
|
}
|
|
|
|
// Storable says whether this object can be stored
|
|
func (o *Object) Storable() bool {
|
|
return true
|
|
}
|
|
|
|
// ==================== Interface fs.Object ====================
|
|
|
|
// SetModTime sets the metadata on the object to set the modification date
|
|
func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
|
|
return fs.ErrorCantSetModTime
|
|
}
|
|
|
|
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
|
downloadLink, err := o.fs.getDownloadLink(ctx, o.libraryID, o.pathInLibrary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader, err := o.fs.download(ctx, downloadLink, o.Size(), options...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reader, nil
|
|
}
|
|
|
|
// Update in to the object with the modTime given of the given size
|
|
//
|
|
// When called from outside an Fs by rclone, src.Size() will always be >= 0.
|
|
// But for unknown-sized objects (indicated by src.Size() == -1), Upload should either
|
|
// return an error or update the object properly (rather than e.g. calling panic).
|
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
|
// The upload sometimes return a temporary 500 error
|
|
// We cannot use the pacer to retry uploading the file as the upload link is single use only
|
|
for retry := 0; retry <= 3; retry++ {
|
|
uploadLink, err := o.fs.getUploadLink(ctx, o.libraryID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uploaded, err := o.fs.upload(ctx, in, uploadLink, o.pathInLibrary)
|
|
if err == ErrorInternalDuringUpload {
|
|
// This is a temporary error, try again with a new upload link
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Set the properties from the upload back to the object
|
|
o.size = uploaded.Size
|
|
|
|
return nil
|
|
}
|
|
return ErrorInternalDuringUpload
|
|
}
|
|
|
|
// Remove this object
|
|
func (o *Object) Remove(ctx context.Context) error {
|
|
return o.fs.deleteFile(ctx, o.libraryID, o.pathInLibrary)
|
|
}
|