8000 feat: custom command · sntran/rclone.js@fcd01cb · GitHub
[go: up one dir, main page]

Skip to content

Commit fcd01cb

Browse files
committed
feat: custom command
A custom command can be called, and `rclone.js` CLI will look for the module that provides the functionality. For example, when calling the custom command `echo` below: ```sh $ npx rclone echo arg1 --string value arg2 --boolean ``` `rclone.js` CLI will look for the followings: - A `echo.js` in the current working directory. - A `echo/index.js` in the current working directory. - A `echo` module inside `node_modules`. - A `rclone-echo` module inside `node_modules`. The custom module just needs to export a function that takes arguments and flags parsed from the CLI. It can either return a child process, or a `Promise`. For a child process, its `stdout` and `stderr` are piped to the caller process.
1 parent be789d5 commit fcd01cb

File tree

6 files changed

+181
-101
lines changed

6 files changed

+181
-101
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,47 @@ $ npx rclone ls source: --max-depth 1
9393
-1 2020-12-12 10:01:44 -1 Documents
9494
-1 2020-12-11 16:24:20 -1 Pictures
9595
```
96+
97+
### Custom command
98+
99+
The CLI also supports executing a custom JS-based command to further extend
100+
usage outside of what the official `rclone` offers:
101+
102+
```sh
103+
$ npx rclone echo.js arg1 --string value arg2 --boolean
104+
```
105+
106+
The custom JS file just needs to export a function that takes the arguments and
107+
flags parsed from the CLI. It can either return a child process, or a `Promise`.
108+
For a child process, its `stdout` and `stderr` are piped to the caller process.
109+
110+
Inside the function, `this` is set to `rclone.js` module.
111+
112+
```js
113+
const { spawn } = require("child_process");
114+
115+
module.exports = function echo(arg1, arg2, flags = {}) {
116+
return spawn("echo", [arg1, arg2, JSON.stringify(flags)]);
117+
}
118+
```
119+
120+
The custom module is loaded through `require`, so it has some nice advantages
121+
when [locating module](https://nodejs.org/api/modules.html#all-together):
122+
123+
- Does not need to specify `.js` extension, `custom` is same as `custom.js`.
124+
- Considers both `foobar.js` and `foobar/index.js`.
125+
- Can be extended through `NODE_PATH` environment variable.
126+
- Can also use module from `node_modules` by its name.
127+
128+
With that, there are a few things custom commands can be used:
129+
130+
- Wraps existing API to add new functionality, such as `archive`.
131+
- Defines a module with the same name as existing API to extend it with new
132+
flags and/or backends.
133+
134+
For a "real-life" example, check out [selfupdate](rclone/selfupdate.js), which
135+
overrides the built-in `selfupdate` command to download rclone executable if it
136+
has not been downloaded yet. Consecutive runs just call `selfupdate` API.
137+
138+
For publishing a custom `rclone` command as NPM package, consider prefixing the
139+
package name with `rclone-` so it's clearer and not conflicting.

bin/rclone.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
#!/usr/bin/env node
2+
const { join } = require("path");
3+
24
const mri = require("mri");
35

46
const rclone = require("../");
57

68
const {_: args, ...flags} = mri(process.argv.slice(2));
79
const [commandName, ...commandArguments] = args;
810

9-
// "update" is not a rclone command.
10-
if (commandName === "update") {
11-
return rclone.update(...commandArguments, flags);
12-
}
13-
1411
// Executes rclone command if available.
15-
const { [commandName]: command } = rclone;
12+
let { [commandName]: command } = rclone;
13+
14+
// The current directory has highest priority.
15+
module.paths.push(".");
16+
// Then the library's "rclone" folder for `rclone.js` custom commands.
17+
module.paths.push(join(__dirname, "..", "rclone"));
18+
19+
try {
20+
// If the command is a custom module, requires it instead.
21+
command = require(commandName);
22+
} catch(error) {
23+
try {
24+
// If exact name is not found, maybe one prefixed with `rclone-`?
25+
command = require(`rclone-${commandName}`);
26+
} catch(error) {
27+
28+
}
29+
}
1630

1731
const subprocess = command ?
18-
command(...commandArguments, flags) :
32+
command.call(rclone, ...commandArguments, flags) :
1933
rclone(...args, flags);
2034

2135
subprocess.stdout?.on("data", (data) => {

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rclone.js",
3-
"version": "0.5.5",
3+
"version": "0.6.0",
44
"description": "JavaScript API for rclone",
55
"keywords": [
66
"rclone"
@@ -16,6 +16,11 @@
1616
"email": "rclone.js@sntran.com",
1717
"url": "https://sntran.com"
1818
},
19+
"files": [
20+
"bin/rclone.js",
21+
"rclone/",
22+
"rclone.js"
23+
],
1924
"main": "rclone.js",
2025
"bin": {
2126
"rclone": "./bin/rclone.js"

rclone.js

Lines changed: 2 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,7 @@
1-
const { existsSync } = require("fs");
21
const { join } = require("path");
32
const { spawn, ChildProcess } = require("child_process");
4-
const https = require("https");
53

6-
let { platform, arch } = process;
7-
8-
switch (platform) {
9-
case "darwin":
10-
platform = "osx";
11-
break;
12-
case "freebsd":
13-
case "linux":
14-
case "openbsd":
15-
break;
16-
case "sunos":
17-
platform = "solaris";
18-
case "win32":
19-
platform = "windows";
20-
default:
21-
break;
22-
}
23-
24-
switch (arch) {
25-
case "arm":
26-
case "arm64":
27-
case "mips":
28-
case "mipsel":
29-
break;
30-
case "x32":
31-
arch = "386";
32-
case "x64":
33-
arch = "amd64";
34-
default:
35-
break;
36-
}
4+
let { platform } = process;
375

386
const RCLONE_DIR = join(__dirname, "bin");
397
const DEFAULT_RCLONE_EXECUTABLE = join(RCLONE_DIR, `rclone${ platform === "windows"? ".exe" : "" }`);
@@ -96,53 +64,6 @@ const promises = api.promises = function(...args) {
9664
});
9765
};
9866

99-
/**
100-
* Updates rclone binary based on current OS.
101-
* @returns {Promise}
102-
*/
103-
api.selfupdate = function(options = {}) {
104-
const {
105-
beta = false,
106-
stable = !beta,
107-
version,
108-
check = false,
109-
} = options;
110-
111-
// Passes along to `rclone` if exists.
112-
if (existsSync(RCLONE_EXECUTABLE)) {
113-
return api("selfupdate", options);
114-
}
115-
116-
const baseUrl = stable ? "https://downloads.rclone.org" : "https://beta.rclone.org";
117-
const channel = stable ? "current" : "beta-latest";
118-
119-
if (check) {
120-
return get(`${ baseUrl }/version.txt`).then(version => {
121-
console.log(`The latest version is ${ version }`);
122-
});
123-
}
124-
125-
console.log("Downloading rclone...");
126-
const archiveName = version ? `${ version }/rclone-${ version }` : `rclone-${ channel }`;
127-
return get(`${ baseUrl }/${ archiveName }-${ platform }-${ arch }.zip`).then(archive => {
128-
console.log("Extracting rclone...");
129-
const AdmZip = require("adm-zip");
130-
const { chmodSync } = require("fs");
131-
132-
const zip = new AdmZip(archive);
133-
zip.getEntries().forEach((entry) => {
134-
const { name, entryName } = entry;
135-
if (/rclone(\.exe)?$/.test(name)) {
136-
zip.extractEntryTo(entry, RCLONE_DIR, false, true);
137-
// Make it executable.
138-
chmodSync(DEFAULT_RCLONE_EXECUTABLE, 0o755);
139-
140-
console.log(`${ entryName.replace(`/${ name }`, "") } is installed.`);
141-
}
142-
});
143-
});
144-
}
145-
14667
const COMMANDS = [
14768
"about", // Get quota information from the remote.
14869
"authorize", // Remote authorization.
@@ -197,6 +118,7 @@ const COMMANDS = [
197118
"rcd", // Run rclone listening to remote control commands only.
198119
"rmdir", // Remove the path if empty.
199120
"rmdirs", // Remove empty directories under the path.
121+
"selfupdate", // Update the rclone binary.
200122
"serve", // Serve a remote over a protocol.
201123
"serve dlna", // Serve remote:path over DLNA
202124
"serve ftp", // Serve remote:path over FTP.
@@ -240,15 +162,3 @@ COMMANDS.forEach(commandName => {
240162
});
241163

242164
module.exports = api;
243-
244-
async function get(url) {
245-
return new Promise((resolve, reject) => {
246-
https.get(url, (response) => {
247-
const chunks = [];
248-
response.on("data", (chunk) => chunks.push(chunk));
249-
response.on("end", () => {
250-
resolve(Buffer.concat(chunks));
251-
});
252-
});
253-
});
254-
}

rclone/selfupdate.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Custom command to selfupdate rclone binary.
2+
3+
const { existsSync } = require("fs");
4+
const { join } = require("path");
5+
const https = require("https");
6+
7+
let { platform, arch } = process;
8+
9+
switch (platform) {
10+
case "darwin":
11+
platform = "osx";
12+
break;
13+
case "freebsd":
14+
case "linux":
15+
case "openbsd":
16+
break;
17+
case "sunos":
18+
platform = "solaris";
19+
case "win32":
20+
platform = "windows";
21+
default:
22+
break;
23+
}
24+
25+
switch (arch) {
26+
case "arm":
27+
case "arm64":
28+
case "mips":
29+
case "mipsel":
30+
break;
31+
case "x32":
32+
arch = "386";
33+
case "x64":
34+
arch = "amd64";
35+
default:
36+
break;
37+
}
38+
39+
/**
40+
* Fetches a remote URL
41+
* @param {string} url the remote URL to fetch.
42+
* @returns {Promise<Buffer>} the response as Buffer.
43+
*/
44+
async function fetch(url) {
45+
return new Promise((resolve, reject) => {
46+
https.get(url, (response) => {
47+
const chunks = [];
48+
response.on("data", (chunk) => chunks.push(chunk));
49+
response.on("end", () => {
50+
resolve(Buffer.concat(chunks));
51+
});
52+
}).on("error", reject);
53+
});
54+
}
55+
56+
const RCLONE_DIR = join(__dirname, "..", "bin");
57+
const DEFAULT_RCLONE_EXECUTABLE = join(RCLONE_DIR, `rclone${ platform === "windows"? ".exe" : "" }`);
58+
const {
59+
RCLONE_EXECUTABLE = DEFAULT_RCLONE_EXECUTABLE,
60+
} = process.env;
61+
62+
/**
63+
* Updates rclone binary based on current OS.
64+
* @returns {Promise}
65+
*/
66+
module.exports = function(options = {}) {
67+
const {
68+
beta = false,
69+
stable = !beta,
70+
version,
71+
check = false,
72+
} = options;
73+
74+
// Passes along to `rclone` if exists.
75+
if (existsSync(RCLONE_EXECUTABLE)) {
76+
return this.selfupdate(options);
77+
}
78+
79+
const baseUrl = stable ? "https://downloads.rclone.org" : "https://beta.rclone.org";
80+
const channel = stable ? "current" : "beta-latest";
81+
82+
if (check) {
83+
return fetch(`${ baseUrl }/version.txt`).then(version => {
84+
console.log(`The latest version is ${ version }`);
85+
});
86+
}
87+
88+
console.log("Downloading rclone...");
89+
const archiveName = version ? `${ version }/rclone-${ version }` : `rclone-${ channel }`;
90+
return fetch(`${ baseUrl }/${ archiveName }-${ platform }-${ arch }.zip`).then(archive => {
91+
console.log("Extracting rclone...");
92+
const AdmZip = require("adm-zip");
93+
const { chmodSync } = require("fs");
94+
95+
const zip = new AdmZip(archive);
96+
zip.getEntries().forEach((entry) => {
97+
const { name, entryName } = entry;
98+
if (/rclone(\.exe)?$/.test(name)) {
99+
zip.extractEntryTo(entry, RCLONE_DIR, false, true);
100+
// Make it executable.
101+
chmodSync(DEFAULT_RCLONE_EXECUTABLE, 0o755);
102+
103+
console.log(`${ entryName.replace(`/${ name }`, "") } is installed.`);
104+
}
105+
});
106+
});
107+
}

0 commit comments

Comments
 (0)
0