├── .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 | ![npm](https://badge.fury.io/js/solid-shell.svg) 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 | --------------------------------------------------------------------------------