├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── scripts
├── documents
├── patch
├── patch-harness
├── should-fail
├── should-fail-harness
├── should-succeed
├── should-succeed-harness
├── simple
├── test
├── test-harness
└── test2
├── sol
└── src
├── prefixes.js
├── semantics.js
├── sol.run.js
├── sol.shell.js
├── sol.show.js
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *#
2 | *~
3 | drafts*
4 | node_modules
5 | test-folder
6 | index.html
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Jeff Zucker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Solid-shell (Sol)
2 |
3 | a command-line tool, batch processor, and interactive shell for managing Solid data
4 |
5 | 
6 |
7 | Solid-shell (hereafter called Sol) is a nodejs tool for accessing Solid data that may be run as an interactive shell, as a batch processor, and on the command line. It provides a front-end for [Solid-File-Client](https://github.com/jeff-zucker/solid-file-client) and supports moving and copying files on remote pods, on local file systems, and between the two.
8 |
9 | Here's an overview of methods :
10 | ```text
11 | Document Management put,post,patch get,head,options,copy,move,delete,recursiveDelete,zip,unzip
12 | Testing/Batch Processing run,exists,notExists,matchText,statusOnly,verbosity
13 | Connectivity login, base
14 | Interactive Shell shell, help, quit
15 | ```
16 | Some recent changes :
17 |
18 | * There are now two methods to delete : simple **delete** (for a file or an empty folder) and **recursiveDelete** for an entire folder tree.
19 | * Added methods for patch and options
20 | * Added statusOnly to show e.g. 200 on a "get" rater than the full body
21 |
22 | ## Installation
23 |
24 | You can run without installing, with this command line: `npx solid-shell shell`.
25 |
26 | If you want to instll, you can either install via npm or clone the github repo
27 |
28 | To install with npm
29 | ```
30 | * npm install -g solid-shell
31 | * you should now be able to use "sol" to run solid-shell
32 | ```
33 | To install via the github repo
34 | * On this page (https://github.com/jeff-zucker/solid-shell) press the code button in the upper right, copy the url presented
35 | * in your local console: git clone COPIED_URL
36 | * that should creaste a folder named solid-shell
37 | * in that folder use "./sol" to run solid-shell
38 |
39 | ## Command-line usage
40 |
41 | For one-off commands, you can run sol commands directly. For example this
42 | recursively uploads a folder to a Solid POD.
43 | ```
44 | sol --login copy ./someLocalPath/ /someRemotePath/
45 | ```
46 | Enter sol -h to see a full list of commands available from the command line.
47 |
48 | ## Interactive shell usage
49 |
50 | You may also use sol as an interactive shell. Enter the shell like so:
51 |
52 | sol shell
53 |
54 | Once in the shell, enter "help" to see a list of commands available in the shell or "quit" to exit the interactive shell.
55 |
56 | ## Logging in
57 |
58 | You can read and write local resources and read public pod-based resources without logging in. To access private pod data, you will need to specify your login credentials and login. Sol looks for the credentials first in environment variables, and if not found, prompts you for them.
59 |
60 | ### Prep for logging in with username/password on NSS
61 |
62 | If using a Node-Solid-Server (NSS) such as solidcommunity.net you may login with username and password. This requires two one-time steps and which can thereafter be skipped:
63 |
64 | 1. Set `https://solid-node-client` as a trusted app for your pod by one of these methods:
65 |
66 | a. manually edit your profile if you know how, or
67 |
68 | b. go to your pod root e.g. `https://YOU.solidcommunity.net/`; login using the databrowser; click on the preferences tab; scroll to the bottom of the trusted apps and add `https//solid-node-client`
69 |
70 | 2. You can let Sol prompt you for login credentials each time, or you can set the following environment variables once and Sol will use them thereafter:
71 | ```
72 | SOLID_USERNAME
73 | SOLID_PASSWORD
74 | SOLID_IDP
75 | SOLID_REMOTE_BASE
76 | ```
77 | 3. See below "performing a login"
78 |
79 | ### Prep for logging in with a token
80 |
81 | If you are not using NSS or are not using the username/password login, please read the documentation for your server and find out how to get the data needed to set these environmen t variables. If these are not in your environment, you will be prompted for them when you login.
82 | ```
83 | SOLID_REFRESH_TOKEN
84 | SOLID_CLIENT_ID
85 | SOLID_CLIENT_SECRET
86 | SOLID_OIDC_ISSUER
87 | SOLID_REMOTE_BASE
88 | ```
89 |
90 | ### Performing a login
91 |
92 | You can invoke login from the command-line, in the interactive shell or from a script. In all cases Sol will look for the environment variables shown above and, if they any are missing, will prompt you for them.
93 |
94 | From the command line :
95 | ```
96 | sol -l head /foo/private.txt
97 | sol --login head /foo/private.txt
98 | ```
99 |
100 | ## Specifying a base folder
101 |
102 | When you specify a base folder, it will be prepended to any URLs starting with /. In other words, if you set the base to "https://me.example.com/public" then a request "get /foo.ttl" will read https://me.example.com/public/foo.ttl.
103 |
104 | You can set the Base in the environment variables shown above, or by using the *base* method in interactive or script mode.
105 |
106 | ## Working with URLs
107 |
108 | All methods in Solid-shell can use files or folders from local
109 | or remote locations. You must specify these options in the URL.
110 |
111 | * Folder URLs always end with a slash.
112 | ```
113 | https://me.example.com/foo/ a folder
114 | https://me.example.com/foo a file
115 | ```
116 | * Absolute URLs start with https:// or file://
117 | ```
118 | https://me.exmaple.com/foo/bar.ttl a remote file
119 | file:///home/me/foo/bar.ttl a local file
120 | ```
121 | * Relative URLs start with ./ or /
122 | ```
123 | ./foo/bar.ttl a local file relative to your current working folder
124 | /foo/bar.ttl a remote file relative to your specified base folder
125 | ```
126 | ## Methods
127 |
128 | ### **put <URL> <content>**
129 |
130 | Create a file or folder with Write permission. If the parent path does not exist, it will be created. E.g. if you don't have a /foo folder, "put /foo/bar.txt" will create /foo and /foo/bar.txt. For files, a put will over-write any existing file of the same name. For folders, if the folder pre-exists, put will keep the existing folder rather than overwrite it or create a new one.
131 |
132 | For files, the URL should contain an extension that clearly labels the content-type e.g. .ttl for turtle, .txt for text, etc.
133 |
134 | Content may be omitted to create a blank file.
135 |
136 | Use of put requires Write permissions on the resource - if you have Append, but not Write permissions, use post instead.
137 |
138 | ### **post <URL> <content>**
139 |
140 | Ceate a file or folder with Append permission. Post works exactly the same as put with these exceptions :
141 |
142 | * It can be used with only Append permissions, it does not need full Write permission.
143 | * If the file or folder already exists, another version with a random prefix on the file name will be created, nothing will ever be overwritten
144 |
145 | ### **get <URL>**
146 |
147 | Read a file or folder and display its contents. Requires Read permission.
148 |
149 | ### **head <URL>**
150 |
151 | Show headers for a file or folder. Requires Read permission.
152 |
153 | ### **copy <URLa> <URLb>**
154 |
155 | Copy a file or recursively copy a folder. Needs Read permission on the *from*p location and Write permission on the *to* location. Completely overwrites the *to* location if it exists.
156 |
157 | ### **move <URLa> <URLb>**
158 |
159 | Move a file or recursively move a folder. Needs Write permission on both the *to* and *from* locations . Completely overwrites the *to* location if it exists.
160 |
161 | ### **delete <URL>**
162 |
163 | Delete a file or recursively delete a folder. Needs Write permission on the resource.
164 |
165 | ### **zip <URL> <zipFile>**
166 |
167 | Create a zip archive. Needs Read permission on the URL and write permission on the zipFile location.
168 |
169 | ### **unzip <zipFile> <URL>**
170 |
171 | Extracts a zip archive. Needs Read permission on the zipFile location and Write permission on the extraction location.
172 |
173 | ## Testing
174 |
175 | You may test the existence of a resource with **exists** or **notExists** and the contents of a resource with **matchText**. See below for an example.
176 |
177 | ## Batch Processing
178 | You may put a series of Sol commands in a file and then run them as a batch either from the command-line or from the interactive shell.
179 |
180 | From the command line:
181 | ```
182 | sol run ./myBatchFile
183 | ```
184 | Within the batch file, commands should be separated by newlines; a line starting with *#* and will be treated as a comment; blank lines will be ignrored. Here's an example batch file:
185 | Here's an example of simple script which can be executed with "sol run ./script-name":
186 | ```
187 | put ./test-folder/test.txt "hello world"
188 | exists ./test-folder/test.txt
189 | matchText ./test-folder/test.txt "hello world"
190 | delete ./test-folder/
191 | notExists ./test-folder/
192 | # END
193 | Expected output :
194 | ok put <./test-folder/test.txt>
195 | ok exists <./test-folder/test.txt>
196 | ok contentsMatch
197 | ok delete <./test-folder/>
198 | ok notExists <./test-folder/>
199 | ```
200 | See the [scripts folder](./scripts) for more script examples.
201 |
202 | © 2019,2021 Jeff Zucker, may be freely distributed under an MIT license.
203 |
204 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "solid-shell",
3 | "version": "2.0.25",
4 | "engines": {
5 | "node": ">=16.0.0"
6 | },
7 | "main": "sol",
8 | "repository": "https://github.com/jeff-zucker/solid-shell.git",
9 | "author": "Jeff Zucker",
10 | "license": "MIT",
11 | "keywords": [
12 | "Solid",
13 | "Linked Data",
14 | "NodeJS"
15 | ],
16 | "bin": {
17 | "sol": "./sol"
18 | },
19 | "scripts": {
20 | "sol": "./sol"
21 | },
22 | "dependencies": {
23 | "commander": "^12.0.0",
24 | "cross-fetch": "^4.0.0",
25 | "mime-types": "^2.1.35",
26 | "rdflib": "^2.2.33",
27 | "readline-sync": "^1.4.10",
28 | "solid-file-client": "^2.1.11",
29 | "solid-namespace": "^0.5.3",
30 | "solid-node-client": "^2.1.20"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/scripts/documents:
--------------------------------------------------------------------------------
1 | # prep test area
2 | #
3 | !
4 | recursiveDelete ./test-folder/
5 | notExists ./test-folder/
6 |
7 | # create resource
8 | #
9 | put ./test-folder/sub1/test1.txt "This is a test."
10 |
11 | # move resource
12 | #
13 | move ./test-folder/sub1/test1.txt ./test-folder/sub1/test-2.txt
14 | notExists ./test-folder/sub1/test1.txt
15 | matchText ./test-folder/sub1/test-2.txt "This is a test."
16 |
17 | # copy resource
18 | #
19 | copy ./test-folder/sub1/test-2.txt ./test-folder/sub1/test-3.txt
20 | exists ./test-folder/sub1/test-2.txt
21 | exists ./test-folder/sub1/test-3.txt
22 |
23 | # zip ./test-folder/sub1/ ./test-folder/zip-folder/test.zip
24 | # exists ./test-folder/zip-folder/test.zip
25 |
26 | # unzip ./test-folder/zip-folder/test.zip ./test-folder/zip-folder/
27 | # exists ./test-folder/zip-folder/sub1/test-3.txt
28 |
29 | recursiveDelete ./test-folder/
30 | notExists ./test-folder/
31 |
32 | # END
33 |
34 | Expected Output :
35 |
36 | ok put <./test-folder/sub1/test.txt>
37 | ok move <./test-folder/sub1/test.txt> to <./test-folder/test-2.txt>
38 | ok notExists <./test-folder/sub1/test.txt>
39 | ok contentsMatch
40 | ok copy <./test-folder/test-2.txt> to <./test-folder/test-3.txt>
41 | ok exists <./test-folder/test-2.txt
42 | ok exists <./test-folder/test-3.txt
43 | ok zip <./test-folder/> to <./test.zip>
44 | ok exists <./test.zip
45 | ok unzip <./test.zip> to <./test-folder-2/>
46 | ok exists <./test-folder-2/test-folder/test-3.txt
47 | ok delete <./test-folder/>
48 | ok delete <./test-folder-2/>
49 | ok delete <./test.zip>
50 | ok notExists <./test-folder/>
51 |
--------------------------------------------------------------------------------
/scripts/patch:
--------------------------------------------------------------------------------
1 | verbosity 0
2 | put /test/good.ttl <#A> a <#foo>.
3 | put /test/bad.ttl <#A> junk junk.
4 | verbosity 1
5 |
6 | #!Delete non-existent triple
7 | patch /test/good.ttl DELETE DATA { <#X> a <#Baz> }
8 |
9 | #!Patch on Container
10 | patch /test/ INSERT DATA { <#X> a <#Baz> }
11 |
12 | # END
13 | #!Syntax error in patch [400 in all implementations]
14 | patch /test/good.ttl INSERT DATA { <#X> junk <#Baz> }
15 |
16 | #!Syntax error in file to be patched [400 in all implementations]
17 | patch /test/bad.ttl INSERT DATA <#A> a <#foo>.
18 |
19 |
20 |
21 |
22 | patch /test/x.ttl INSERT DATA { <#A> a <#Foo> }
23 | patch /test/x.ttl INSERT DATA { <#B> a <#Foo> }
24 | patch /test/x.ttl INSERT DATA { <#C> a <#Bar> }
25 | patch /test/x.ttl DELETE DATA { <#B> a <#Foo> }
26 |
27 | !Syntax error in patch
28 | 400 - all
29 |
30 | !Syntax error in file to be patched
31 | 400 CSS
32 | 500 NSS & SRF
33 | 201 ESS
34 |
35 | !Patch on non-rdf resource
36 |
37 | !Delete non-existent triple
38 | !Patch on Container
39 |
40 | CSS 400 205 400 501 205 409
41 | SRF 400 200 500 409 409 409
42 | ESS 400 201 201 405 201 204
43 | NSS 400 200 500 500 409 500
44 |
--------------------------------------------------------------------------------
/scripts/patch-harness:
--------------------------------------------------------------------------------
1 | statusOnly true
2 |
3 | !
4 | ! CSS
5 | verbosity 0
6 | base http://localhost:3000/jeff/test-folder/writable
7 | run ./scripts/patch
8 |
9 | !
10 | ! SOLID-REST (LOCAL FILE SYSTEM)
11 | verbosity 0
12 | base ./test-folder/writable
13 | run ./scripts/patch
14 |
15 | !
16 | ! LOGIN
17 | login
18 |
19 | ! ESS
20 | verbosity 0
21 | base https://pod.inrupt.com/jeff-zucker/public
22 | run ./scripts/patch
23 |
24 | !
25 | ! NSS
26 | verbosity 0
27 | base https://jeff-zucker.solidcommunity.net
28 | run ./scripts/patch
29 |
30 |
31 |
--------------------------------------------------------------------------------
/scripts/should-fail:
--------------------------------------------------------------------------------
1 | verbosity 0
2 | put /test/x.ttl <#A> a <#Foo>.
3 | verbosity 1
4 |
5 | ! *** Post to .acl
6 | post /test/foo.acl junk
7 |
8 | ! ** put/post without content-type
9 | put ./test/foo
10 | post ./test/foo
11 |
12 | ! *** Patch delete non-existent triple
13 | patch /test/x.ttl DELETE DATA { <#X> a <#Bar> }
14 |
15 | ! *** Delete on non-empty container
16 | delete /test/
17 |
18 | ! *** Delete/get/head/ on non-existent resource
19 | delete /does-not-exist.txt
20 | get /does-not-exist.txt
21 | head /does-not-exist.txt
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/scripts/should-fail-harness:
--------------------------------------------------------------------------------
1 | statusOnly true
2 |
3 | !
4 | ! CSS
5 | verbosity 0
6 | base http://localhost:3000/jeff/test-folder/writable
7 | run ./scripts/should-fail
8 |
9 | !
10 | ! SOLID-REST (LOCAL FILE SYSTEM)
11 | verbosity 0
12 | base ./test-folder/writable
13 | run ./scripts/should-fail
14 |
15 | !
16 | ! LOGIN
17 | login
18 |
19 | ! ESS
20 | verbosity 0
21 | base https://pod.inrupt.com/jeff-zucker/public
22 | run ./scripts/should-fail
23 |
24 | !
25 | ! NSS
26 | verbosity 0
27 | base https://jeff-zucker.solidcommunity.net
28 | run ./scripts/should-fail
29 |
30 |
31 |
--------------------------------------------------------------------------------
/scripts/should-succeed:
--------------------------------------------------------------------------------
1 | verbosity 0
2 | recursiveDelete /test/
3 | verbosity 1
4 | #put /test/sub1/
5 | post /test/sub1/sub2/
6 | put /test/sub1/sub2/sub3/x.txt hello world
7 | post /test/sub1/sub2/y.ttl <#A> a <#Foo>.
8 | get /test/sub1/sub2/
9 | get /test/sub1/sub2/y.ttl
10 | head /test/sub1/sub2/
11 | head /test/sub1/sub2/y.ttl
12 | options /test/sub1/sub2/y.ttl
13 | patch /test/sub1/sub2/y.ttl INSERT DATA { <#A> a <#Bar> }
14 | delete /test/sub1/sub2/sub3/x.txt
15 | delete /test/sub1/sub2/sub3/
16 |
17 | # END
18 | zip /test/sub1/ /test/zipFolder/archive.zip
19 | unzip /test/zipFolder/archive.zip /test/zipFolder/
20 |
21 |
22 | copy /public/test-folder/test/ /public/test-folder/b/
23 | get /public/test-folder/test/
24 | get /public/test-folder/b/
25 |
26 | move /public/test-folder/test/ /public/test-folder/c/
27 | get /public/test-folder/c/
28 |
29 |
30 | //get /public/test-folder/b/test1/x.txt
31 | //get /public/test-folder/c/test1/x.txt
32 | //get /public/test-folder/d/test1/x.txt
33 |
34 |
35 | recursiveDelete /public/test-folder/test/
36 |
--------------------------------------------------------------------------------
/scripts/should-succeed-harness:
--------------------------------------------------------------------------------
1 | statusOnly true
2 |
3 | !
4 | ! CSS
5 | verbosity 0
6 | base http://localhost:3000/jeff/test-folder/writable
7 | run ./scripts/should-succeed
8 |
9 | !
10 | ! SOLID-REST (LOCAL FILE SYSTEM)
11 | verbosity 0
12 | base ./test-folder/writable
13 | run ./scripts/should-succeed
14 |
15 | !
16 | ! LOGIN
17 | login
18 |
19 | ! ESS
20 | verbosity 0
21 | base https://pod.inrupt.com/jeff-zucker/public
22 | run ./scripts/should-succeed
23 |
24 | !
25 | ! NSS
26 | verbosity 0
27 | base https://jeff-zucker.solidcommunity.net
28 | run ./scripts/should-succeed
29 |
30 |
31 |
--------------------------------------------------------------------------------
/scripts/simple:
--------------------------------------------------------------------------------
1 | put ./test-folder/test.txt "hello world"
2 | exists ./test-folder/test.txt
3 | matchText ./test-folder/test.txt "hello world"
4 | recursiveDelete ./test-folder/
5 | notExists ./test-folder/
6 |
7 | # END
8 |
9 | Expected output :
10 |
11 | 201 put <...>
12 | ok exists <...>
13 | ok contentsMatch <...>
14 | ok recursiveDelete <...>
15 | ok notExists <...>
16 |
--------------------------------------------------------------------------------
/scripts/test:
--------------------------------------------------------------------------------
1 | # prep test
2 | #
3 | recursiveDelete ./test-folder/
4 |
5 | # CREATE
6 | #
7 | put ./test-folder/sub1/test-1.txt "This is a test."
8 |
9 |
10 | # READ
11 | #
12 | matchText ./test-folder/sub1/test-1.txt "This is a test."
13 |
14 | # COPY
15 | #
16 | copy ./test-folder/sub1/test-1.txt ./test-folder/sub1/test-2.txt
17 | exists ./test-folder/sub1/test-1.txt
18 | exists ./test-folder/sub1/test-2.txt
19 |
20 | # DELETE
21 | #
22 | delete ./test-folder/sub1/test-2.txt
23 | notExists ./test-folder/sub1/test-2.txt
24 |
25 | # MOVE
26 | #
27 | move ./test-folder/sub1/test-1.txt ./test-folder/sub1/test-3.txt
28 | notExists ./test-folder/sub1/test-1.txt
29 | matchText ./test-folder/sub1/test-3.txt "This is a test."
30 |
31 |
32 | # ZIP
33 | #
34 | zip ./test-folder/sub1/ ./test-folder/zip-folder/test.zip
35 | exists ./test-folder/zip-folder/test.zip
36 | unzip ./test-folder/zip-folder/test.zip ./test-folder/zip-folder/
37 | exists ./test-folder/zip-folder/sub1/test-3.txt
38 |
39 | recursiveDelete ./test-folder/
40 | notExists ./test-folder/
41 |
42 | # END
43 |
44 | Expected Output :
45 |
46 | ok put <./test-folder/sub1/test.txt>
47 | ok move <./test-folder/sub1/test.txt> to <./test-folder/test-2.txt>
48 | ok notExists <./test-folder/sub1/test.txt>
49 | ok contentsMatch
50 | ok copy <./test-folder/test-2.txt> to <./test-folder/test-3.txt>
51 | ok exists <./test-folder/test-2.txt
52 | ok exists <./test-folder/test-3.txt
53 | ok zip <./test-folder/> to <./test.zip>
54 | ok exists <./test.zip
55 | ok unzip <./test.zip> to <./test-folder-2/>
56 | ok exists <./test-folder-2/test-folder/test-3.txt
57 | ok delete <./test-folder/>
58 | ok delete <./test-folder-2/>
59 | ok delete <./test.zip>
60 | ok notExists <./test-folder/>
61 |
--------------------------------------------------------------------------------
/scripts/test-harness:
--------------------------------------------------------------------------------
1 | #statusOnly true
2 | verbosity 0
3 |
4 | !
5 | ! LOGIN
6 | login
7 |
8 | !
9 | ! NSS
10 | run ./scripts/test2
11 |
12 | !
13 | ! SOLID-REST (LOCAL FILE SYSTEM)
14 | base ./
15 | run ./scripts/test2
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/scripts/test2:
--------------------------------------------------------------------------------
1 | # prep test
2 | #
3 | recursiveDelete /test-folder/
4 |
5 | # CREATE
6 | #
7 | put /test-folder/sub1/test-1.txt "This is a test."
8 |
9 |
10 | # READ
11 | #
12 | matchText /test-folder/sub1/test-1.txt "This is a test."
13 |
14 | # COPY
15 | #
16 | copy /test-folder/sub1/test-1.txt /test-folder/sub1/test-2.txt
17 | exists /test-folder/sub1/test-1.txt
18 | exists /test-folder/sub1/test-2.txt
19 |
20 | # DELETE
21 | #
22 | delete /test-folder/sub1/test-2.txt
23 | notExists /test-folder/sub1/test-2.txt
24 |
25 | # MOVE
26 | #
27 | move /test-folder/sub1/test-1.txt /test-folder/sub1/test-3.txt
28 | notExists /test-folder/sub1/test-1.txt
29 | matchText /test-folder/sub1/test-3.txt "This is a test."
30 |
31 |
32 | # ZIP
33 | #
34 | zip /test-folder/sub1/ /test-folder/zip-folder/test.zip
35 | exists /test-folder/zip-folder/test.zip
36 | unzip /test-folder/zip-folder/test.zip /test-folder/zip-folder/
37 | exists /test-folder/zip-folder/sub1/test-3.txt
38 |
39 | recursiveDelete /test-folder/
40 | notExists /test-folder/
41 |
42 | # END
43 |
44 | Expected Output :
45 |
46 | ok put
47 | ok move to
48 | ok notExists
49 | ok contentsMatch
50 | ok copy to
51 | ok exists to
54 | ok exists to
56 | ok exists
58 | ok delete
59 | ok delete
60 | ok notExists
61 |
--------------------------------------------------------------------------------
/sol:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const {program} = require('commander');
3 | const sol = require('./src/sol.run.js'); // the commands
4 | const shell = require('./src/sol.shell.js'); // the prompts
5 | const {packageVersions} = require('./src/utils.js'); // version display
6 | /*
7 | * This file is the command-line interface
8 | */
9 |
10 | async function main() {
11 | await packageVersions(['../','solid-file-client','solid-node-client','@solid-rest/file']);
12 | program.parse(process.argv);
13 | }
14 | program.option('-l,--login',"login");
15 |
16 | program
17 | .command('put [CONTENT...]')
18 | .description('create a file or folder')
19 | .action( async (URL,CONTENT) => {
20 | if( program.opts().login ) await sol.runSol("login")
21 | CONTENT = CONTENT.join(" ");
22 | sol.runSol("put",[URL,CONTENT]).then(()=>{
23 | },err=>console.log(err));
24 | });
25 | program
26 | .command('post [CONTENT...]')
27 | .description('create a file or folder')
28 | .action( async (URL,CONTENT) => {
29 | if( program.opts().login ) await sol.runSol("login")
30 | CONTENT = CONTENT.join(" ");
31 | sol.runSol("post",[URL,CONTENT]).then(()=>{
32 | },err=>console.log(err));
33 | });
34 | program
35 | .command('patch [CONTENT...]')
36 | .description('create a file or folder')
37 | .action( async (URL,CONTENT) => {
38 | if( program.opts().login ) await sol.runSol("login")
39 | CONTENT = CONTENT.join(" ");
40 | sol.runSol("patch",[URL,CONTENT]).then(()=>{
41 | },err=>console.log(err));
42 | });
43 | program
44 | .command('options ')
45 | .description('create a file or folder')
46 | .action( async (URL,CONTENT) => {
47 | if( program.opts().login ) await sol.runSol("login")
48 | CONTENT = CONTENT.join(" ");
49 | sol.runSol("options").then(()=>{
50 | },err=>console.log(err));
51 | });
52 | program
53 | .command('head ')
54 | .description('show headers for a file or folder')
55 | .action( async (URL) => {
56 | if( program.opts().login ) await sol.runSol("login")
57 | sol.runSol("head",[URL]).then(()=>{
58 | },err=>console.log(err));
59 | });
60 | program
61 | .command('get ')
62 | .description('show contents of a file or folder')
63 | .action( async (URL) => {
64 | if( program.opts().login ) await sol.runSol("login")
65 | sol.runSol("get",[URL]).then(()=>{
66 | },err=>console.log(err));
67 | });
68 | program
69 | .command('copy [noAux]')
70 | .description('copy a file or recursively copy a folder')
71 | .action( async (oldURL,newURL,noAux) => {
72 | if( program.opts().login ) await sol.runSol("login")
73 | sol.runSol("copy",[oldURL,newURL,noAux]).then(()=>{
74 | },err=>console.log(err));
75 | });
76 | program
77 | .command('move ')
78 | .description('move a file or recursively move a folder')
79 | .action( async (oldURL,newURL) => {
80 | if( program.opts().login ) await sol.runSol("login")
81 | sol.runSol("mv",[oldURL,newURL]).then(()=>{
82 | },err=>console.log(err));
83 | });
84 | program
85 | .command('delete ')
86 | .description('delete a file or an empty folder')
87 | .action( async (URL) => {
88 | if( program.opts().login ) await sol.runSol("login")
89 | sol.runSol("delete",[URL]).then(()=>{
90 | },err=>console.log(err));
91 | });
92 | program
93 | .command('recursiveDelete ')
94 | .description('recursively delete a folder tree')
95 | .action( async (URL) => {
96 | if( program.opts().login ) await sol.runSol("login")
97 | sol.runSol("delete",[URL]).then(()=>{
98 | },err=>console.log(err));
99 | });
100 | program
101 | .command('emptyFolder ')
102 | .description('recursively delete the contents of a folder tree')
103 | .action( async (URL) => {
104 | if( program.opts().login ) await sol.runSol("login")
105 | sol.runSol("emptyFolder",[URL]).then(()=>{
106 | },err=>console.log(err));
107 | });
108 | program
109 | .command('zip ')
110 | .description('create a zip archive')
111 | .action( async (folderURL,zipFileURL) => {
112 | if( program.opts().login ) await sol.runSol("login")
113 | sol.runSol("zip",[folderURL,zipFileURL]).then(()=>{
114 | },err=>console.log(err));
115 | });
116 | program
117 | .command('unzip ')
118 | .description('extract a zip archive')
119 | .action( async (zipFileURL,folderURL) => {
120 | if( program.opts().login ) await sol.runSol("login")
121 | sol.runSol("unzip",[zipFileURL,folderURL]).then(()=>{
122 | },err=>console.log(err));
123 | });
124 | program
125 | .command('run ')
126 | .description('batch run commands in a script file')
127 | .action( async (scriptFile) => {
128 | if( program.opts().login ) await sol.runSol("login")
129 | sol.runSol("run",[scriptFile]).then(()=>{
130 | },err=>console.log(err) )
131 | },err=>console.log(err));
132 | program
133 | .command('shell')
134 | .description('run as an interactive shell')
135 | .action( async () => {
136 | console.clear();
137 | if( program.opts().login ) await sol.runSol("login")
138 | sol.runSol("help").then(()=>{
139 | shell.sh()
140 | },err=>console.log(err));
141 | });
142 |
143 | main();
144 |
145 |
146 |
--------------------------------------------------------------------------------
/src/prefixes.js:
--------------------------------------------------------------------------------
1 | const prefixes = {
2 | acl: 'http://www.w3.org/ns/auth/acl#',
3 | arg: 'http://www.w3.org/ns/pim/arg#',
4 | as: 'https://www.w3.org/ns/activitystreams#',
5 | cal: 'http://www.w3.org/2002/12/cal/ical#',
6 | cert: 'http://www.w3.org/ns/auth/cert#',
7 | contact: 'http://www.w3.org/2000/10/swap/pim/contact#',
8 | dc: 'http://purl.org/dc/elements/1.1/',
9 | dct: 'http://purl.org/dc/terms/',
10 | doap: 'http://usefulinc.com/ns/doap#',
11 | foaf: 'http://xmlns.com/foaf/0.1/',
12 | geo: 'http://www.w3.org/2003/01/geo/wgs84_pos#',
13 | gpx: 'http://www.w3.org/ns/pim/gpx#',
14 | http: 'http://www.w3.org/2007/ont/http#',
15 | httph: 'http://www.w3.org/2007/ont/httph#',
16 | icalTZ: 'http://www.w3.org/2002/12/cal/icaltzd#', // Beware: not cal:
17 | ldp: 'http://www.w3.org/ns/ldp#',
18 | link: 'http://www.w3.org/2007/ont/link#',
19 | log: 'http://www.w3.org/2000/10/swap/log#',
20 | meeting: 'http://www.w3.org/ns/pim/meeting#',
21 | mo: 'http://purl.org/ontology/mo/',
22 | org: 'http://www.w3.org/ns/org#',
23 | owl: 'http://www.w3.org/2002/07/owl#',
24 | pad: 'http://www.w3.org/ns/pim/pad#',
25 | patch: 'http://www.w3.org/ns/pim/patch#',
26 | prov: 'http://www.w3.org/ns/prov#',
27 | qu: 'http://www.w3.org/2000/10/swap/pim/qif#',
28 | trip: 'http://www.w3.org/ns/pim/trip#',
29 | rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
30 | rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
31 | rss: 'http://purl.org/rss/1.0/',
32 | sched: 'http://www.w3.org/ns/pim/schedule#',
33 | schema: 'http://schema.org/', // @@ beware confusion with documents no 303
34 | sioc: 'http://rdfs.org/sioc/ns#',
35 | solid: 'http://www.w3.org/ns/solid/terms#',
36 | space: 'http://www.w3.org/ns/pim/space#',
37 | stat: 'http://www.w3.org/ns/posix/stat#',
38 | tab: 'http://www.w3.org/2007/ont/link#',
39 | tabont: 'http://www.w3.org/2007/ont/link#',
40 | ui: 'http://www.w3.org/ns/ui#',
41 | vcard: 'http://www.w3.org/2006/vcard/ns#',
42 | wf: 'http://www.w3.org/2005/01/wf/flow#',
43 | xsd: 'http://www.w3.org/2001/XMLSchema#',
44 | cco: 'http://www.ontologyrepository.com/CommonCoreOntologies/'
45 | }
46 | function removePrefix(thing){
47 | let thing2 = thing.replace(/.*\#/,'').replace(/.*\//,'');
48 | return thing2;
49 | }
50 | function getPrefix(thing,source){
51 | if( !thing.match(/:/) ) return thing;
52 | let [prefix,term] = thing.split(/:/);
53 | if( !prefix ) return source + "#" + thing.replace(/:/,'');
54 | if(prefixes[prefix]) return prefixes[prefix] + term;
55 | }
56 | function parseQuery($rdf,args,source){
57 | let [s,p,o] = args
58 | if(s) s = s.replace(/\.$/,'');
59 | if(p) p = p.replace(/\.$/,'');
60 | if(o) o = o.replace(/\.$/,'');
61 | s = s && s==="?" ?null :s;
62 | p = p && p==="?" ?null :p;
63 | o = o && o==="?" ?null :o;
64 | if(s) s = $rdf.sym(getPrefix(s,source));
65 | if(p) {
66 | if(p==="a") p = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
67 | else p = getPrefix(p,source);
68 | p = $rdf.sym(p);
69 | }
70 | if(o) o = $rdf.sym(getPrefix(o,source));
71 | return [s,p,o];
72 | }
73 |
74 | module.exports = {
75 | prefixes, parseQuery, removePrefix, getPrefix
76 | }
77 |
--------------------------------------------------------------------------------
/src/semantics.js:
--------------------------------------------------------------------------------
1 | const u = require("./utils.js");
2 |
3 | async function load(source){
4 | console.log(source)
5 | try {
6 | await fetcher.load(source);
7 | return `ok load <${u.unMunge(source)}>`;
8 | }
9 | catch(err) {
10 | if(err.status==404) return `${u.unMunge(source)} - not found`;
11 | return `Could not load ${u.unMunge(source)} - ${err.status}`;
12 | }
13 | }
14 | module.exports = {load}
15 |
--------------------------------------------------------------------------------
/src/sol.run.js:
--------------------------------------------------------------------------------
1 | // general imports
2 | //
3 | const $rdf = global.$rdf = require("rdflib");
4 | const fs = require("fs");
5 | const path = require("path");
6 | const contentTypeLookup = require('mime-types').contentType;
7 |
8 | // imports from other JZ libraries
9 | //
10 | const SolidFileClient=require("solid-file-client");
11 | const {SolidNodeClient} = require('solid-node-client');
12 |
13 | // imports from this library
14 | //
15 | const semantics = require("./semantics.js");
16 | const {mungeURL,unMunge,isFolder,do_err,log,getContentType}=require('./utils.js')
17 | const pr = require('./prefixes.js');
18 | const show = require("./sol.show.js");
19 | const shell = require('./sol.shell.js');
20 |
21 | const client = new SolidNodeClient({parser:$rdf});
22 | const fc = new SolidFileClient(client)
23 |
24 | var verbosity = 2
25 | let credentials;
26 | let rbase=process.env.SOLID_REMOTE_BASE;
27 |
28 | const LINK = {
29 | CONTAINER: '; rel="type"',
30 | RESOURCE: '; rel="type"'
31 | }
32 | const kb = $rdf.graph();
33 | const fetcher = $rdf.fetcher(kb,{fetch:client.fetch.bind(client)});
34 | let source = "https://example.com/";
35 | let statusOnly = false;
36 |
37 | module.exports.runSol = runSol;
38 | async function runSol(com,args) {
39 | let fn,target,expected,opts,turtle,q,content,type;
40 | return new Promise(async (resolve,reject)=>{ switch(com){
41 | case "load" :
42 | source = mungeURL(args[0]);
43 | log( await semantics.load(source) );
44 | resolve();
45 | break
46 | case "putback" :
47 | case "putBack" :
48 | source = mungeURL(args[0]);
49 | try {
50 | fetcher.putBack(source).then(()=>{
51 | log(`ok putback <${source}>.`);
52 | resolve();
53 | });
54 | }
55 | catch(err) {
56 | log(`Could not putback ${source} - ${err.status}`);
57 | }
58 | break;
59 |
60 | case "add" :
61 | if(args[0].startsWith("[")) {
62 | source = args.shift()
63 | source = source.replace(/^\[/,'').replace(/\]$/,'');
64 | source = mungeURL( source );
65 | }
66 | turtle = "@prefix : <#> .\n" + args.join(' ');
67 | try {
68 | $rdf.parse(turtle, kb, source, "text/turtle");
69 | resolve();
70 | }
71 | catch(err) {
72 | log(`Could not load Turtle - ${err}\n\n${turtle}`);
73 | resolve();
74 | }
75 | break;
76 |
77 | case "match" :
78 | /*
79 | if(args[0].startsWith("[")) {
80 | source = args.shift()
81 | source = source.replace(/^\[/,'').replace(/\]$/,'');
82 | source = mungeURL( source );
83 | }
84 | */
85 | q = pr.parseQuery($rdf,args,source);
86 | let matches = "";
87 | try {
88 | for(var m of kb.match(q[0],q[1],q[2])){
89 | let s1 = pr.removePrefix(m.subject.value);
90 | let p1 = pr.removePrefix(m.predicate.value);
91 | p1 = p1.match(/type/) ?"a" :p1;
92 | let o1 = pr.removePrefix(m.object.value)
93 | matches += s1 + " " + p1 + " " + o1 + ".\n"
94 | }
95 | log(matches);
96 | resolve(matches);
97 | }
98 | catch(err) {
99 | log(`ERROR - ${err}`);
100 | resolve();
101 | }
102 | break;
103 |
104 | case "remove" :
105 | if(args[0] && args[0].startsWith("[")) {
106 | source = args.shift()
107 | source = source.replace(/^\[/,'').replace(/\]$/,'');
108 | source = mungeURL( source );
109 | }
110 | q = pr.parseQuery($rdf,args,source)
111 | try {
112 | for(var m of kb.match(q[0],q[1],q[2])){
113 | kb.remove(m);
114 | }
115 | resolve();
116 | }
117 | catch(err) {
118 | log(`Could not remove: ${err}`);
119 | resolve();
120 | }
121 | break;
122 |
123 | case "replace" :
124 | if(args[0].startsWith("[")) {
125 | source = args.shift()
126 | source = source.replace(/^\[/,'').replace(/\]$/,'');
127 | source = mungeURL( source );
128 | }
129 | let [s1,p1,o1,s2,p2,o2] = args;
130 | q = pr.parseQuery($rdf,["?",p1,o1],source)
131 | try {
132 | for(var m of kb.match(q[0],q[1],q[2])){
133 | kb.remove(m);
134 | q = pr.parseQuery($rdf,["?",p2,o2],source)
135 | kb.add( m.subject, q[1], q[2] );
136 | }
137 | resolve();
138 | }
139 | catch(err) {
140 | log(`Could not replace: ${err}`);
141 | resolve();
142 | }
143 | break;
144 |
145 | case "query" :
146 | try {
147 | /*
148 | [
149 | '?o': NamedNode { termType: 'NamedNode', classOrder: 5, value: ':#P' },
150 | '?p': NamedNode {
151 | termType: 'NamedNode',
152 | classOrder: 5,
153 | value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
154 | },
155 | '?s': NamedNode { termType: 'NamedNode', classOrder: 5, value: ':#K' }
156 | ]
157 | */
158 | sparql = 'PREFIX : <#>\n' + args.join(' ');
159 | const preparedQuery = $rdf.SPARQLToQuery( sparql, false, kb );
160 | let wanted = preparedQuery.vars;
161 | let results = [];
162 | console.log(sparql);
163 | kb.query(preparedQuery, (stmts)=>{
164 | console.log("got ",stmts.length);
165 | for(var s of stmts){
166 | results.push([s.subject.value,s.predicate.value,s.object.value])
167 | }
168 | console.log(results);
169 | resolve(results)
170 | })
171 | }
172 | catch(err) {
173 | log(`Could not run SPARQL - ${err}\n\n${sparql}`);
174 | resolve();
175 | }
176 | break;
177 |
178 | case "help" :
179 | case "h" :
180 | show("help","",verbosity);
181 | resolve();
182 | break;
183 |
184 | case "verbosity" :
185 | case "v" :
186 | let v = args[0]
187 | if(typeof(v)!="undefined" && v<5) {
188 | verbosity = v
189 | }
190 | else {
191 | do_err( "Bad verbosity level" )
192 | }
193 | resolve()
194 | break
195 |
196 | case "login" :
197 | let force = args[0];
198 | login(force).then( session => {
199 | resolve();
200 | }, err => reject("error logging in : "+err) );
201 | break;
202 |
203 | function showStatus( response, msg ){
204 | if(verbosity > 0) log( response.status + " " + msg );
205 | // log( response.status + " " + response.statusText + ", " + msg );
206 | }
207 |
208 | // REST METHODS
209 | case "get" :
210 | source = mungeURL(args[0]);
211 | if(!source) resolve();
212 | type = source.endsWith("/") ?"Container" :"Resource"
213 | if(statusOnly){
214 | fc.fetch(source).then( (response) => {
215 | showStatus(response,`GET ${type}`);
216 | resolve()
217 | },(response)=>{
218 | showStatus(response,`GET ${type} ${source}`);
219 | resolve()
220 | })
221 | }
222 | else if( source.endsWith("/") ){
223 | // log("\nfetching from folder "+source)
224 | fc.readFolder(source).then( folderObject => {
225 | show("folder",folderObject,verbosity);
226 | resolve()
227 | },err=>{
228 | showStatus(err,"get "+source);
229 | resolve()
230 | })
231 | }
232 | else {
233 | // log("\nfetching from file "+source);
234 | fc.readFile(source).then( fileBody =>{
235 | show("file",fileBody,verbosity);
236 | resolve()
237 | },err=>{
238 | showStatus(err,"get "+source);
239 | resolve()
240 | })
241 | }
242 | break;
243 |
244 | case "head" :
245 | source = mungeURL(args.shift());
246 | if(!source) resolve();
247 | type = source.endsWith("/") ?"Container" :"Resource"
248 | if(statusOnly){
249 | fc.head(source).then( response => {
250 | showStatus(response,"HEAD "+type);
251 | resolve()
252 | },err=>{
253 | showStatus(err,"head "+source);
254 | resolve()
255 | })
256 | }
257 | else {
258 | log("\nfetching head from "+source);
259 | fc.readHead(source).then( headers => {
260 | log(headers)
261 | resolve()
262 | },err=>{
263 | showStatus(err,"head "+source);
264 | resolve()
265 | })
266 | }
267 | break;
268 |
269 | case "options" :
270 | source = mungeURL(args.shift());
271 | if(!source) resolve();
272 | type = source.endsWith("/") ?"Container" :"Resource"
273 | if(statusOnly){
274 | fc.options(source).then( response => {
275 | showStatus(response,"OPTIONS "+type);
276 | resolve()
277 | },err=>{
278 | showStatus(err,"OPTIONS "+source);
279 | resolve()
280 | })
281 | }
282 | else {
283 | fc.options(source).then( options => {
284 | log(options)
285 | resolve()
286 | },err=>{
287 | showStatus(err,"OPTIONS "+source);
288 | resolve()
289 | })
290 | }
291 | break;
292 |
293 | case "put" :
294 | source = mungeURL(args.shift())
295 | if(!source) resolve();
296 | content = args.join(" ") || ""
297 | type = source.endsWith("/") ?"Container" :"Resource"
298 | if(statusOnly){
299 | let cType = getContentType(source);
300 | fc.put(source,{body:content,headers:{"content-type":cType}}).then( response => {
301 | showStatus(response,`PUT ${type}`);
302 | resolve()
303 | },response=>{
304 | showStatus(response,`PUT ${type} <${source}>`);
305 | resolve()
306 | })
307 | break;
308 | }
309 | if( source.endsWith("/") ){
310 | fc.createFolder(source).then( (r) => {
311 | showStatus(r,"put "+source)
312 | resolve()
313 | },err=>{
314 | showStatus(err,"put "+source)
315 | resolve()
316 | })
317 | }
318 | else {
319 | let cType = getContentType(source);
320 | fc.createFile(source,content,cType).then( (r) => {
321 | showStatus({status:"ok",statusText:"ok"},"put <"+source+">");
322 | resolve()
323 | },err=>{
324 | showStatus(err,"put "+source)
325 | resolve()
326 | })
327 | }
328 | break;
329 |
330 | case "post" :
331 | source = mungeURL(args.shift())
332 | if(!source) resolve();
333 | content = args.join(" ") || ""
334 | type = source.endsWith("/") ?"Container" :"Resource"
335 | if(statusOnly){
336 | let cType = getContentType(source);
337 | let link = source.endsWith("/") ?LINK.CONTAINER :LINK.RESOURCE
338 | fc.postItem(source,content,cType,link).then( (response) => {
339 | showStatus(response,`POST ${type}`);
340 | resolve()
341 | },response=>{
342 | showStatus(response,`POST ${type}`);
343 | resolve()
344 | })
345 | break;
346 | }
347 | if( source.endsWith("/") ){
348 | let cType = getContentType(source);
349 | fc.postItem(source,content,cType,LINK.CONTAINER).then( (response) => {
350 | showStatus(response,"post "+source);
351 | resolve();
352 | },err=>{
353 | showStatus(response,"post "+source);
354 | resolve();
355 | });
356 | }
357 | else {
358 | let cType = getContentType(source);
359 | fc.postItem(source,content,cType,LINK.RESOURCE).then( () => {
360 | log(`ok put <${source}>`)
361 | resolve()
362 | },err=>{ do_err(err,verbosity); resolve() })
363 | }
364 | break;
365 |
366 | case "patch" :
367 | source = mungeURL(args.shift())
368 | if(!source) resolve();
369 | content = args.join(" ") || ""
370 | let cType = getContentType(source);
371 | fc.patch(source, {
372 | body: content,
373 | method: 'PATCH',
374 | headers: {
375 | "Content-type" : "application/sparql-update",
376 | }
377 | }).then( (response) => {
378 | showStatus(response,`PATCH Resource`);
379 | resolve()
380 | },response=>{
381 | showStatus(response,`PATCH Resource`);
382 | resolve()
383 | })
384 | break;
385 |
386 | case "rm" :
387 | case "delete" :
388 | source = mungeURL(args[0]);
389 | if(!source) resolve();
390 | type = source.endsWith("/") ?"Container" :"Resource"
391 | if( source.endsWith("/") ){
392 | fc.delete(source).then( (response) => {
393 | showStatus({status:"ok",statusText:"ok"},"delete <"+source+">");
394 | resolve()
395 | },response=>{
396 | showStatus({status:"ok",statusText:"ok"},"delete <"+source+">");
397 | resolve()
398 | })
399 | }
400 | else {
401 | fc.deleteFile(source).then( (response) => {
402 | showStatus({status:"ok",statusText:"ok"},"delete <"+source+">");
403 | resolve();
404 | },response=>{
405 | showStatus({status:"ok",statusText:"ok"},"delete <"+source+">");
406 | resolve()
407 | })
408 | }
409 | break;
410 |
411 | case "recursiveDelete" :
412 | source = mungeURL(args[0]);
413 | if(!source) resolve();
414 | if( source.endsWith("/") ){
415 | try {
416 | let r = await fc.head(source);
417 | if(r.status==404){
418 | console.log(" ok recursiveDelete - already empty <",source+">");
419 | resolve();
420 | }
421 | else {
422 | try {
423 | response = await fc.deleteFolderRecursively(source);
424 | if(verbosity>0)
425 | console.log(" ok recursiveDelete, nothing to delete <"+source+">");
426 | }
427 | catch(e){console.log(e.status,e.statusText)}
428 | resolve()
429 | }
430 | }catch(e){
431 | if(e.status==404){
432 | console.log(" ok recursiveDelete - already empty <",source+">");
433 | resolve();
434 | }
435 | }
436 | }
437 | else {
438 | fc.deleteFile(source).then( (response) => {
439 | showStatus(response,"delete "+source);
440 | resolve();
441 | },err=>{
442 | if(err.status !=404) showStatus(err,"delete "+source);
443 | resolve()
444 | })
445 | }
446 | break;
447 |
448 | case "emptyFolder" :
449 | source = mungeURL(args[0]);
450 | if(!source) resolve();
451 | if( source.endsWith("/") ){
452 | fc.deleteFolderContents(source).then( (response) => {
453 | if(verbosity>0)
454 | showStatus({status:"",statusText:"ok"},"emptyFolder "+source);
455 | resolve()
456 | },err=>{
457 | if(err.status !=404) showStatus(err,"emptyFolder "+source);
458 | resolve()
459 | })
460 | }
461 | break;
462 |
463 | case "cp" :
464 | case "copy" :
465 | let opts = (args[2]&&args[2]=="noAuxFiles") ?{links:'exclude'} :{};
466 | //if(com==="cps") opts.merge="source"
467 | //if(com==="cpt") opts.merge="target"
468 | source = mungeURL(args[0]);
469 | target = mungeURL(args[1]);
470 | if(!source) resolve();
471 | if(!target) resolve();
472 | try {
473 | res = await fc.copy(source,target,opts)
474 | log(`ok copy <${source}>\n to <${target}>`);
475 | }
476 | catch(err){ do_err(err,verbosity); }
477 | resolve();
478 | break;
479 |
480 | case "copyNoAuxFiles" :
481 | opts = {links:'exclude'};
482 | //if(com==="cps") opts.merge="source"
483 | //if(com==="cpt") opts.merge="target"
484 | source = mungeURL(args[0]);
485 | target = mungeURL(args[1]);
486 | if(!source) resolve();
487 | if(!target) resolve();
488 | try {
489 | res = await fc.copy(source,target,opts)
490 | log(`ok copy <${source}>\n to <${target}>`);
491 | }
492 | catch(err){ do_err(err,verbosity); }
493 | resolve();
494 | break;
495 |
496 | case "zip" :
497 | source = mungeURL(args[0]);
498 | target = mungeURL(args[1]);
499 | if(!source) resolve();
500 | if(!target) resolve();
501 | fc.createZipArchive(source,target,{ blob: false }).then( () => {
502 | log(`ok zip to <${target}>`);
503 | resolve();
504 | },err=>{ do_err(err,verbosity); resolve() })
505 | break;
506 |
507 | case "unzip" :
508 | source = mungeURL(args[0]);
509 | target = mungeURL(args[1]);
510 | if(!source) resolve();
511 | if(!target) resolve();
512 | fc.extractZipArchive(source,target, { blob: false }).then( () => {
513 | log(`ok unzip to <${target}>`);
514 | resolve();
515 | },err=>{ do_err(err,verbosity); resolve() })
516 | break;
517 |
518 | case "mv" :
519 | case "move" :
520 | source = mungeURL(args[0]);
521 | target = mungeURL(args[1]);
522 | if(!source) resolve();
523 | if(!target) resolve();
524 | fc.move(source,target).then( () => {
525 | log(`ok move <${source}>\n to <${target}>`)
526 | resolve();
527 | },err=>{ do_err(err,verbosity); resolve() })
528 | break;
529 |
530 | case "run" :
531 | source = mungeURL(args[0]);
532 | if(!source) resolve();
533 | try {
534 | content = await fc.readFile(source)
535 | }catch(err){ do_err(err,verbosity); resolve() }
536 | let statements = content.split("\n")
537 | for(stmt of statements) {
538 | stmt = stmt.trim();
539 | if(stmt.match(/^#\s*END/)) break // stop on END
540 | if(stmt.length===0) continue // ignore blank line
541 | if(stmt.startsWith("#")) continue // ignore comment
542 | if(stmt.startsWith("!")){ // echo ! lines
543 | console.log( stmt.replace(/^\!\s*/,'') );
544 | continue;
545 | }
546 | let args=stmt.split(/\s+/)
547 | let c = args.shift()
548 | await runSol(c,args)
549 | }
550 | resolve()
551 | break;
552 |
553 | case "statusOnly" :
554 | statusOnly = args.shift().trim() === "true" ?true :false;
555 | resolve();
556 | break;
557 |
558 | case "base" :
559 | source = mungeURL(args.shift());
560 | if(!source){
561 | console.log("Must specify a URL!");
562 | resolve();
563 | }
564 | source = source.replace(/\/$/,'');
565 | credentials = credentials || {};
566 | credentials.base= rbase= process.env.SOLID_REMOTE_BASE= source;
567 | if(verbosity>0)
568 | log("Remote Base set to "+credentials.base);
569 | resolve(credentials.base);
570 | break;
571 |
572 | case "exists" :
573 | case "notExists" :
574 | source = mungeURL(args[0])
575 | try {
576 | let r = await fc.head(source);
577 | if(r.status==404) {
578 | if(com==="exists") log(`FAIL exists <${source}> ${e.status||""}`)
579 | if(com==="notExists") log(`ok notExists <${source}>`)
580 | }
581 | else {
582 | if(com==="exists") log(`ok exists <${source}>${r.status}`)
583 | if(com==="notExists") log(`FAIL notExists <${source}>${r.status}`)
584 | }
585 | }catch(e){
586 | if(e.status==404) {
587 | if(com==="exists") log(`FAIL exists <${source}> ${e.status||""}`)
588 | if(com==="notExists") log(`ok notExists <${source}>`)
589 | }
590 | }
591 | resolve()
592 | break;
593 |
594 | case "matchText" :
595 | source = mungeURL(args.shift())
596 | expected = args.join(' ');
597 | if(!source) {
598 | log("No source file specified!");
599 | resolve();
600 | }
601 | if(isFolder(source)){
602 | log("Can't use contentsMatch with a folder!");
603 | resolve();
604 | }
605 | try{
606 | fc.readFile(source).then( (got) => {
607 | const orgSource = source;
608 | source = path.basename(source)
609 | let results = got.indexOf(expected)>-1;
610 | if(results){
611 | log(`ok contentsMatch <${orgSource}>`)
612 | }
613 | else {
614 | log(`FAIL contentsMatch <${orgSource}, got: ${got}`)
615 | }
616 | resolve(results);
617 | },err=>{
618 | log("ERROR : Could not read "+source);
619 | resolve()
620 | })
621 | }catch(e){}
622 | break;
623 |
624 | default :
625 | if(com) log("can't parse command ",com)
626 | resolve();
627 | }});
628 | }
629 | /**
630 | * getCredentials()
631 | *
632 | * check for ~/.solid-auth-cli-config.json
633 | * if not found, check environment variables
634 | * prompt for things not found in either place
635 | * see solid-auth-cli for details
636 | */
637 | async function getCredentials(force){
638 | const e = process.env
639 | let creds = {};
640 | if( !force && e.SOLID_IDP && e.SOLID_USERNAME && e.SOLID_PASSWORD && e.SOLID_REMOTE_BASE) {
641 | return {
642 | idp:process.env.SOLID_IDP,
643 | username:process.env.SOLID_USERNAME,
644 | password:process.env.SOLID_PASSWORD,
645 | base:process.env.SOLID_REMOTE_BASE,
646 | }
647 | }
648 | else if( !force && e.SOLID_CLIENT_ID && e.SOLID_CLIENT_SECRET
649 | && e.SOLID_REFRESH_TOKEN && e.SOLID_OIDC_ISSUER
650 | ) {
651 | return {
652 | oidcIssuer:e.SOLID_OIDC_ISSUER,
653 | refreshToken:e.SOLID_REFRESH_TOKEN,
654 | clientId:e.SOLID_CLIENT_ID,
655 | clientSecret:e.SOLID_CLIENT_SECRET,
656 | base:process.env.SOLID_REMOTE_BASE,
657 | }
658 | }
659 | let idp = await shell.prompt("idp? ");
660 | let username = await shell.prompt("username? ");
661 | let password = await shell.prompt("password? ","mute");
662 | let base = rbase = await shell.prompt("remote base? ");
663 | return { idp, username, password, base }
664 | }
665 | /**
666 | * login()
667 | */
668 | async function login(force){
669 | log("logging in ...")
670 | try {
671 | credentials = await getCredentials(force)
672 | let session = await client.login(credentials)
673 | if(session.isLoggedIn) {
674 | log(`logged in as <${session.webId}>`)
675 | return session
676 | }
677 | else {
678 | log(`Could not log in
679 | to <${credentials.idp}>
680 | as "${credentials.username}"
681 | using a password that is ${credentials.password.length} characters long.
682 | `)
683 | }
684 | }catch(e){console.log('LOGIN ERROR : ',e)}
685 | }
686 | console.error = (msg) => {
687 | if(!msg.match(/fetchQueue/)) console.log(msg)
688 | }
689 | /*
690 | Verbosity
691 | 0 completely silent
692 | 1 report brief error messages
693 | 2 report brief error messages and steps
694 | 3 report full error messages and steps
695 | */
696 |
--------------------------------------------------------------------------------
/src/sol.shell.js:
--------------------------------------------------------------------------------
1 | const sol = require('./sol.run.js');
2 | const readlineSync = require('readline-sync');
3 |
4 | module.exports.sh = sh;
5 | module.exports.prompt = prompt;
6 |
7 | function prompt(question,mute) {
8 | return new Promise(function(resolve, reject) {
9 | if(mute) resolve( readlineSync.question(question,{hideEchoBack: true}))
10 | else resolve( readlineSync.question( question ) )
11 | });
12 | }
13 |
14 | function sh(){
15 | prompt("\n> ").then( data => {
16 | let args = data.split(/\s+/)
17 | let com = args.shift()
18 | if( com.match(/^(q|quit|exit)$/) ) process.exit();
19 | sol.runSol( com, args ).then(()=>{sh()},err=>{
20 | console.log(err);
21 | sh(); // recurse
22 | });
23 | });
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/src/sol.show.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const {shellVersion} = require('./utils.js');
3 |
4 | module.exports = async function(type,thing,verbosity){
5 | switch(type) {
6 | case "file" :
7 | log(thing,verbosity)
8 | break
9 | case "folder" :
10 | /*
11 | * TBD : full folder listing with size, stat, perms
12 | */
13 | let fo = ( thing.folders.length>0 ) ? "Folders: " : ""
14 | let fi = ( thing.files.length>0 ) ? "Files: " : ""
15 | for(var o in thing.folders ){
16 | fo = fo + thing.folders[o].name + " "
17 | }
18 | let folderLinks = thing.links
19 | if( folderLinks ) {
20 | if( folderLinks.meta )
21 | fi = fi + path.basename(folderLinks.meta) + " "
22 | if( folderLinks.acl )
23 | fi = fi + path.basename(folderLinks.acl) + " "
24 | }
25 | for(var i in thing.files ){
26 | fi = fi + thing.files[i].name + " "
27 | let fileLinks = thing.files[i].links || {}
28 | if( fileLinks.meta ){
29 | fi = fi + path.basename(fileLinks.meta) + " "
30 | }
31 | if( fileLinks.acl ){
32 | fi = fi + path.basename(fileLinks.acl) + " "
33 | }
34 | }
35 | log(fo+"\n"+fi,verbosity)
36 | break;
37 |
38 | case "help" :
39 | let version = await shellVersion();
40 | log(`------------------------------------------------------------------------------
41 | Solid Shell ${version} - a command line, batch processor, & interactive shell
42 | ------------------------------------------------------------------------------
43 | Document Management put, get, head, copy, move, delete, zip, unzip
44 | Testing/Batch Processing run, exists, notExists, matchText
45 | Connectivity login, base
46 | Interactive Shell help, quit
47 | ------------------------------------------------------------------------------
48 | URLs beginging with / = remote base, ./ = local cwd, or use https: and file:
49 | ------------------------------------------------------------------------------`
50 | ,verbosity)
51 | break
52 | }
53 | }
54 | function log(msg,verbosity) {
55 | if(verbosity>0){
56 | console.log(msg)
57 | }
58 | }
59 |
60 | /*
61 | Document Management put, get, head, copy, move, delete, zip, unzip
62 | Semantic Management load, putBack, add, remove, match
63 | Testing/Batch Processing run, exists, notExists, matchText, verbosity
64 | Connectivity login, base
65 | Interactive Shell help, quit
66 |
67 | put create a file or folder with Write permission
68 | post create a file or folder with Append permission
69 | get read a file or folder
70 | head show headers for an item
71 | copy copy a file or recursively copy a folder
72 | move move a file or recursively move a folder
73 | delete delete a file or recursively delete a folder
74 | run run a batch file of sol commands
75 | load loads a file into an in-memory store
76 | putBack writes an in-memory store to a file
77 | add adds a triple to the in-memory store
78 | remove removes a triple from the in-memory store
79 | login login to a Solid server (see README for details)
80 | base set the remote base (will be prepended to /)
81 | verbosity set verbosity level
82 | help show this help
83 | quit quit
84 | matchTriple true if triple is found in the in-memory store
85 | exists true if URL exists & is readable by current user
86 | notExists true if URL does not exist or is not readable
87 | matchText true if contents of URL match supplied text`
88 |
89 | https://* absolute address in a Solid server
90 | /* addresss in a Solid server relative to a user-defined base
91 | file://* absolute address in a local serverless file system
92 | ./* addresss in a local file system relative to current directory`
93 |
94 | URL starts with https:// absolute remote address
95 | file:// absolute local address
96 | / remote address relative to specified base
97 | ./ local address relative to current working folder
98 |
99 | URL ends with / folder (e.g. /foo/ is a folder )
100 | no / file (e.g. /foo is a file )`
101 | */
102 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const contentTypeLookup = require('mime-types').contentType;
2 |
3 | let lbase = process.platform==="win32" ? `file:///${process.cwd().replace(/\\/g,'/')}` : "file://" + process.cwd();
4 |
5 |
6 | async function packVers(pname){
7 |
8 | try {
9 | // Get the path to the package.json file of the package
10 | const packageJsonPath = require.resolve(`${pname}/package.json`);
11 |
12 | // Load the package.json file
13 | let packageJson = require(packageJsonPath);
14 |
15 | // Extract the version of the package
16 | const packageVersion = packageJson.version;
17 | if(pname=='../')pname='solid-shell';
18 | return([pname,packageVersion]);
19 | } catch (error) {
20 | // console.error(`Failed to determine the version of ${pname}: ${error.message}`);
21 | }
22 | }
23 |
24 | async function shellVersion(){
25 | return (await packVers('../'))[1];
26 | }
27 | async function packageVersions(packages){
28 | let str = "";
29 | for(let p of packages){
30 | p = await packVers(p);
31 | str += `${p[0]}:${p[1]}, `;
32 | }
33 | console.log( " "+str.replace(/, $/,'') );
34 | }
35 |
36 | function unMunge(url) {
37 | if(!url) return
38 | let rbase = process.env.SOLID_REMOTE_BASE;
39 | const lregex = new RegExp("\^"+lbase);
40 | const rregex = new RegExp("\^"+rbase);
41 | url = url.replace( lregex, "." ).replace( rregex, "" );
42 | return url
43 | }
44 | /**
45 | * mungeURL()
46 | *
47 | * adds the base pod location to remote relative URLs (start with /)
48 | * adds the current working folder to local relative URLs (start with ./)
49 | *
50 | *
51 | */
52 | function mungeURL(url) {
53 | let rbase = process.env.SOLID_REMOTE_BASE;
54 | if(!url) return
55 | if( url.match(/^https:\/[^/]/) ){
56 | url = url.replace(/^https:\//,"https://")
57 | }
58 | if( url.match(/^\//) && rbase ){
59 | return rbase + url;
60 | }
61 | else if( url.match(/^\.\//) ){
62 | return lbase + url.replace(/\./,'');
63 | }
64 | else if( !url.match(/^(http|file|app)/) ){
65 | console.log(url);
66 | console.log("URL must start with https:// or file:// or / or ./")
67 | return false
68 | }
69 | return url
70 | }
71 | function isFolder(thing){
72 | return thing.endsWith('/');
73 | }
74 | function do_err(err,verbosity){
75 | verbosity ||=4;
76 | console.log(`Error: ${(err.status||"unknown")} ${(err.statusText||"")}`)
77 | if(verbosity>3) console.log(err.message);
78 | }
79 | function log(msg) {
80 | // if(verbosity==2 || verbosity==3){
81 | console.log(" "+msg)
82 | // }
83 | }
84 | function getContentType(path) {
85 | if ( path.endsWith('/')
86 | || path.endsWith('.meta')
87 | || path.endsWith('.acl')
88 | || path.endsWith('.ttl')
89 | ){
90 | return 'text/turtle';
91 | }
92 | else {
93 | let extension = path.replace(/.*\./,'');
94 | let cType = contentTypeLookup(extension);
95 | return cType===extension ?null :cType;
96 | }
97 | }
98 |
99 | module.exports = {
100 | mungeURL, unMunge, isFolder, do_err, log, getContentType, packageVersions, shellVersion
101 | }
102 |
--------------------------------------------------------------------------------