diff --git a/backend/cache/cache.go b/backend/cache/cache.go index c0e7ec2ea..6abed89a9 100644 --- a/backend/cache/cache.go +++ b/backend/cache/cache.go @@ -1414,14 +1414,11 @@ func (f *Fs) CleanUp() error { } // About gets quota information from the Fs -func (f *Fs) About() error { - f.CleanUpCache(false) - +func (f *Fs) About() (*fs.Usage, error) { do := f.Fs.Features().About if do == nil { - return nil + return nil, errors.New("About not supported") } - return do() } diff --git a/backend/crypt/crypt.go b/backend/crypt/crypt.go index 6d575db24..cd4a009f0 100644 --- a/backend/crypt/crypt.go +++ b/backend/crypt/crypt.go @@ -452,6 +452,15 @@ func (f *Fs) CleanUp() error { return do() } +// About gets quota information from the Fs +func (f *Fs) About() (*fs.Usage, error) { + do := f.Fs.Features().About + if do == nil { + return nil, errors.New("About not supported") + } + return do() +} + // UnWrap returns the Fs that this Fs is wrapping func (f *Fs) UnWrap() fs.Fs { return f.Fs @@ -699,6 +708,7 @@ var ( _ fs.CleanUpper = (*Fs)(nil) _ fs.UnWrapper = (*Fs)(nil) _ fs.ListRer = (*Fs)(nil) + _ fs.Abouter = (*Fs)(nil) _ fs.ObjectInfo = (*ObjectInfo)(nil) _ fs.Object = (*Object)(nil) _ fs.ObjectUnWrapper = (*Object)(nil) diff --git a/backend/drive/drive.go b/backend/drive/drive.go index 94e8e19ae..c5f05357c 100644 --- a/backend/drive/drive.go +++ b/backend/drive/drive.go @@ -1051,7 +1051,7 @@ func (f *Fs) CleanUp() error { } // About gets quota information -func (f *Fs) About() error { +func (f *Fs) About() (*fs.Usage, error) { var about *drive.About var err error err = f.pacer.Call(func() (bool, error) { @@ -1059,18 +1059,19 @@ func (f *Fs) About() error { return shouldRetry(err) }) if err != nil { - fs.Errorf(f, "Failed to get Drive storageQuota: %v", err) - return nil + return nil, errors.Wrap(err, "failed to get Drive storageQuota") } - quota := float64(about.StorageQuota.Limit) / (1 << 30) - usagetotal := float64(about.StorageQuota.Usage) / (1 << 30) - usagedrive := float64(about.StorageQuota.UsageInDrive) / (1 << 30) - usagetrash := float64(about.StorageQuota.UsageInDriveTrash) / (1 << 30) - fmt.Printf("Quota: %.0f GiB | Used: %.1f GiB (Trash: %.1f GiB) | Available: %.1f GiB | Usage: %d%%\n", - quota, usagedrive, usagetrash, quota-usagedrive, int((usagedrive/quota)*100)) - fmt.Printf("Space used in other Google services (such as Gmail): %.2f GiB\n", - usagetotal-usagedrive) - return nil + q := about.StorageQuota + usage := &fs.Usage{ + Used: fs.NewUsageValue(q.UsageInDrive), // bytes in use + Trashed: fs.NewUsageValue(q.UsageInDriveTrash), // bytes in trash + Other: fs.NewUsageValue(q.Usage - q.UsageInDrive), // other usage eg gmail in drive + } + if q.Limit > 0 { + usage.Total = fs.NewUsageValue(q.Limit) // quota of bytes that can be used + usage.Free = fs.NewUsageValue(q.Limit - q.Usage) // bytes which can be uploaded before reaching the quota + } + return usage, nil } // Move src to this remote using server side move operations. @@ -1664,6 +1665,7 @@ var ( _ fs.PutUncheckeder = (*Fs)(nil) _ fs.PublicLinker = (*Fs)(nil) _ fs.MergeDirser = (*Fs)(nil) + _ fs.Abouter = (*Fs)(nil) _ fs.Object = (*Object)(nil) - _ fs.MimeTyper = &Object{} + _ fs.MimeTyper = (*Object)(nil) ) diff --git a/backend/dropbox/dropbox.go b/backend/dropbox/dropbox.go index 3453da9a7..9ba148971 100644 --- a/backend/dropbox/dropbox.go +++ b/backend/dropbox/dropbox.go @@ -33,6 +33,7 @@ import ( "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files" "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/sharing" + "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/users" "github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs/config" "github.com/ncw/rclone/fs/config/flags" @@ -128,6 +129,7 @@ type Fs struct { features *fs.Features // optional features srv files.Client // the connection to the dropbox server sharingClient sharing.Client // as above, but for generating sharing links + users users.Client // as above, but for accessing user information slashRoot string // root with "/" prefix, lowercase slashRootSlash string // root with "/" prefix and postfix, lowercase pacer *pacer.Pacer // To pace the API calls @@ -209,11 +211,13 @@ func NewFs(name, root string) (fs.Fs, error) { } srv := files.New(config) sharingClient := sharing.New(config) + users := users.New(config) f := &Fs{ name: name, srv: srv, sharingClient: sharingClient, + users: users, pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant), } f.features = (&fs.Features{ @@ -727,6 +731,33 @@ func (f *Fs) DirMove(src fs.Fs, srcRemote, dstRemote string) error { return nil } +// About gets quota information +func (f *Fs) About() (usage *fs.Usage, err error) { + var q *users.SpaceUsage + err = f.pacer.Call(func() (bool, error) { + q, err = f.users.GetSpaceUsage() + return shouldRetry(err) + }) + if err != nil { + return nil, errors.Wrap(err, "about failed") + } + var total uint64 + if q.Allocation != nil { + if q.Allocation.Individual != nil { + total += q.Allocation.Individual.Allocated + } + if q.Allocation.Team != nil { + total += q.Allocation.Team.Allocated + } + } + usage = &fs.Usage{ + Total: fs.NewUsageValue(int64(total)), // quota of bytes that can be used + Used: fs.NewUsageValue(int64(q.Used)), // bytes in use + Free: fs.NewUsageValue(int64(total - q.Used)), // bytes which can be uploaded before reaching the quota + } + return usage, nil +} + // Hashes returns the supported hash sets. func (f *Fs) Hashes() hash.Set { return hash.Set(hash.Dropbox) @@ -1012,5 +1043,6 @@ var ( _ fs.Mover = (*Fs)(nil) _ fs.PublicLinker = (*Fs)(nil) _ fs.DirMover = (*Fs)(nil) + _ fs.Abouter = (*Fs)(nil) _ fs.Object = (*Object)(nil) ) diff --git a/backend/local/about_unix.go b/backend/local/about_unix.go new file mode 100644 index 000000000..745f2e559 --- /dev/null +++ b/backend/local/about_unix.go @@ -0,0 +1,29 @@ +// +build darwin dragonfly freebsd linux + +package local + +import ( + "syscall" + + "github.com/ncw/rclone/fs" + "github.com/pkg/errors" +) + +// About gets quota information +func (f *Fs) About() (*fs.Usage, error) { + var s syscall.Statfs_t + err := syscall.Statfs(f.root, &s) + if err != nil { + return nil, errors.Wrap(err, "failed to read disk usage") + } + bs := int64(s.Bsize) + usage := &fs.Usage{ + Total: fs.NewUsageValue(bs * int64(s.Blocks)), // quota of bytes that can be used + Used: fs.NewUsageValue(bs * int64(s.Blocks-s.Bfree)), // bytes in use + Free: fs.NewUsageValue(bs * int64(s.Bavail)), // bytes which can be uploaded before reaching the quota + } + return usage, nil +} + +// check interface +var _ fs.Abouter = &Fs{} diff --git a/backend/local/about_windows.go b/backend/local/about_windows.go new file mode 100644 index 000000000..4c9dcec3b --- /dev/null +++ b/backend/local/about_windows.go @@ -0,0 +1,36 @@ +// +build windows + +package local + +import ( + "syscall" + "unsafe" + + "github.com/ncw/rclone/fs" + "github.com/pkg/errors" +) + +var getFreeDiskSpace = syscall.NewLazyDLL("kernel32.dll").NewProc("GetDiskFreeSpaceExW") + +// About gets quota information +func (f *Fs) About() (*fs.Usage, error) { + var available, total, free int64 + _, _, e1 := getFreeDiskSpace.Call( + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(f.root))), + uintptr(unsafe.Pointer(&available)), // lpFreeBytesAvailable - for this user + uintptr(unsafe.Pointer(&total)), // lpTotalNumberOfBytes + uintptr(unsafe.Pointer(&free)), // lpTotalNumberOfFreeBytes + ) + if e1 != syscall.Errno(0) { + return nil, errors.Wrap(e1, "failed to read disk usage") + } + usage := &fs.Usage{ + Total: fs.NewUsageValue(total), // quota of bytes that can be used + Used: fs.NewUsageValue(total - free), // bytes in use + Free: fs.NewUsageValue(available), // bytes which can be uploaded before reaching the quota + } + return usage, nil +} + +// check interface +var _ fs.Abouter = &Fs{} diff --git a/backend/onedrive/api/types.go b/backend/onedrive/api/types.go index b305ee011..82ccbe369 100644 --- a/backend/onedrive/api/types.go +++ b/backend/onedrive/api/types.go @@ -50,10 +50,10 @@ type IdentitySet struct { // Quota groups storage space quota-related information on OneDrive into a single structure. type Quota struct { - Total int `json:"total"` - Used int `json:"used"` - Remaining int `json:"remaining"` - Deleted int `json:"deleted"` + Total int64 `json:"total"` + Used int64 `json:"used"` + Remaining int64 `json:"remaining"` + Deleted int64 `json:"deleted"` State string `json:"state"` // normal | nearing | critical | exceeded } diff --git a/backend/onedrive/onedrive.go b/backend/onedrive/onedrive.go index d97886f5c..51684a1a8 100644 --- a/backend/onedrive/onedrive.go +++ b/backend/onedrive/onedrive.go @@ -922,6 +922,31 @@ func (f *Fs) DirCacheFlush() { f.dirCache.ResetRoot() } +// About gets quota information +func (f *Fs) About() (usage *fs.Usage, err error) { + var drive api.Drive + opts := rest.Opts{ + Method: "GET", + Path: "", + } + var resp *http.Response + err = f.pacer.Call(func() (bool, error) { + resp, err = f.srv.CallJSON(&opts, nil, &drive) + return shouldRetry(resp, err) + }) + if err != nil { + return nil, errors.Wrap(err, "about failed") + } + q := drive.Quota + usage = &fs.Usage{ + Total: fs.NewUsageValue(q.Total), // quota of bytes that can be used + Used: fs.NewUsageValue(q.Used), // bytes in use + Trashed: fs.NewUsageValue(q.Deleted), // bytes in trash + Free: fs.NewUsageValue(q.Remaining), // bytes which can be uploaded before reaching the quota + } + return usage, nil +} + // Hashes returns the supported hash sets. func (f *Fs) Hashes() hash.Set { return hash.Set(hash.SHA1) @@ -1273,6 +1298,7 @@ var ( _ fs.Mover = (*Fs)(nil) // _ fs.DirMover = (*Fs)(nil) _ fs.DirCacheFlusher = (*Fs)(nil) + _ fs.Abouter = (*Fs)(nil) _ fs.Object = (*Object)(nil) _ fs.MimeTyper = &Object{} ) diff --git a/backend/pcloud/api/types.go b/backend/pcloud/api/types.go index b74d916c3..408be792e 100644 --- a/backend/pcloud/api/types.go +++ b/backend/pcloud/api/types.go @@ -151,3 +151,35 @@ type ChecksumFileResult struct { Hashes Metadata Item `json:"metadata"` } + +// UserInfo is returned from /userinfo +type UserInfo struct { + Error + Cryptosetup bool `json:"cryptosetup"` + Plan int `json:"plan"` + CryptoSubscription bool `json:"cryptosubscription"` + PublicLinkQuota int64 `json:"publiclinkquota"` + Email string `json:"email"` + UserID int `json:"userid"` + Result int `json:"result"` + Quota int64 `json:"quota"` + TrashRevretentionDays int `json:"trashrevretentiondays"` + Premium bool `json:"premium"` + PremiumLifetime bool `json:"premiumlifetime"` + EmailVerified bool `json:"emailverified"` + UsedQuota int64 `json:"usedquota"` + Language string `json:"language"` + Business bool `json:"business"` + CryptoLifetime bool `json:"cryptolifetime"` + Registered string `json:"registered"` + Journey struct { + Claimed bool `json:"claimed"` + Steps struct { + VerifyMail bool `json:"verifymail"` + UploadFile bool `json:"uploadfile"` + AutoUpload bool `json:"autoupload"` + DownloadApp bool `json:"downloadapp"` + DownloadDrive bool `json:"downloaddrive"` + } `json:"steps"` + } `json:"journey"` +} diff --git a/backend/pcloud/pcloud.go b/backend/pcloud/pcloud.go index c9614a910..e45c0f4e2 100644 --- a/backend/pcloud/pcloud.go +++ b/backend/pcloud/pcloud.go @@ -806,6 +806,30 @@ func (f *Fs) DirCacheFlush() { f.dirCache.ResetRoot() } +// About gets quota information +func (f *Fs) About() (usage *fs.Usage, err error) { + opts := rest.Opts{ + Method: "POST", + Path: "/userinfo", + } + var resp *http.Response + var q api.UserInfo + err = f.pacer.Call(func() (bool, error) { + resp, err = f.srv.CallJSON(&opts, nil, &q) + err = q.Error.Update(err) + return shouldRetry(resp, err) + }) + if err != nil { + return nil, errors.Wrap(err, "about failed") + } + usage = &fs.Usage{ + Total: fs.NewUsageValue(q.Quota), // quota of bytes that can be used + Used: fs.NewUsageValue(q.UsedQuota), // bytes in use + Free: fs.NewUsageValue(q.Quota - q.UsedQuota), // bytes which can be uploaded before reaching the quota + } + return usage, nil +} + // Hashes returns the supported hash sets. func (f *Fs) Hashes() hash.Set { return hash.Set(hash.MD5 | hash.SHA1) @@ -1107,5 +1131,6 @@ var ( _ fs.Mover = (*Fs)(nil) _ fs.DirMover = (*Fs)(nil) _ fs.DirCacheFlusher = (*Fs)(nil) + _ fs.Abouter = (*Fs)(nil) _ fs.Object = (*Object)(nil) ) diff --git a/backend/swift/swift.go b/backend/swift/swift.go index 5a5763438..880037de1 100644 --- a/backend/swift/swift.go +++ b/backend/swift/swift.go @@ -498,6 +498,24 @@ func (f *Fs) ListR(dir string, callback fs.ListRCallback) (err error) { return list.Flush() } +// About gets quota information +func (f *Fs) About() (*fs.Usage, error) { + containers, err := f.c.ContainersAll(nil) + if err != nil { + return nil, errors.Wrap(err, "container listing failed") + } + var total, objects int64 + for _, c := range containers { + total += c.Bytes + objects += c.Count + } + usage := &fs.Usage{ + Used: fs.NewUsageValue(total), // bytes in use + Objects: fs.NewUsageValue(objects), // objects in use + } + return usage, nil +} + // Put the object into the container // // Copy the reader in to the new object which is returned diff --git a/cmd/about/about.go b/cmd/about/about.go index b5c58358a..4f2d5e270 100644 --- a/cmd/about/about.go +++ b/cmd/about/about.go @@ -1,27 +1,112 @@ package about import ( + "encoding/json" + "fmt" + "os" + "github.com/ncw/rclone/cmd" - "github.com/ncw/rclone/fs/operations" + "github.com/ncw/rclone/fs" + "github.com/pkg/errors" "github.com/spf13/cobra" ) +var ( + jsonOutput bool + fullOutput bool +) + func init() { - cmd.Root.AddCommand(commandDefintion) + cmd.Root.AddCommand(commandDefinition) + commandDefinition.Flags().BoolVar(&jsonOutput, "json", false, "Format output as JSON") + commandDefinition.Flags().BoolVar(&fullOutput, "full", false, "Full numbers instead of SI units") } -var commandDefintion = &cobra.Command{ +// printValue formats uv to be output +func printValue(what string, uv *int64) { + what += ":" + if uv == nil { + return + } + var val string + if fullOutput { + val = fmt.Sprintf("%d", *uv) + } else { + val = fs.SizeSuffix(*uv).String() + } + fmt.Printf("%-9s%v\n", what, val) +} + +var commandDefinition = &cobra.Command{ Use: "about remote:", Short: `Get quota information from the remote.`, Long: ` Get quota information from the remote, like bytes used/free/quota and bytes used in the trash. Not supported by all remotes. + +This will print to stdout something like this: + + Total: 17G + Used: 7.444G + Free: 1.315G + Trashed: 100.000M + Other: 8.241G + +Where the fields are: + + * Total: total size available. + * Used: total size used + * Free: total amount this user could upload. + * Trashed: total amount in the trash + * Other: total amount in other storage (eg Gmail, Google Photos) + * Objects: total number of objects in the storage + +Note that not all the backends provide all the fields - they will be +missing if they are not known for that backend. Where it is known +that the value is unlimited the value will also be omitted. + +Use the --full flag to see the numbers written out in full, eg + + Total: 18253611008 + Used: 7993453766 + Free: 1411001220 + Trashed: 104857602 + Other: 8849156022 + +Use the --json flag for a computer readable output, eg + + { + "total": 18253611008, + "used": 7993453766, + "trashed": 104857602, + "other": 8849156022, + "free": 1411001220 + } `, Run: func(command *cobra.Command, args []string) { cmd.CheckArgs(1, 1, command, args) - fsrc := cmd.NewFsSrc(args) + f := cmd.NewFsSrc(args) cmd.Run(true, false, command, func() error { - return operations.About(fsrc) + doAbout := f.Features().About + if doAbout == nil { + return errors.Errorf("%v doesn't support about", f) + } + u, err := doAbout() + if err != nil { + return errors.Wrap(err, "About call failed") + } + if jsonOutput { + out := json.NewEncoder(os.Stdout) + out.SetIndent("", "\t") + return out.Encode(u) + } + printValue("Total", u.Total) + printValue("Used", u.Used) + printValue("Free", u.Free) + printValue("Trashed", u.Trashed) + printValue("Other", u.Other) + printValue("Objects", u.Objects) + return nil }) }, } diff --git a/docs/content/overview.md b/docs/content/overview.md index 67e3458cd..7c3ee111f 100644 --- a/docs/content/overview.md +++ b/docs/content/overview.md @@ -125,21 +125,21 @@ operations more efficient. | Amazon S3 | No | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | Backblaze B2 | No | No | No | No | Yes | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | Box | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | -| Dropbox | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | Yes | No | +| Dropbox | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | Yes | Yes | | FTP | No | No | Yes | Yes | No | No | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | Google Cloud Storage | Yes | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | Google Drive | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | | HTTP | No | No | No | No | No | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | -| Hubic | Yes † | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | +| Hubic | Yes † | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | Yes | | Microsoft Azure Blob Storage | Yes | Yes | No | No | No | Yes | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | -| Microsoft OneDrive | Yes | Yes | Yes | No [#197](https://github.com/ncw/rclone/issues/197) | No [#575](https://github.com/ncw/rclone/issues/575) | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | -| Openstack Swift | Yes † | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | -| pCloud | Yes | Yes | Yes | Yes | Yes | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | +| Microsoft OneDrive | Yes | Yes | Yes | No [#197](https://github.com/ncw/rclone/issues/197) | No [#575](https://github.com/ncw/rclone/issues/575) | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | Yes | +| Openstack Swift | Yes † | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | Yes | +| pCloud | Yes | Yes | Yes | Yes | Yes | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | Yes | | QingStor | No | Yes | No | No | No | Yes | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | SFTP | No | No | Yes | Yes | No | No | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | WebDAV | Yes | Yes | Yes | Yes | No | No | Yes ‡ | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | | Yandex Disk | Yes | No | No | No | Yes | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | No | -| The local filesystem | Yes | No | Yes | Yes | No | No | Yes | No | No | +| The local filesystem | Yes | No | Yes | Yes | No | No | Yes | No | Yes | ### Purge ### diff --git a/fs/fs.go b/fs/fs.go index 63797f989..7a67faae6 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -262,6 +262,25 @@ type ListRCallback func(entries DirEntries) error // ListRFn is defines the call used to recursively list a directory type ListRFn func(dir string, callback ListRCallback) error +// NewUsageValue makes a valid value +func NewUsageValue(value int64) *int64 { + p := new(int64) + *p = value + return p +} + +// Usage is returned by the About call +// +// If a value is nil then it isn't supported by that backend +type Usage struct { + Total *int64 `json:"total,omitempty"` // quota of bytes that can be used + Used *int64 `json:"used,omitempty"` // bytes in use + Trashed *int64 `json:"trashed,omitempty"` // bytes in trash + Other *int64 `json:"other,omitempty"` // other usage eg gmail in drive + Free *int64 `json:"free,omitempty"` // bytes which can be uploaded before reaching the quota + Objects *int64 `json:"objects,omitempty"` // objects in the storage system +} + // Features describe the optional features of the Fs type Features struct { // Feature flags, whether Fs @@ -378,8 +397,8 @@ type Features struct { // of listing recursively that doing a directory traversal. ListR ListRFn - // Get quota information from the Fs - About func() error + // About gets quota information from the Fs + About func() (*Usage, error) } // Disable nil's out the named feature. If it isn't found then it @@ -718,7 +737,7 @@ type RangeSeeker interface { // Abouter is an optional interface for Fs type Abouter interface { // About gets quota information from the Fs - About() error + About() (*Usage, error) } // ObjectsChan is a channel of Objects diff --git a/fs/operations/operations.go b/fs/operations/operations.go index c0f5f4457..6882e06c9 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -1049,19 +1049,6 @@ func CleanUp(f fs.Fs) error { return doCleanUp() } -// About gets quota information from the remote -func About(f fs.Fs) error { - doAbout := f.Features().About - if doAbout == nil { - return errors.Errorf("%v doesn't support about", f) - } - if fs.Config.DryRun { - fs.Logf(f, "Not running about as --dry-run set") - return nil - } - return doAbout() -} - // wrap a Reader and a Closer together into a ReadCloser type readCloser struct { io.Reader diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go index dc32a80f8..304bba178 100644 --- a/fstest/fstests/fstests.go +++ b/fstest/fstests/fstests.go @@ -1079,6 +1079,23 @@ func Run(t *testing.T, opt *Opt) { file.Check(t, obj, remote.Precision()) }) + // TestAbout tests the About optional interface + t.Run("TestObjectAbout", func(t *testing.T) { + skipIfNotOk(t) + + // Check have About + doAbout := remote.Features().About + if doAbout == nil { + t.Skip("FS does not support About") + } + + // Can't really check the output much! + usage, err := doAbout() + require.NoError(t, err) + require.NotNil(t, usage) + assert.NotEqual(t, int64(0), usage.Total) + }) + // TestObjectPurge tests Purge t.Run("TestObjectPurge", func(t *testing.T) { skipIfNotOk(t)