From 454dfd3c9e275e51b18975ca9a33030382c3f697 Mon Sep 17 00:00:00 2001
From: Nick Craig-Wood <nick@craig-wood.com>
Date: Sat, 8 Jun 2019 09:19:07 +0100
Subject: [PATCH] rc: Add operations/fsinfo: Return information about the
 remote

This returns a information about the remote including Name, Root,
Hashes and optional Features.
---
 fs/operations/operations.go      | 37 +++++++++++++++
 fs/operations/operations_test.go | 20 ++++++++
 fs/operations/rc.go              | 81 +++++++++++++++++++++++++++++++-
 fs/operations/rc_test.go         | 27 +++++++++++
 4 files changed, 164 insertions(+), 1 deletion(-)

diff --git a/fs/operations/operations.go b/fs/operations/operations.go
index a142140a2..5d15e80c6 100644
--- a/fs/operations/operations.go
+++ b/fs/operations/operations.go
@@ -1750,3 +1750,40 @@ func DirMove(f fs.Fs, srcRemote, dstRemote string) (err error) {
 
 	return nil
 }
+
+// FsInfo provides information about a remote
+type FsInfo struct {
+	// Name of the remote (as passed into NewFs)
+	Name string
+
+	// Root of the remote (as passed into NewFs)
+	Root string
+
+	// String returns a description of the FS
+	String string
+
+	// Precision of the ModTimes in this Fs in Nanoseconds
+	Precision time.Duration
+
+	// Returns the supported hash types of the filesystem
+	Hashes []string
+
+	// Features returns the optional features of this Fs
+	Features map[string]bool
+}
+
+// GetFsInfo gets the information (FsInfo) about a given Fs
+func GetFsInfo(f fs.Fs) *FsInfo {
+	info := &FsInfo{
+		Name:      f.Name(),
+		Root:      f.Root(),
+		String:    f.String(),
+		Precision: f.Precision(),
+		Hashes:    make([]string, 0, 4),
+		Features:  f.Features().Enabled(),
+	}
+	for _, hashType := range f.Hashes().Array() {
+		info.Hashes = append(info.Hashes, hashType.String())
+	}
+	return info
+}
diff --git a/fs/operations/operations_test.go b/fs/operations/operations_test.go
index 09cc60c64..507af4d5a 100644
--- a/fs/operations/operations_test.go
+++ b/fs/operations/operations_test.go
@@ -1182,3 +1182,23 @@ func TestDirMove(t *testing.T) {
 	)
 
 }
+
+func TestGetFsInfo(t *testing.T) {
+	r := fstest.NewRun(t)
+	defer r.Finalise()
+
+	f := r.Fremote
+	info := operations.GetFsInfo(f)
+	assert.Equal(t, f.Name(), info.Name)
+	assert.Equal(t, f.Root(), info.Root)
+	assert.Equal(t, f.String(), info.String)
+	assert.Equal(t, f.Precision(), info.Precision)
+	hashSet := hash.NewHashSet()
+	for _, hashName := range info.Hashes {
+		var ht hash.Type
+		require.NoError(t, ht.Set(hashName))
+		hashSet.Add(ht)
+	}
+	assert.Equal(t, f.Hashes(), hashSet)
+	assert.Equal(t, f.Features().Enabled(), info.Features)
+}
diff --git a/fs/operations/rc.go b/fs/operations/rc.go
index d5d3fcc4f..351f7cccc 100644
--- a/fs/operations/rc.go
+++ b/fs/operations/rc.go
@@ -68,9 +68,10 @@ func init() {
 		Help: `This takes the following parameters
 
 - fs - a remote name string eg "drive:"
-- remote - a path within that remote eg "dir"
 
 The result is as returned from rclone about --json
+
+See the [about command](/commands/rclone_size/) command for more information on the above.
 `,
 	})
 }
@@ -290,3 +291,81 @@ func rcPublicLink(in rc.Params) (out rc.Params, err error) {
 	out["url"] = url
 	return out, nil
 }
+
+func init() {
+	rc.Add(rc.Call{
+		Path:  "operations/fsinfo",
+		Fn:    rcFsInfo,
+		Title: "Return information about the remote",
+		Help: `This takes the following parameters
+
+- fs - a remote name string eg "drive:"
+
+This returns info about the remote passed in;
+
+` + "```" + `
+{
+	// optional features and whether they are available or not
+	"Features": {
+		"About": true,
+		"BucketBased": false,
+		"CanHaveEmptyDirectories": true,
+		"CaseInsensitive": false,
+		"ChangeNotify": false,
+		"CleanUp": false,
+		"Copy": false,
+		"DirCacheFlush": false,
+		"DirMove": true,
+		"DuplicateFiles": false,
+		"GetTier": false,
+		"ListR": false,
+		"MergeDirs": false,
+		"Move": true,
+		"OpenWriterAt": true,
+		"PublicLink": false,
+		"Purge": true,
+		"PutStream": true,
+		"PutUnchecked": false,
+		"ReadMimeType": false,
+		"ServerSideAcrossConfigs": false,
+		"SetTier": false,
+		"SetWrapper": false,
+		"UnWrap": false,
+		"WrapFs": false,
+		"WriteMimeType": false
+	},
+	// Names of hashes available
+	"Hashes": [
+		"MD5",
+		"SHA-1",
+		"DropboxHash",
+		"QuickXorHash"
+	],
+	"Name": "local",	// Name as created
+	"Precision": 1,		// Precision of timestamps in ns
+	"Root": "/",		// Path as created
+	"String": "Local file system at /" // how the remote will appear in logs
+}
+` + "```" + `
+
+This command does not have a command line equivalent so use this instead:
+
+    rclone rc --loopback operations/fsinfo fs=remote:
+
+`,
+	})
+}
+
+// Fsinfo the remote
+func rcFsInfo(in rc.Params) (out rc.Params, err error) {
+	f, err := rc.GetFs(in)
+	if err != nil {
+		return nil, err
+	}
+	info := GetFsInfo(f)
+	err = rc.Reshape(&out, info)
+	if err != nil {
+		return nil, errors.Wrap(err, "fsinfo Reshape failed")
+	}
+	return out, nil
+}
diff --git a/fs/operations/rc_test.go b/fs/operations/rc_test.go
index 426abc62a..c3c16bfd2 100644
--- a/fs/operations/rc_test.go
+++ b/fs/operations/rc_test.go
@@ -370,3 +370,30 @@ func TestRcPublicLink(t *testing.T) {
 	require.Error(t, err)
 	assert.Contains(t, err.Error(), "doesn't support public links")
 }
+
+// operations/fsinfo: Return information about the remote
+func TestRcFsInfo(t *testing.T) {
+	r, call := rcNewRun(t, "operations/fsinfo")
+	defer r.Finalise()
+	in := rc.Params{
+		"fs": r.FremoteName,
+	}
+	got, err := call.Fn(in)
+	require.NoError(t, err)
+	want := operations.GetFsInfo(r.Fremote)
+	assert.Equal(t, want.Name, got["Name"])
+	assert.Equal(t, want.Root, got["Root"])
+	assert.Equal(t, want.String, got["String"])
+	assert.Equal(t, float64(want.Precision), got["Precision"])
+	var hashes []interface{}
+	for _, hash := range want.Hashes {
+		hashes = append(hashes, hash)
+	}
+	assert.Equal(t, hashes, got["Hashes"])
+	var features = map[string]interface{}{}
+	for k, v := range want.Features {
+		features[k] = v
+	}
+	assert.Equal(t, features, got["Features"])
+
+}