2018-04-06 19:33:51 +00:00
|
|
|
// +build !plan9
|
2017-11-12 17:54:25 +00:00
|
|
|
|
|
|
|
package cache
|
|
|
|
|
|
|
|
import (
|
2019-06-17 08:34:30 +00:00
|
|
|
"context"
|
2017-11-12 17:54:25 +00:00
|
|
|
"io"
|
|
|
|
"path"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
"github.com/pkg/errors"
|
2019-07-28 17:47:38 +00:00
|
|
|
"github.com/rclone/rclone/fs"
|
|
|
|
"github.com/rclone/rclone/fs/hash"
|
|
|
|
"github.com/rclone/rclone/lib/readers"
|
2018-01-29 22:05:04 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
objectInCache = "Object"
|
|
|
|
objectPendingUpload = "TempObject"
|
2017-11-12 17:54:25 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Object is a generic file like object that stores basic information about it
|
|
|
|
type Object struct {
|
|
|
|
fs.Object `json:"-"`
|
|
|
|
|
2019-06-04 16:18:23 +00:00
|
|
|
ParentFs fs.Fs `json:"-"` // parent fs
|
|
|
|
CacheFs *Fs `json:"-"` // cache fs
|
|
|
|
Name string `json:"name"` // name of the directory
|
|
|
|
Dir string `json:"dir"` // abs path of the object
|
|
|
|
CacheModTime int64 `json:"modTime"` // modification or creation time - IsZero for unknown
|
|
|
|
CacheSize int64 `json:"size"` // size of directory and contents or -1 if unknown
|
|
|
|
CacheStorable bool `json:"storable"` // says whether this object can be stored
|
|
|
|
CacheType string `json:"cacheType"`
|
|
|
|
CacheTs time.Time `json:"cacheTs"`
|
|
|
|
cacheHashesMu sync.Mutex
|
2018-01-29 22:05:04 +00:00
|
|
|
CacheHashes map[hash.Type]string // all supported hashes cached
|
2017-11-12 17:54:25 +00:00
|
|
|
|
|
|
|
refreshMutex sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewObject builds one from a generic fs.Object
|
2018-01-29 22:05:04 +00:00
|
|
|
func NewObject(f *Fs, remote string) *Object {
|
2017-11-12 17:54:25 +00:00
|
|
|
fullRemote := path.Join(f.Root(), remote)
|
|
|
|
dir, name := path.Split(fullRemote)
|
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
cacheType := objectInCache
|
|
|
|
parentFs := f.UnWrap()
|
2018-05-14 17:06:57 +00:00
|
|
|
if f.opt.TempWritePath != "" {
|
2018-01-29 22:05:04 +00:00
|
|
|
_, err := f.cache.SearchPendingUpload(fullRemote)
|
|
|
|
if err == nil { // queued for upload
|
|
|
|
cacheType = objectPendingUpload
|
|
|
|
parentFs = f.tempFs
|
|
|
|
fs.Debugf(fullRemote, "pending upload found")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-12 17:54:25 +00:00
|
|
|
co := &Object{
|
2018-01-29 22:05:04 +00:00
|
|
|
ParentFs: parentFs,
|
2017-11-12 17:54:25 +00:00
|
|
|
CacheFs: f,
|
|
|
|
Name: cleanPath(name),
|
|
|
|
Dir: cleanPath(dir),
|
|
|
|
CacheModTime: time.Now().UnixNano(),
|
|
|
|
CacheSize: 0,
|
|
|
|
CacheStorable: false,
|
2018-01-29 22:05:04 +00:00
|
|
|
CacheType: cacheType,
|
2017-12-18 12:55:37 +00:00
|
|
|
CacheTs: time.Now(),
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
|
|
|
return co
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectFromOriginal builds one from a generic fs.Object
|
2019-06-17 08:34:30 +00:00
|
|
|
func ObjectFromOriginal(ctx context.Context, f *Fs, o fs.Object) *Object {
|
2017-11-12 17:54:25 +00:00
|
|
|
var co *Object
|
|
|
|
fullRemote := cleanPath(path.Join(f.Root(), o.Remote()))
|
|
|
|
dir, name := path.Split(fullRemote)
|
2018-01-29 22:05:04 +00:00
|
|
|
|
|
|
|
cacheType := objectInCache
|
|
|
|
parentFs := f.UnWrap()
|
2018-05-14 17:06:57 +00:00
|
|
|
if f.opt.TempWritePath != "" {
|
2018-01-29 22:05:04 +00:00
|
|
|
_, err := f.cache.SearchPendingUpload(fullRemote)
|
|
|
|
if err == nil { // queued for upload
|
|
|
|
cacheType = objectPendingUpload
|
|
|
|
parentFs = f.tempFs
|
|
|
|
fs.Debugf(fullRemote, "pending upload found")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-12 17:54:25 +00:00
|
|
|
co = &Object{
|
2018-01-29 22:05:04 +00:00
|
|
|
ParentFs: parentFs,
|
2017-11-12 17:54:25 +00:00
|
|
|
CacheFs: f,
|
|
|
|
Name: cleanPath(name),
|
|
|
|
Dir: cleanPath(dir),
|
2018-01-29 22:05:04 +00:00
|
|
|
CacheType: cacheType,
|
2017-12-18 12:55:37 +00:00
|
|
|
CacheTs: time.Now(),
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
2019-06-17 08:34:30 +00:00
|
|
|
co.updateData(ctx, o)
|
2017-11-12 17:54:25 +00:00
|
|
|
return co
|
|
|
|
}
|
|
|
|
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) updateData(ctx context.Context, source fs.Object) {
|
2017-11-12 17:54:25 +00:00
|
|
|
o.Object = source
|
2019-06-17 08:34:30 +00:00
|
|
|
o.CacheModTime = source.ModTime(ctx).UnixNano()
|
2017-11-12 17:54:25 +00:00
|
|
|
o.CacheSize = source.Size()
|
|
|
|
o.CacheStorable = source.Storable()
|
2017-12-18 12:55:37 +00:00
|
|
|
o.CacheTs = time.Now()
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Lock()
|
2018-01-29 22:05:04 +00:00
|
|
|
o.CacheHashes = make(map[hash.Type]string)
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Unlock()
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fs returns its FS info
|
|
|
|
func (o *Object) Fs() fs.Info {
|
|
|
|
return o.CacheFs
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a human friendly name for this object
|
|
|
|
func (o *Object) String() string {
|
|
|
|
if o == nil {
|
|
|
|
return "<nil>"
|
|
|
|
}
|
|
|
|
return o.Remote()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remote returns the remote path
|
|
|
|
func (o *Object) Remote() string {
|
|
|
|
p := path.Join(o.Dir, o.Name)
|
2018-01-29 22:05:04 +00:00
|
|
|
return o.CacheFs.cleanRootFromPath(p)
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// abs returns the absolute path to the object
|
|
|
|
func (o *Object) abs() string {
|
|
|
|
return path.Join(o.Dir, o.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ModTime returns the cached ModTime
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
|
|
|
_ = o.refresh(ctx)
|
2017-11-12 17:54:25 +00:00
|
|
|
return time.Unix(0, o.CacheModTime)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Size returns the cached Size
|
|
|
|
func (o *Object) Size() int64 {
|
2019-06-17 08:34:30 +00:00
|
|
|
_ = o.refresh(context.TODO())
|
2017-11-12 17:54:25 +00:00
|
|
|
return o.CacheSize
|
|
|
|
}
|
|
|
|
|
|
|
|
// Storable returns the cached Storable
|
|
|
|
func (o *Object) Storable() bool {
|
2019-06-17 08:34:30 +00:00
|
|
|
_ = o.refresh(context.TODO())
|
2017-11-12 17:54:25 +00:00
|
|
|
return o.CacheStorable
|
|
|
|
}
|
|
|
|
|
2018-03-08 20:03:34 +00:00
|
|
|
// refresh will check if the object info is expired and request the info from source if it is
|
|
|
|
// all these conditions must be true to ignore a refresh
|
|
|
|
// 1. cache ts didn't expire yet
|
|
|
|
// 2. is not pending a notification from the wrapped fs
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) refresh(ctx context.Context) error {
|
2018-03-08 20:03:34 +00:00
|
|
|
isNotified := o.CacheFs.isNotifiedRemote(o.Remote())
|
2018-05-14 17:06:57 +00:00
|
|
|
isExpired := time.Now().After(o.CacheTs.Add(time.Duration(o.CacheFs.opt.InfoAge)))
|
2018-03-08 20:03:34 +00:00
|
|
|
if !isExpired && !isNotified {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-06-17 08:34:30 +00:00
|
|
|
return o.refreshFromSource(ctx, true)
|
2018-03-08 20:03:34 +00:00
|
|
|
}
|
|
|
|
|
2017-11-12 17:54:25 +00:00
|
|
|
// refreshFromSource requests the original FS for the object in case it comes from a cached entry
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) refreshFromSource(ctx context.Context, force bool) error {
|
2017-11-12 17:54:25 +00:00
|
|
|
o.refreshMutex.Lock()
|
|
|
|
defer o.refreshMutex.Unlock()
|
2018-01-29 22:05:04 +00:00
|
|
|
var err error
|
|
|
|
var liveObject fs.Object
|
2017-11-12 17:54:25 +00:00
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
if o.Object != nil && !force {
|
2017-11-12 17:54:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
2018-01-29 22:05:04 +00:00
|
|
|
if o.isTempFile() {
|
2019-06-17 08:34:30 +00:00
|
|
|
liveObject, err = o.ParentFs.NewObject(ctx, o.Remote())
|
2018-01-29 22:05:04 +00:00
|
|
|
err = errors.Wrapf(err, "in parent fs %v", o.ParentFs)
|
|
|
|
} else {
|
2019-06-17 08:34:30 +00:00
|
|
|
liveObject, err = o.CacheFs.Fs.NewObject(ctx, o.Remote())
|
2018-01-29 22:05:04 +00:00
|
|
|
err = errors.Wrapf(err, "in cache fs %v", o.CacheFs.Fs)
|
|
|
|
}
|
2017-11-12 17:54:25 +00:00
|
|
|
if err != nil {
|
2018-01-29 22:05:04 +00:00
|
|
|
fs.Errorf(o, "error refreshing object in : %v", err)
|
2017-11-12 17:54:25 +00:00
|
|
|
return err
|
|
|
|
}
|
2019-06-17 08:34:30 +00:00
|
|
|
o.updateData(ctx, liveObject)
|
2017-11-12 17:54:25 +00:00
|
|
|
o.persist()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetModTime sets the ModTime of this object
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
|
|
|
|
if err := o.refreshFromSource(ctx, false); err != nil {
|
2017-11-12 17:54:25 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-06-17 08:34:30 +00:00
|
|
|
err := o.Object.SetModTime(ctx, t)
|
2017-11-12 17:54:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
o.CacheModTime = t.UnixNano()
|
|
|
|
o.persist()
|
2018-01-29 22:05:04 +00:00
|
|
|
fs.Debugf(o, "updated ModTime: %v", t)
|
2017-11-12 17:54:25 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open is used to request a specific part of the file using fs.RangeOption
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
2018-09-03 14:41:06 +00:00
|
|
|
var err error
|
|
|
|
|
|
|
|
if o.Object == nil {
|
2019-06-17 08:34:30 +00:00
|
|
|
err = o.refreshFromSource(ctx, true)
|
2018-09-03 14:41:06 +00:00
|
|
|
} else {
|
2019-06-17 08:34:30 +00:00
|
|
|
err = o.refresh(ctx)
|
2018-09-03 14:41:06 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
2017-11-12 17:54:25 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-06-17 08:34:30 +00:00
|
|
|
cacheReader := NewObjectHandle(ctx, o, o.CacheFs)
|
2018-01-27 10:07:17 +00:00
|
|
|
var offset, limit int64 = 0, -1
|
2017-11-12 17:54:25 +00:00
|
|
|
for _, option := range options {
|
|
|
|
switch x := option.(type) {
|
|
|
|
case *fs.SeekOption:
|
2018-01-27 10:07:17 +00:00
|
|
|
offset = x.Offset
|
2017-12-09 21:54:26 +00:00
|
|
|
case *fs.RangeOption:
|
2018-01-22 19:44:55 +00:00
|
|
|
offset, limit = x.Decode(o.Size())
|
2017-12-09 21:54:26 +00:00
|
|
|
}
|
2018-04-06 18:53:06 +00:00
|
|
|
_, err = cacheReader.Seek(offset, io.SeekStart)
|
2017-12-09 21:54:26 +00:00
|
|
|
if err != nil {
|
2018-01-22 19:44:55 +00:00
|
|
|
return nil, err
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-22 19:44:55 +00:00
|
|
|
return readers.NewLimitedReadCloser(cacheReader, limit), nil
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update will change the object data
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
|
|
|
if err := o.refreshFromSource(ctx, false); err != nil {
|
2017-11-12 17:54:25 +00:00
|
|
|
return err
|
|
|
|
}
|
2018-01-29 22:05:04 +00:00
|
|
|
// pause background uploads if active
|
2018-05-14 17:06:57 +00:00
|
|
|
if o.CacheFs.opt.TempWritePath != "" {
|
2018-01-29 22:05:04 +00:00
|
|
|
o.CacheFs.backgroundRunner.pause()
|
|
|
|
defer o.CacheFs.backgroundRunner.play()
|
|
|
|
// don't allow started uploads
|
|
|
|
if o.isTempFile() && o.tempFileStartedUpload() {
|
|
|
|
return errors.Errorf("%v is currently uploading, can't update", o)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fs.Debugf(o, "updating object contents with size %v", src.Size())
|
2017-11-12 17:54:25 +00:00
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
// FIXME use reliable upload
|
2019-06-17 08:34:30 +00:00
|
|
|
err := o.Object.Update(ctx, in, src, options...)
|
2017-11-12 17:54:25 +00:00
|
|
|
if err != nil {
|
|
|
|
fs.Errorf(o, "error updating source: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
// deleting cached chunks and info to be replaced with new ones
|
|
|
|
_ = o.CacheFs.cache.RemoveObject(o.abs())
|
2018-06-08 20:33:05 +00:00
|
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
|
|
o.CacheFs.notifyChangeUpstreamIfNeeded(o.Remote(), fs.EntryObject)
|
2018-01-29 22:05:04 +00:00
|
|
|
|
2019-06-17 08:34:30 +00:00
|
|
|
o.CacheModTime = src.ModTime(ctx).UnixNano()
|
2017-11-12 17:54:25 +00:00
|
|
|
o.CacheSize = src.Size()
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Lock()
|
2018-01-29 22:05:04 +00:00
|
|
|
o.CacheHashes = make(map[hash.Type]string)
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Unlock()
|
2018-01-29 22:05:04 +00:00
|
|
|
o.CacheTs = time.Now()
|
2017-11-12 17:54:25 +00:00
|
|
|
o.persist()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove deletes the object from both the cache and the source
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) Remove(ctx context.Context) error {
|
|
|
|
if err := o.refreshFromSource(ctx, false); err != nil {
|
2017-11-12 17:54:25 +00:00
|
|
|
return err
|
|
|
|
}
|
2018-01-29 22:05:04 +00:00
|
|
|
// pause background uploads if active
|
2018-05-14 17:06:57 +00:00
|
|
|
if o.CacheFs.opt.TempWritePath != "" {
|
2018-01-29 22:05:04 +00:00
|
|
|
o.CacheFs.backgroundRunner.pause()
|
|
|
|
defer o.CacheFs.backgroundRunner.play()
|
|
|
|
// don't allow started uploads
|
|
|
|
if o.isTempFile() && o.tempFileStartedUpload() {
|
|
|
|
return errors.Errorf("%v is currently uploading, can't delete", o)
|
|
|
|
}
|
|
|
|
}
|
2019-06-17 08:34:30 +00:00
|
|
|
err := o.Object.Remove(ctx)
|
2017-11-12 17:54:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
fs.Debugf(o, "removing object")
|
2017-11-12 17:54:25 +00:00
|
|
|
_ = o.CacheFs.cache.RemoveObject(o.abs())
|
2018-01-29 22:05:04 +00:00
|
|
|
_ = o.CacheFs.cache.removePendingUpload(o.abs())
|
2018-02-10 20:01:05 +00:00
|
|
|
parentCd := NewDirectory(o.CacheFs, cleanPath(path.Dir(o.Remote())))
|
|
|
|
_ = o.CacheFs.cache.ExpireDir(parentCd)
|
2018-03-08 20:03:34 +00:00
|
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
|
|
o.CacheFs.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
2018-01-29 22:05:04 +00:00
|
|
|
|
|
|
|
return nil
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Hash requests a hash of the object and stores in the cache
|
|
|
|
// since it might or might not be called, this is lazy loaded
|
2019-06-17 08:34:30 +00:00
|
|
|
func (o *Object) Hash(ctx context.Context, ht hash.Type) (string, error) {
|
|
|
|
_ = o.refresh(ctx)
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Lock()
|
2018-01-29 22:05:04 +00:00
|
|
|
if o.CacheHashes == nil {
|
|
|
|
o.CacheHashes = make(map[hash.Type]string)
|
2017-11-12 17:54:25 +00:00
|
|
|
}
|
2018-01-29 22:05:04 +00:00
|
|
|
cachedHash, found := o.CacheHashes[ht]
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Unlock()
|
2017-11-12 17:54:25 +00:00
|
|
|
if found {
|
|
|
|
return cachedHash, nil
|
|
|
|
}
|
2019-06-17 08:34:30 +00:00
|
|
|
if err := o.refreshFromSource(ctx, false); err != nil {
|
2017-11-12 17:54:25 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2019-06-17 08:34:30 +00:00
|
|
|
liveHash, err := o.Object.Hash(ctx, ht)
|
2017-11-12 17:54:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Lock()
|
2018-01-29 22:05:04 +00:00
|
|
|
o.CacheHashes[ht] = liveHash
|
2019-06-04 16:18:23 +00:00
|
|
|
o.cacheHashesMu.Unlock()
|
2017-11-12 17:54:25 +00:00
|
|
|
|
|
|
|
o.persist()
|
|
|
|
fs.Debugf(o, "object hash cached: %v", liveHash)
|
|
|
|
|
|
|
|
return liveHash, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// persist adds this object to the persistent cache
|
|
|
|
func (o *Object) persist() *Object {
|
|
|
|
err := o.CacheFs.cache.AddObject(o)
|
|
|
|
if err != nil {
|
|
|
|
fs.Errorf(o, "failed to cache object: %v", err)
|
|
|
|
}
|
|
|
|
return o
|
|
|
|
}
|
|
|
|
|
2018-01-29 22:05:04 +00:00
|
|
|
func (o *Object) isTempFile() bool {
|
|
|
|
_, err := o.CacheFs.cache.SearchPendingUpload(o.abs())
|
|
|
|
if err != nil {
|
|
|
|
o.CacheType = objectInCache
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
o.CacheType = objectPendingUpload
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Object) tempFileStartedUpload() bool {
|
|
|
|
started, err := o.CacheFs.cache.SearchPendingUpload(o.abs())
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return started
|
|
|
|
}
|
|
|
|
|
2018-07-26 10:53:46 +00:00
|
|
|
// UnWrap returns the Object that this Object is wrapping or
|
|
|
|
// nil if it isn't wrapping anything
|
|
|
|
func (o *Object) UnWrap() fs.Object {
|
|
|
|
return o.Object
|
|
|
|
}
|
|
|
|
|
2017-11-12 17:54:25 +00:00
|
|
|
var (
|
2018-07-26 10:53:46 +00:00
|
|
|
_ fs.Object = (*Object)(nil)
|
|
|
|
_ fs.ObjectUnWrapper = (*Object)(nil)
|
2017-11-12 17:54:25 +00:00
|
|
|
)
|