1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

...

56 Commits

Author SHA1 Message Date
Saad Jutt
fb4f3442d5 chore(release): 0.0.47 2022-03-30 01:04:45 +05:00
Muhammad Saad
09d1b7d5d4 Merge pull request #114 from sasjs/show-contents-as-api-responded
fix: show contents as api responded
2022-03-30 01:04:29 +05:00
Saad Jutt
99839ae62f chore: fix vulnerabilities 2022-03-30 00:55:59 +05:00
Saad Jutt
f700561e1a fix(web): updated STUDIO log and webout 2022-03-30 00:55:38 +05:00
Saad Jutt
8b4b4b91ab chore(release): 0.0.46 2022-03-30 00:01:18 +05:00
Muhammad Saad
acb3ae0493 Merge pull request #113 from sasjs/issue-108
feat(drive): GET folder contents API added
2022-03-30 00:00:52 +05:00
Saad Jutt
f48aeb1b0b chore(release): 0.0.45 2022-03-29 23:38:09 +05:00
Saad Jutt
5c0e8e5344 chore: Merge branch 'main' into issue-108 2022-03-29 23:28:03 +05:00
Saad Jutt
0ac9e4af7d feat(drive): GET folder contents API added 2022-03-29 23:27:44 +05:00
Muhammad Saad
ee80f3f968 Merge pull request #112 from sasjs/allanbowe/lst-destination-needs-111
fix: providing a PRINT destination during SAS invocation.
2022-03-29 23:11:32 +05:00
Allan Bowe
7f4201ba85 fix: proving a PRINT destination during SAS invocation.
Closes #111
2022-03-29 16:11:15 +00:00
munja
f830bbc058 chore(release): 0.0.44 2022-03-29 11:25:00 +01:00
Allan Bowe
f8e1522a5a Merge pull request #110 from sasjs/issue-108
fix: increased req body size
2022-03-28 14:22:44 +03:00
Saad Jutt
0a5aeceab5 fix: DELETE req cannot have body 2022-03-28 05:05:03 +05:00
Saad Jutt
6dc39c0d91 fix: increased req body size 2022-03-28 04:53:24 +05:00
Saad Jutt
117a53ceea fix(session): increased session + bug fixed 2022-03-24 20:22:06 +05:00
Saad Jutt
dd56a95314 fix(stp): use same session from file upload 2022-03-24 18:06:28 +05:00
Saad Jutt
c5117abe71 chore: README.md updated 2022-03-24 05:56:08 +05:00
Saad Jutt
84c632a861 chore(release): 0.0.43 2022-03-24 04:25:40 +05:00
Muhammad Saad
3ddd09eba0 Merge pull request #105 from sasjs/deploy-app-with-current-names-also
Deploy app with current names also
2022-03-24 04:25:12 +05:00
Saad Jutt
0c0301433c test: fixed 2022-03-24 04:22:30 +05:00
Saad Jutt
954b2e3e2e chore: removed test file 2022-03-24 01:12:04 +05:00
Saad Jutt
5655311b96 fix: fallback logo on AppStream 2022-03-24 01:07:06 +05:00
Saad Jutt
9ace33d783 fix(deploy): user can deploy to same appName with different/same appLoc 2022-03-24 00:54:59 +05:00
Saad Jutt
adc5aca0f0 chore(release): 0.0.42 2022-03-23 22:49:29 +05:00
Muhammad Saad
71c6be6b84 Merge pull request #104 from sasjs/webout-raw
fix: execute api, webout as raw
2022-03-23 22:47:07 +05:00
Saad Jutt
9c751877d1 fix: execute api, webout as raw 2022-03-23 22:41:02 +05:00
Saad Jutt
2204d54cd6 chore(release): 0.0.41 2022-03-23 21:32:06 +05:00
Saad Jutt
f4eb75ff34 fix(scroll): closes #100 2022-03-23 21:31:54 +05:00
Saad Jutt
a3cde343b7 chore(release): 0.0.40 2022-03-23 20:17:15 +05:00
Saad Jutt
7a70d40dbf fix: macros available for SAS 2022-03-23 20:15:18 +05:00
Saad Jutt
d27e070fc8 fix: moved macros from codebase to drive
This reverts commit d2956fc641.
2022-03-23 19:38:29 +05:00
Saad Jutt
27e260e6a4 fix(deploy): validating empty file or service in filetree 2022-03-23 19:38:20 +05:00
Saad Jutt
2796db8ead chore(release): 0.0.39 2022-03-23 18:07:12 +05:00
Muhammad Saad
84f7c2ab89 Merge pull request #103 from sasjs/executable-macros-fix
Executable macros fix
2022-03-23 18:07:01 +05:00
Saad Jutt
e68090181a fix: included sasjs core macros at compile time 2022-03-23 18:05:03 +05:00
Saad Jutt
d2956fc641 Revert "fix: moved macros from codebase to drive"
This reverts commit 9ac3191891.
2022-03-23 17:59:06 +05:00
Saad Jutt
a701bb25e7 Revert "fix: quick fix for executables"
This reverts commit 9e53470947.
2022-03-23 17:58:18 +05:00
Saad Jutt
5758bcd392 chore(release): 0.0.38 2022-03-23 17:16:01 +05:00
Saad Jutt
9e53470947 fix: quick fix for executables 2022-03-23 17:15:46 +05:00
Saad Jutt
81f6605249 chore(release): 0.0.37 2022-03-23 09:23:20 +05:00
Muhammad Saad
0b45402946 Merge pull request #102 from sasjs/issue-95
fix: moved macros from codebase to drive
2022-03-23 09:23:01 +05:00
Saad Jutt
9ac3191891 fix: moved macros from codebase to drive 2022-03-23 09:19:33 +05:00
Saad Jutt
cd00aa2af8 fix: appStream html view 2022-03-22 21:52:39 +05:00
Saad Jutt
0147bcb701 fix(webin): closes #99 2022-03-22 21:28:31 +05:00
Saad Jutt
bf53ad30f4 chore(release): 0.0.36 2022-03-22 04:07:27 +05:00
Muhammad Saad
a003b8836b Merge pull request #98 from sasjs/issue-91
feat: App Stream
2022-03-22 04:02:26 +05:00
Saad Jutt
df6003df94 fix(appstream): app logo + improvements 2022-03-22 03:55:51 +05:00
Saad Jutt
b1d0fdbb02 chore(release): 0.0.35 2022-03-21 18:24:35 +05:00
Muhammad Saad
2c34395110 Merge pull request #97 from sasjs/issue-96
feat(cors): whitelisting is configurable through .env variables
2022-03-21 18:21:39 +05:00
Saad Jutt
534e4e5bf3 chore: README.md updated 2022-03-21 18:17:26 +05:00
Saad Jutt
6146372eba chore: README.md updated 2022-03-21 18:05:40 +05:00
Saad Jutt
aaa469a142 chore: .env.example updated 2022-03-21 17:54:20 +05:00
Saad Jutt
4fd5bf948e fix(cors): removed trailing slashes of urls 2022-03-21 17:49:28 +05:00
Saad Jutt
99f91fbce2 feat(cors): whitelisting is configurable through .env variables 2022-03-21 17:36:42 +05:00
Saad Jutt
98a00ec7ac feat: App Stream, load on startup, new route added 2022-03-21 17:17:29 +05:00
39 changed files with 1039 additions and 295 deletions

View File

@@ -2,6 +2,124 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
### Bug Fixes
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
### Features
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
### Bug Fixes
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
### Bug Fixes
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
### Bug Fixes
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
### Bug Fixes
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
### Bug Fixes
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
### Bug Fixes
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
### Bug Fixes
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
### Bug Fixes
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
### Bug Fixes
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
### Features
* App Stream, load on startup, new route added ([98a00ec](https://github.com/sasjs/server/commit/98a00ec7ace5da765f049864799be44ba6538e8a))
### Bug Fixes
* **appstream:** app logo + improvements ([df6003d](https://github.com/sasjs/server/commit/df6003df942fd52b956f3d4069d6d7615441d372))
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
### Features
* **cors:** whitelisting is configurable through .env variables ([99f91fb](https://github.com/sasjs/server/commit/99f91fbce2a029dd963ed30c9007a9b046ea6560))
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
### Bug Fixes
* **cors:** removed trailing slashes of urls ([4fd5bf9](https://github.com/sasjs/server/commit/4fd5bf948e4ad8a274d3176d5509163e67980061))
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18) ### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18)

View File

@@ -13,7 +13,7 @@ SASjs Server is available in two modes - Desktop (without authentication) and Se
## Installation ## Installation
Installation can be made programmatically using command line, or by manually downloading and running the executable. Installation can be made programmatically using command line, or by manually downloading and running the executable.
### Programmatic ### Programmatic
@@ -48,16 +48,42 @@ When launching the app, it will make use of specific environment variables. Thes
Example contents of a `.env` file: Example contents of a `.env` file:
``` ```
MODE=desktop # options: [desktop|server] default: desktop # options: [desktop|server] default: `desktop`
CORS=disable # options: [disable|enable] default: disable MODE=
PROTOCOL=http # options: [http|https] default: http
PORT=5000 # default: 5000 # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
PORT_WEB=3000 # port for sasjs web component(react). default: 3000 CORS=
# options: <http://localhost:3000 https://abc.com ...> space separated urls
WHITELIST=
# options: [http|https] default: http
PROTOCOL=
# default: 5000
PORT=
# optional
# for MODE: `desktop`, prompts user
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
SAS_PATH=/path/to/sas/executable.exe SAS_PATH=/path/to/sas/executable.exe
# optional
# for MODE: `desktop`, prompts user
# for MODE: `server` defaults to /tmp
DRIVE_PATH=/tmp DRIVE_PATH=/tmp
PROTOCOL=http # options: [http|https] default: http
# ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem FULL_CHAIN=fullchain.pem
# ENV variables required for MODE: `server`
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
``` ```
## Persisting the Session ## Persisting the Session
@@ -94,11 +120,10 @@ Instead of `app_name` you can pass:
- `all` to act on all processes - `all` to act on all processes
- `id` to act on a specific process id - `id` to act on a specific process id
## Server Version ## Server Version
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use. The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
* CLIENTID: `clientID1` - CLIENTID: `clientID1`
* USERNAME: `secretuser` - USERNAME: `secretuser`
* PASSWORD: `secretpassword` - PASSWORD: `secretpassword`

View File

@@ -1,10 +1,10 @@
MODE=[desktop|server] default considered as desktop MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem FULL_CHAIN=fullchain.pem
PORT=[5000] default value is 5000 PORT=[5000] default value is 5000
PORT_WEB=[port for sasjs web component(react)] default value is 3000
ACCESS_TOKEN_SECRET=<secret> ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret> REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret> AUTH_CODE_SECRET=<secret>

12
api/package-lock.json generated
View File

@@ -7107,9 +7107,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
@@ -15624,9 +15624,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"mkdirp": { "mkdirp": {
"version": "1.0.4", "version": "1.0.4",

View File

@@ -29,6 +29,7 @@
"assets": [ "assets": [
"./build/public/**/*", "./build/public/**/*",
"./build/sasjsbuild/**/*", "./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./web/build/**/*" "./web/build/**/*"
], ],
"targets": [ "targets": [
@@ -88,5 +89,10 @@
}, },
"configuration": { "configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas" "sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
},
"nodemonConfig": {
"ignore": [
"tmp/**/*"
]
} }
} }

21
api/public/sasjs-logo.svg Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F6E40C;}
</style>
<rect id="XMLID_1_" width="32" height="32"/>
<g id="XMLID_654_">
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -161,6 +161,8 @@ components:
$ref: '#/components/schemas/FolderMember' $ref: '#/components/schemas/FolderMember'
- -
$ref: '#/components/schemas/ServiceMember' $ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
type: array type: array
required: required:
- name - name
@@ -172,20 +174,30 @@ components:
enum: enum:
- service - service
type: string type: string
MemberType.file:
enum:
- file
type: string
ServiceMember: ServiceMember:
properties: properties:
name: name:
type: string type: string
type: type:
anyOf: $ref: '#/components/schemas/MemberType.service'
- code:
$ref: '#/components/schemas/MemberType.service' type: string
- required:
$ref: '#/components/schemas/MemberType.file' - name
- type
- code
type: object
additionalProperties: false
MemberType.file:
enum:
- file
type: string
FileMember:
properties:
name:
type: string
type:
$ref: '#/components/schemas/MemberType.file'
code: code:
type: string type: string
required: required:
@@ -203,6 +215,8 @@ components:
$ref: '#/components/schemas/FolderMember' $ref: '#/components/schemas/FolderMember'
- -
$ref: '#/components/schemas/ServiceMember' $ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
type: array type: array
required: required:
- members - members
@@ -214,6 +228,8 @@ components:
type: string type: string
message: message:
type: string type: string
streamServiceName:
type: string
example: example:
$ref: '#/components/schemas/FileTree' $ref: '#/components/schemas/FileTree'
required: required:
@@ -225,6 +241,8 @@ components:
properties: properties:
appLoc: appLoc:
type: string type: string
streamWebFolder:
type: string
fileTree: fileTree:
$ref: '#/components/schemas/FileTree' $ref: '#/components/schemas/FileTree'
required: required:
@@ -594,7 +612,6 @@ paths:
responses: responses:
'204': '204':
description: 'No content' description: 'No content'
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Get file from SASjs Drive' summary: 'Get file from SASjs Drive'
tags: tags:
- Drive - Drive
@@ -605,19 +622,10 @@ paths:
- -
in: query in: query
name: _filePath name: _filePath
required: false required: true
schema: schema:
type: string type: string
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
requestBody:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
filePath:
type: string
delete: delete:
operationId: DeleteFile operationId: DeleteFile
responses: responses:
@@ -631,7 +639,6 @@ paths:
required: required:
- status - status
type: object type: object
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Delete file from SASjs Drive' summary: 'Delete file from SASjs Drive'
tags: tags:
- Drive - Drive
@@ -642,19 +649,10 @@ paths:
- -
in: query in: query
name: _filePath name: _filePath
required: false required: true
schema: schema:
type: string type: string
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
requestBody:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
filePath:
type: string
post: post:
operationId: SaveFile operationId: SaveFile
responses: responses:
@@ -757,6 +755,36 @@ paths:
type: string type: string
required: required:
- file - file
/SASjsApi/drive/folder:
get:
operationId: GetFolder
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
folders: {items: {type: string}, type: array}
files: {items: {type: string}, type: array}
required:
- folders
- files
type: object
summary: 'Get folder contents from SASjs Drive'
tags:
- Drive
security:
-
bearerAuth: []
parameters:
-
in: query
name: _folderPath
required: false
schema:
type: string
example: /Public/somefolder
/SASjsApi/drive/filetree: /SASjsApi/drive/filetree:
get: get:
operationId: GetFileTree operationId: GetFileTree

View File

@@ -1,7 +1,14 @@
import path from 'path' import path from 'path'
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils' import {
asyncForEach,
copy,
createFile,
createFolder,
deleteFolder,
listFilesInFolder
} from '@sasjs/utils'
import { apiRoot, sasJSCoreMacros } from '../src/utils' import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core') const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
@@ -16,6 +23,10 @@ export const copySASjsCore = async () => {
await copy(coreSubFolderPath, sasJSCoreMacros) await copy(coreSubFolderPath, sasJSCoreMacros)
}) })
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
} }
copySASjsCore() copySASjsCore()

View File

@@ -7,29 +7,34 @@ import cors from 'cors'
import { import {
connectDB, connectDB,
copySASjsCore,
getWebBuildFolderPath, getWebBuildFolderPath,
sasJSCoreMacros, loadAppStreamConfig,
setProcessVariables setProcessVariables,
setupFolders
} from './utils' } from './utils'
dotenv.config() dotenv.config()
const app = express() const app = express()
const { MODE, CORS, PORT_WEB } = process.env const { MODE, CORS, WHITELIST } = process.env
const whiteList = [
`http://localhost:${PORT_WEB ?? 3000}`,
'https://sas.analytium.co.uk:8343'
]
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
console.log('All CORS Requests are enabled') const whiteList: string[] = []
WHITELIST?.split(' ')?.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList })) app.use(cors({ credentials: true, origin: whiteList }))
} }
app.use(cookieParser()) app.use(cookieParser())
app.use(morgan('tiny')) app.use(morgan('tiny'))
app.use(express.json({ limit: '50mb' })) app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public'))) app.use(express.static(path.join(__dirname, '../public')))
const onError: ErrorRequestHandler = (err, req, res, next) => { const onError: ErrorRequestHandler = (err, req, res, next) => {
@@ -38,17 +43,20 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
} }
export default setProcessVariables().then(async () => { export default setProcessVariables().then(async () => {
await setupFolders()
await copySASjsCore()
// loading these modules after setting up variables due to // loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc // multer's usage of process var process.driveLoc
const { setupRoutes } = await import('./routes/setupRoutes') const { setupRoutes } = await import('./routes/setupRoutes')
setupRoutes(app) setupRoutes(app)
await loadAppStreamConfig()
// should be served after setting up web route // should be served after setting up web route
// index.html needs to be injected with some js script. // index.html needs to be injected with some js script.
app.use(express.static(getWebBuildFolderPath())) app.use(express.static(getWebBuildFolderPath()))
console.log('sasJSCoreMacros', sasJSCoreMacros)
app.use(onError) app.use(onError)
await connectDB() await connectDB()

View File

@@ -20,7 +20,12 @@ import {
fileExists, fileExists,
moveFile, moveFile,
createFolder, createFolder,
deleteFile as deleteFileOnSystem deleteFile as deleteFileOnSystem,
folderExists,
listFilesAndSubFoldersInFolder,
listFilesInFolder,
listSubFoldersInFolder,
isFolder
} from '@sasjs/utils' } from '@sasjs/utils'
import { createFileTree, ExecutionController, getTreeExample } from './internal' import { createFileTree, ExecutionController, getTreeExample } from './internal'
@@ -29,12 +34,14 @@ import { getTmpFilesFolderPath } from '../utils'
interface DeployPayload { interface DeployPayload {
appLoc: string appLoc: string
streamWebFolder?: string
fileTree: FileTree fileTree: FileTree
} }
interface DeployResponse { interface DeployResponse {
status: string status: string
message: string message: string
streamServiceName?: string
example?: FileTree example?: FileTree
} }
@@ -87,9 +94,6 @@ export class DriveController {
} }
/** /**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
* *
* @summary Get file from SASjs Drive * @summary Get file from SASjs Drive
* @query _filePath Location of SAS program * @query _filePath Location of SAS program
@@ -98,28 +102,31 @@ export class DriveController {
@Get('/file') @Get('/file')
public async getFile( public async getFile(
@Request() request: express.Request, @Request() request: express.Request,
@Query() _filePath: string
@Query() _filePath?: string,
@FormField() filePath?: string
) { ) {
return getFile(request, (_filePath ?? filePath)!) return getFile(request, _filePath)
}
/**
*
* @summary Get folder contents from SASjs Drive
* @query _folderPath Location of SAS program
* @example _folderPath "/Public/somefolder"
*/
@Get('/folder')
public async getFolder(@Query() _folderPath?: string) {
return getFolder(_folderPath)
} }
/** /**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
* *
* @summary Delete file from SASjs Drive * @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program * @query _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file" * @example _filePath "/Public/somefolder/some.file"
*/ */
@Delete('/file') @Delete('/file')
public async deleteFile( public async deleteFile(@Query() _filePath: string) {
@Query() _filePath?: string, return deleteFile(_filePath)
@FormField() filePath?: string
) {
return deleteFile((_filePath ?? filePath)!)
} }
/** /**
@@ -190,14 +197,23 @@ const getFileTree = () => {
} }
const deploy = async (data: DeployPayload) => { const deploy = async (data: DeployPayload) => {
const driveFilesPath = getTmpFilesFolderPath()
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
const appLocPath = path
.join(getTmpFilesFolderPath(), ...appLocParts)
.replace(new RegExp('/', 'g'), path.sep)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
if (!isFileTree(data.fileTree)) { if (!isFileTree(data.fileTree)) {
throw { code: 400, ...invalidDeployFormatResponse } throw { code: 400, ...invalidDeployFormatResponse }
} }
await createFileTree( await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
data.fileTree.members,
data.appLoc.replace(/^\//, '').split('/')
).catch((err) => {
throw { code: 500, ...execDeployErrorResponse, ...err } throw { code: 500, ...execDeployErrorResponse, ...err }
}) })
@@ -216,7 +232,7 @@ const getFile = async (req: express.Request, filePath: string) => {
} }
if (!(await fileExists(filePathFull))) { if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.') throw new Error("File doesn't exist.")
} }
const extension = path.extname(filePathFull).toLowerCase() const extension = path.extname(filePathFull).toLowerCase()
@@ -227,6 +243,36 @@ const getFile = async (req: express.Request, filePath: string) => {
req.res?.sendFile(path.resolve(filePathFull)) req.res?.sendFile(path.resolve(filePathFull))
} }
const getFolder = async (folderPath?: string) => {
const driveFilesPath = getTmpFilesFolderPath()
if (folderPath) {
const folderPathFull = path
.join(getTmpFilesFolderPath(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath)) {
throw new Error('Cannot get folder outside drive.')
}
if (!(await folderExists(folderPathFull))) {
throw new Error("Folder doesn't exist.")
}
if (!(await isFolder(folderPathFull))) {
throw new Error('Not a Folder.')
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
return { files, folders }
}
const files: string[] = await listFilesInFolder(driveFilesPath)
const folders: string[] = await listSubFoldersInFolder(driveFilesPath)
return { files, folders }
}
const deleteFile = async (filePath: string) => { const deleteFile = async (filePath: string) => {
const driveFilesPath = getTmpFilesFolderPath() const driveFilesPath = getTmpFilesFolderPath()
@@ -294,9 +340,3 @@ const updateFile = async (
return { status: 'success' } return { status: 'success' }
} }
const validateFilePath = async (filePath: string) => {
if (!(await fileExists(filePath))) {
throw 'DriveController: File does not exists.'
}
}

View File

@@ -8,14 +8,14 @@ import {
moveFile, moveFile,
readFileBinary readFileBinary
} from '@sasjs/utils' } from '@sasjs/utils'
import { PreProgramVars, TreeNode } from '../../types' import { PreProgramVars, Session, TreeNode } from '../../types'
import { import {
extractHeaders, extractHeaders,
generateFileUploadSasCode, generateFileUploadSasCode,
getTmpFilesFolderPath, getTmpFilesFolderPath,
getTmpMacrosPath,
HTTPHeaders, HTTPHeaders,
isDebugOn, isDebugOn
sasJSCoreMacros
} from '../../utils' } from '../../utils'
export interface ExecutionVars { export interface ExecutionVars {
@@ -39,7 +39,8 @@ export class ExecutionController {
preProgramVariables: PreProgramVars, preProgramVariables: PreProgramVars,
vars: ExecutionVars, vars: ExecutionVars,
otherArgs?: any, otherArgs?: any,
returnJson?: boolean returnJson?: boolean,
session?: Session
) { ) {
if (!(await fileExists(programPath))) if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.' throw 'ExecutionController: SAS file does not exist.'
@@ -51,7 +52,8 @@ export class ExecutionController {
preProgramVariables, preProgramVariables,
vars, vars,
otherArgs, otherArgs,
returnJson returnJson,
session
) )
} }
@@ -60,11 +62,13 @@ export class ExecutionController {
preProgramVariables: PreProgramVars, preProgramVariables: PreProgramVars,
vars: ExecutionVars, vars: ExecutionVars,
otherArgs?: any, otherArgs?: any,
returnJson?: boolean returnJson?: boolean,
sessionByFileUpload?: Session
): Promise<ExecuteReturnRaw | ExecuteReturnJson> { ): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController = getSessionController() const sessionController = getSessionController()
const session = await sessionController.getSession() const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true session.inUse = true
session.consumed = true session.consumed = true
@@ -106,7 +110,7 @@ export class ExecutionController {
` `
program = ` program = `
options insert=(SASAUTOS="${sasJSCoreMacros}"); options insert=(SASAUTOS="${getTmpMacrosPath()}");
/* runtime vars */ /* runtime vars */
${varStatments} ${varStatments}

View File

@@ -23,7 +23,9 @@ export class FileUploadController {
const sessionController = getSessionController() const sessionController = getSessionController()
session = await sessionController.getSession() session = await sessionController.getSession()
session.inUse = true // marking consumed true, so that it's not available
// as readySession for any other request
session.consumed = true
req.sasSession = session req.sasSession = session

View File

@@ -31,7 +31,7 @@ export class SessionController {
? readySessions[0] ? readySessions[0]
: await this.createSession() : await this.createSession()
if (readySessions.length < 2) this.createSession() if (readySessions.length < 3) this.createSession()
return session return session
} }
@@ -87,6 +87,8 @@ ${autoExecContent}`
codePath, codePath,
'-LOG', '-LOG',
path.join(session.path, 'log.log'), path.join(session.path, 'log.log'),
'-PRINT',
path.join(session.path, 'output.lst'),
'-WORK', '-WORK',
session.path, session.path,
'-AUTOEXEC', '-AUTOEXEC',

View File

@@ -1,11 +1,17 @@
import path from 'path' import path from 'path'
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types' import {
MemberType,
FolderMember,
ServiceMember,
FileTree,
FileMember
} from '../../types'
import { getTmpFilesFolderPath } from '../../utils/file' import { getTmpFilesFolderPath } from '../../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils' import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller // REFACTOR: export FileTreeCpntroller
export const createFileTree = async ( export const createFileTree = async (
members: (FolderMember | ServiceMember)[], members: (FolderMember | ServiceMember | FileMember)[],
parentFolders: string[] = [] parentFolders: string[] = []
) => { ) => {
const destinationPath = path.join( const destinationPath = path.join(
@@ -13,29 +19,32 @@ export const createFileTree = async (
path.join(...parentFolders) path.join(...parentFolders)
) )
await asyncForEach(members, async (member: FolderMember | ServiceMember) => { await asyncForEach(
let name = member.name members,
async (member: FolderMember | ServiceMember | FileMember) => {
let name = member.name
if (member.type === MemberType.service) name += '.sas' if (member.type === MemberType.service) name += '.sas'
if (member.type === MemberType.folder) { if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) => await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name }) Promise.reject({ error: err, failedToCreate: name })
) )
await createFileTree(member.members, [...parentFolders, name]).catch( await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name }) (err) => Promise.reject({ error: err, failedToCreate: name })
) )
} else { } else {
const encoding = member.type === MemberType.file ? 'base64' : undefined const encoding = member.type === MemberType.file ? 'base64' : undefined
await createFile( await createFile(
path.join(destinationPath, name), path.join(destinationPath, name),
member.code, member.code,
encoding encoding
).catch((err) => Promise.reject({ error: err, failedToCreate: name })) ).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
}
} }
}) )
return Promise.resolve() return Promise.resolve()
} }

View File

@@ -143,9 +143,8 @@ const executeReturnRaw = async (
query query
)) as ExecuteReturnRaw )) as ExecuteReturnRaw
// Should over-ride response header for // Should over-ride response header for debug
// debug on GET request to see entire log // on GET request to see entire log rendering on browser.
// rendering on browser.
if (isDebugOn(query)) { if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain' httpHeaders['content-type'] = 'text/plain'
} }
@@ -185,7 +184,8 @@ const executeReturnJson = async (
getPreProgramVariables(req), getPreProgramVariables(req),
{ ...req.query, ...req.body }, { ...req.query, ...req.body },
{ filesNamesMap: filesNamesMap }, { filesNamesMap: filesNamesMap },
true true,
req.sasSession
)) as ExecuteReturnJson )) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout let weboutRes: string | IRecordOfAny = webout

View File

@@ -1,9 +1,8 @@
import path from 'path' import path from 'path'
import { Request } from 'express' import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer' import multer, { FileFilterCallback, Options } from 'multer'
import { getTmpUploadsPath } from '../utils' import { blockFileRegex, getTmpUploadsPath } from '../utils'
const acceptableExtensions = ['.sas']
const fieldNameSize = 300 const fieldNameSize = 300
const fileSize = 10485760 // 10 MB const fileSize = 10485760 // 10 MB
@@ -31,15 +30,11 @@ const fileFilter: Options['fileFilter'] = (
file: Express.Multer.File, file: Express.Multer.File,
callback: FileFilterCallback callback: FileFilterCallback
) => { ) => {
const fileExtension = path.extname(file.originalname).toLocaleLowerCase() const fileExtension = path.extname(file.originalname)
const shouldBlockUpload = blockFileRegex.test(file.originalname)
if (!acceptableExtensions.includes(fileExtension)) { if (shouldBlockUpload) {
return callback( return callback(
new Error( new Error(`File extension '${fileExtension}' not acceptable.`)
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
', '
)}`
)
) )
} }

View File

@@ -8,7 +8,8 @@ import { DriveController } from '../../controllers/'
import { import {
deployValidation, deployValidation,
fileBodyValidation, fileBodyValidation,
fileParamValidation fileParamValidation,
folderParamValidation
} from '../../utils' } from '../../utils'
const controller = new DriveController() const controller = new DriveController()
@@ -22,9 +23,15 @@ driveRouter.post('/deploy', async (req, res) => {
try { try {
const response = await controller.deploy(body) const response = await controller.deploy(body)
const appLoc = body.appLoc.replace(/^\//, '')?.split('/') if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
publishAppStream(appLoc) body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
@@ -38,12 +45,24 @@ driveRouter.post('/deploy', async (req, res) => {
driveRouter.get('/file', async (req, res) => { driveRouter.get('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query) const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) return res.status(400).send(errQ.details[0].message) if (errQ) return res.status(400).send(errQ.details[0].message)
try { try {
await controller.getFile(req, query._filePath, body.filePath) await controller.getFile(req, query._filePath)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
driveRouter.get('/folder', async (req, res) => {
const { error: errQ, value: query } = folderParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.getFolder(query._folderPath)
res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
} }
@@ -51,12 +70,11 @@ driveRouter.get('/file', async (req, res) => {
driveRouter.delete('/file', async (req, res) => { driveRouter.delete('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query) const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) return res.status(400).send(errQ.details[0].message) if (errQ) return res.status(400).send(errQ.details[0].message)
try { try {
const response = await controller.deleteFile(query._filePath, body.filePath) const response = await controller.deleteFile(query._filePath)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())

View File

@@ -10,7 +10,9 @@ import {
readFile, readFile,
deleteFolder, deleteFolder,
generateTimestamp, generateTimestamp,
copy copy,
createFolder,
createFile
} from '@sasjs/utils' } from '@sasjs/utils'
import * as fileUtilModules from '../../../utils/file' import * as fileUtilModules from '../../../utils/file'
@@ -44,7 +46,7 @@ const user = {
isActive: true isActive: true
} }
describe('files', () => { describe('drive', () => {
let con: Mongoose let con: Mongoose
let mongoServer: MongoMemoryServer let mongoServer: MongoMemoryServer
const controller = new UserController() const controller = new UserController()
@@ -69,6 +71,7 @@ describe('files', () => {
await mongoServer.stop() await mongoServer.stop()
await deleteFolder(tmpFolder) await deleteFolder(tmpFolder)
}) })
describe('deploy', () => { describe('deploy', () => {
const shouldFailAssertion = async (payload: any) => { const shouldFailAssertion = async (payload: any) => {
const res = await request(app) const res = await request(app)
@@ -172,17 +175,126 @@ describe('files', () => {
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code) await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
await deleteFolder(getTmpFilesFolderPath()) await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
})
})
describe('folder', () => {
describe('get', () => {
const getFolderApi = '/SASjsApi/drive/folder'
it('should get root SAS folder on drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({ files: [], folders: [] })
})
it('should get a SAS folder on drive having _folderPath as query param', async () => {
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
const dirLevel1 = 'level1'
const dirLevel2 = 'level2'
const fileLevel1 = 'file1'
const fileLevel2 = 'file2'
await createFolder(path.join(pathToDrive, dirLevel1, dirLevel2))
await createFile(
path.join(pathToDrive, dirLevel1, fileLevel1),
'some file content'
)
await createFile(
path.join(pathToDrive, dirLevel1, dirLevel2, fileLevel2),
'some file content'
)
const res1 = await request(app)
.get(getFolderApi)
.query({ _folderPath: '/' })
.auth(accessToken, { type: 'bearer' })
expect(res1.statusCode).toEqual(200)
expect(res1.body).toEqual({ files: [], folders: [dirLevel1] })
const res2 = await request(app)
.get(getFolderApi)
.query({ _folderPath: dirLevel1 })
.auth(accessToken, { type: 'bearer' })
expect(res2.statusCode).toEqual(200)
expect(res2.body).toEqual({ files: [fileLevel1], folders: [dirLevel2] })
const res3 = await request(app)
.get(getFolderApi)
.query({ _folderPath: `${dirLevel1}/${dirLevel2}` })
.auth(accessToken, { type: 'bearer' })
expect(res3.statusCode).toEqual(200)
expect(res3.body).toEqual({ files: [fileLevel2], folders: [] })
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get(getFolderApi).expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folder is not present', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
.expect(403)
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath outside Drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '/../path/code.sas' })
.expect(403)
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath is of a file', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
filePath
)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: filePath })
.expect(403)
expect(res.text).toEqual('Error: Not a Folder.')
expect(res.body).toEqual({})
})
}) })
}) })
describe('file', () => { describe('file', () => {
describe('create', () => { describe('create', () => {
it('should create a SAS file on drive having filePath as form field', async () => { it('should create a SAS file on drive having filePath as form field', async () => {
const pathToUpload = `/my/path/code-1.sas`
const res = await request(app) const res = await request(app)
.post('/SASjsApi/drive/file') .post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.field('filePath', '/my/path/code.sas') .field('filePath', pathToUpload)
.attach('file', path.join(__dirname, 'files', 'sample.sas')) .attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200) expect(res.statusCode).toEqual(200)
@@ -192,10 +304,12 @@ describe('files', () => {
}) })
it('should create a SAS file on drive having _filePath as query param', async () => { it('should create a SAS file on drive having _filePath as query param', async () => {
const pathToUpload = `/my/path/code-2.sas`
const res = await request(app) const res = await request(app)
.post('/SASjsApi/drive/file') .post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/my/path/code1.sas' }) .query({ _filePath: pathToUpload })
.attach('file', path.join(__dirname, 'files', 'sample.sas')) .attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200) expect(res.statusCode).toEqual(200)
@@ -217,7 +331,7 @@ describe('files', () => {
it('should respond with Forbidden if file is already present', async () => { it('should respond with Forbidden if file is already present', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas' const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
const pathToCopy = path.join( const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(), fileUtilModules.getTmpFilesFolderPath(),
@@ -266,7 +380,7 @@ describe('files', () => {
it("should respond with Bad Request if filePath doesn't has correct extension", async () => { it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.oth' const pathToUpload = '/my/path/code.exe'
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`) .post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
@@ -275,7 +389,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual('Valid extensions for filePath: .sas') expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -293,7 +407,7 @@ describe('files', () => {
}) })
it("should respond with Bad Request if attached file doesn't has correct extension", async () => { it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth') const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
const pathToUpload = '/my/path/code.sas' const pathToUpload = '/my/path/code.sas'
const res = await request(app) const res = await request(app)
@@ -303,9 +417,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
`File extension '.oth' not acceptable. Valid extension(s): .sas`
)
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -388,7 +500,7 @@ describe('files', () => {
const res = await request(app) const res = await request(app)
.patch('/SASjsApi/drive/file') .patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`) .field('filePath', `/my/path/code-3.sas`)
.attach('file', path.join(__dirname, 'files', 'sample.sas')) .attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(403) .expect(403)
@@ -426,16 +538,16 @@ describe('files', () => {
it("should respond with Bad Request if filePath doesn't has correct extension", async () => { it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.oth' const pathToUpload = '/my/path/code.exe'
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`) .patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
// .field('filePath', pathToUpload) .query({ _filePath: pathToUpload })
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual('Valid extensions for filePath: .sas') expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -453,7 +565,7 @@ describe('files', () => {
}) })
it("should respond with Bad Request if attached file doesn't has correct extension", async () => { it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth') const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
const pathToUpload = '/my/path/code.sas' const pathToUpload = '/my/path/code.sas'
const res = await request(app) const res = await request(app)
@@ -463,9 +575,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
`File extension '.oth' not acceptable. Valid extension(s): .sas`
)
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -487,6 +597,79 @@ describe('files', () => {
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
}) })
describe('get', () => {
it('should get a SAS file on drive having _filePath as query param', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const fileToCopyContent = await readFile(fileToCopyPath)
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
filePath
)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: filePath })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({})
expect(res.text).toEqual(fileToCopyContent)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/drive/file').expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: `/my/path/code-4.sas` })
.expect(403)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/../path/code.sas' })
.expect(403)
expect(res.text).toEqual('Error: Cannot get file outside drive.')
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/my/path/code.exe' })
.expect(400)
expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.expect(400)
expect(res.text).toEqual(`"_filePath" is required`)
expect(res.body).toEqual({})
})
})
}) })
}) })

View File

@@ -0,0 +1,57 @@
import { AppStreamConfig } from '../../types'
const style = `<style>
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
.app-container .app {
width: 150px;
margin: 10px;
overflow: hidden;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
border-radius: 10px;
}
</style>`
const defaultAppLogo = '/sasjs-logo.svg'
const singleAppStreamHtml = (
streamServiceName: string,
appLoc: string,
logo?: string
) =>
` <a class="app" href="${streamServiceName}" title="${appLoc}">
<img
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
onerror="this.src = '${defaultAppLogo}';"
/>
${streamServiceName}
</a>`
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
<html>
<head>
<base href="/AppStream/">
${style}
</head>
<body>
<h1>App Stream</h1>
<div class="app-container">
${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) =>
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
)
.join('')}
</div>
</body>
</html>`

View File

@@ -2,25 +2,64 @@ import path from 'path'
import express from 'express' import express from 'express'
import { folderExists } from '@sasjs/utils' import { folderExists } from '@sasjs/utils'
import { getTmpFilesFolderPath } from '../../utils' import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
import { appStreamHtml } from './appStreamHtml'
const router = express.Router() const router = express.Router()
export const publishAppStream = async (appLoc: string[]) => { router.get('/', async (_, res) => {
const appLocUrl = encodeURI(appLoc.join('/')) const content = appStreamHtml(process.appStreamConfig)
const appLocPath = appLoc.join(path.sep)
const pathToDeployment = path.join( return res.send(content)
getTmpFilesFolderPath(), })
appLocPath,
'services', export const publishAppStream = async (
'webv' appLoc: string,
) streamWebFolder: string,
streamServiceName?: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
const driveFilesPath = getTmpFilesFolderPath()
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
const appLocPath = path.join(driveFilesPath, ...appLocParts)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
if (!pathToDeployment.includes(appLocPath)) {
throw new Error('streamWebFolder cannot be outside appLoc.')
}
if (await folderExists(pathToDeployment)) { if (await folderExists(pathToDeployment)) {
router.use(`/${appLocUrl}`, express.static(pathToDeployment)) const appCount = process.appStreamConfig
console.log('Serving Stream App: ', appLocUrl) ? Object.keys(process.appStreamConfig).length
: 0
if (!streamServiceName) {
streamServiceName = `AppStreamName${appCount + 1}`
}
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
addEntryToAppStreamConfig(
streamServiceName,
appLoc,
streamWebFolder,
streamLogo,
addEntryToFile
)
const sasJsPort = process.env.PORT ?? 5000
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)
return { streamServiceName }
} }
return {}
} }
export default router export default router

View File

@@ -0,0 +1,7 @@
export interface AppStreamConfig {
[key: string]: {
appLoc: string
streamWebFolder: string
streamLogo?: string
}
}

View File

@@ -1,23 +1,28 @@
export interface FileTree { export enum MemberType {
members: (FolderMember | ServiceMember)[] service = 'service',
file = 'file',
folder = 'folder'
} }
export enum MemberType { export interface ServiceMember {
folder = 'folder', name: string
service = 'service', type: MemberType.service
file = 'file' code: string
}
export interface FileMember {
name: string
type: MemberType.file
code: string
} }
export interface FolderMember { export interface FolderMember {
name: string name: string
type: MemberType.folder type: MemberType.folder
members: (FolderMember | ServiceMember)[] members: (FolderMember | ServiceMember | FileMember)[]
} }
export interface FileTree {
export interface ServiceMember { members: (FolderMember | ServiceMember | FileMember)[]
name: string
type: MemberType.service | MemberType.file
code: string
} }
export const isFileTree = (arg: any): arg is FileTree => export const isFileTree = (arg: any): arg is FileTree =>
@@ -25,11 +30,25 @@ export const isFileTree = (arg: any): arg is FileTree =>
arg.members && arg.members &&
Array.isArray(arg.members) && Array.isArray(arg.members) &&
arg.members.filter( arg.members.filter(
(member: FolderMember | ServiceMember) => (member: ServiceMember | FileMember | FolderMember) =>
!isFolderMember(member) && !isServiceMember(member) !isServiceMember(member, '-') &&
!isFileMember(member, '-') &&
!isFolderMember(member, '-')
).length === 0 ).length === 0
const isFolderMember = (arg: any): arg is FolderMember => const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
typeof arg.code === 'string'
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
typeof arg.code === 'string'
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
arg && arg &&
typeof arg.name === 'string' && typeof arg.name === 'string' &&
arg.type === MemberType.folder && arg.type === MemberType.folder &&
@@ -37,21 +56,7 @@ const isFolderMember = (arg: any): arg is FolderMember =>
Array.isArray(arg.members) && Array.isArray(arg.members) &&
arg.members.filter( arg.members.filter(
(member: FolderMember | ServiceMember) => (member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member, pre + '-') &&
!isServiceMember(member) && !isFileMember(member, pre + '-') &&
!isFileMember(member) !isFolderMember(member, pre + '-')
).length === 0 ).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'
const isFileMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
arg.code &&
typeof arg.code === 'string'

View File

@@ -3,5 +3,6 @@ declare namespace NodeJS {
sasLoc: string sasLoc: string
driveLoc: string driveLoc: string
sessionController?: import('../controllers/internal').SessionController sessionController?: import('../controllers/internal').SessionController
appStreamConfig: import('./').AppStreamConfig
} }
} }

View File

@@ -1,4 +1,5 @@
// TODO: uppercase types // TODO: uppercase types
export * from './AppStreamConfig'
export * from './Execution' export * from './Execution'
export * from './FileTree' export * from './FileTree'
export * from './InfoJWT' export * from './InfoJWT'

View File

@@ -0,0 +1,89 @@
import { createFile, fileExists, readFile } from '@sasjs/utils'
import { publishAppStream } from '../routes/appStream'
import { AppStreamConfig } from '../types'
import { getTmpAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
if (process.env.NODE_ENV === 'test') return
const appStreamConfigPath = getTmpAppStreamConfigPath()
const content = (await fileExists(appStreamConfigPath))
? await readFile(appStreamConfigPath)
: '{}'
let appStreamConfig: AppStreamConfig
try {
appStreamConfig = JSON.parse(content)
if (!isValidAppStreamConfig(appStreamConfig)) throw 'invalid type'
} catch (_) {
appStreamConfig = {}
}
process.appStreamConfig = {}
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
const { appLoc, streamWebFolder, streamLogo } = entry
publishAppStream(
appLoc,
streamWebFolder,
streamServiceName,
streamLogo,
false
)
}
console.log('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (
streamServiceName: string,
appLoc: string,
streamWebFolder: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
if (streamServiceName && appLoc && streamWebFolder) {
process.appStreamConfig[streamServiceName] = {
appLoc,
streamWebFolder,
streamLogo
}
if (addEntryToFile) saveAppStreamConfig()
}
}
export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
if (streamServiceName) {
delete process.appStreamConfig[streamServiceName]
saveAppStreamConfig()
}
}
const saveAppStreamConfig = async () => {
const appStreamConfigPath = getTmpAppStreamConfigPath()
try {
await createFile(
appStreamConfigPath,
JSON.stringify(process.appStreamConfig, null, 2)
)
} catch (_) {}
}
const isValidAppStreamConfig = (config: any) => {
if (config) {
return !Object.entries(config).some(([streamServiceName, entry]) => {
const { appLoc, streamWebFolder, streamLogo } = entry as any
return (
typeof streamServiceName !== 'string' ||
typeof appLoc !== 'string' ||
typeof streamWebFolder !== 'string'
)
})
}
return false
}

View File

@@ -0,0 +1,34 @@
import path from 'path'
import {
asyncForEach,
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils'
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
if (process.env.NODE_ENV === 'test') return
console.log('Copying Macros from container to drive(tmp).')
const macrosDrivePath = getTmpMacrosPath()
await deleteFolder(macrosDrivePath)
await createFolder(macrosDrivePath)
const macros = await readFile(sasJSCoreMacrosInfo)
await asyncForEach(macros.split('\n'), async (macroName) => {
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
const macroContent = await readFile(macroFileSourcePath)
const macroFileDestPath = path.join(macrosDrivePath, macroName)
await createFile(macroFileDestPath, macroContent)
})
console.log('Macros Drive Path:', macrosDrivePath)
}

View File

@@ -9,12 +9,18 @@ export const sysInitCompiledPath = path.join(
) )
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore') export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
export const getWebBuildFolderPath = () => export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build') path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () => process.driveLoc export const getTmpFolderPath = () => process.driveLoc
export const getTmpAppStreamConfigPath = () =>
path.join(getTmpFolderPath(), 'appStreamConfig.json')
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads') export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
export const getTmpFilesFolderPath = () => export const getTmpFilesFolderPath = () =>

View File

@@ -1,16 +1,19 @@
export * from './appStreamConfig'
export * from './connectDB' export * from './connectDB'
export * from './copySASjsCore'
export * from './extractHeaders' export * from './extractHeaders'
export * from './file' export * from './file'
export * from './generateAccessToken' export * from './generateAccessToken'
export * from './generateAuthCode' export * from './generateAuthCode'
export * from './generateRefreshToken' export * from './generateRefreshToken'
export * from './isDebugOn'
export * from './getCertificates' export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './isDebugOn'
export * from './parseLogToArray' export * from './parseLogToArray'
export * from './removeTokensInDB' export * from './removeTokensInDB'
export * from './saveTokensInDB' export * from './saveTokensInDB'
export * from './setProcessVariables' export * from './setProcessVariables'
export * from './setupFolders'
export * from './sleep' export * from './sleep'
export * from './upload' export * from './upload'
export * from './validation' export * from './validation'

View File

@@ -0,0 +1,7 @@
import { createFolder } from '@sasjs/utils'
import { getTmpFilesFolderPath } from './file'
export const setupFolders = async () => {
const drivePath = getTmpFilesFolderPath()
await createFolder(drivePath)
}

View File

@@ -1,9 +1,21 @@
import path from 'path'
import fs from 'fs'
import { getTmpSessionsFolderPath } from '.'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
import { listFilesInFolder } from '@sasjs/utils' import { listFilesInFolder } from '@sasjs/utils'
interface FilenameMapSingle {
fieldName: string
originalName: string
}
interface FilenamesMap {
[key: string]: FilenameMapSingle
}
interface UploadedFiles extends FilenameMapSingle {
fileref: string
filepath: string
count: number
}
/** /**
* It will create an object that maps hashed file names to the original names * It will create an object that maps hashed file names to the original names
* @param files array of files to be mapped * @param files array of files to be mapped
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
export const makeFilesNamesMap = (files: MulterFile[]) => { export const makeFilesNamesMap = (files: MulterFile[]) => {
if (!files) return null if (!files) return null
const filesNamesMap: { [key: string]: string } = {} const filesNamesMap: FilenamesMap = {}
for (let file of files) { for (let file of files) {
filesNamesMap[file.filename] = file.originalname filesNamesMap[file.filename] = {
fieldName: file.fieldname,
originalName: file.originalname
}
} }
return filesNamesMap return filesNamesMap
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
* @returns generated sas code * @returns generated sas code
*/ */
export const generateFileUploadSasCode = async ( export const generateFileUploadSasCode = async (
filesNamesMap: any, filesNamesMap: FilenamesMap,
sasSessionFolder: string sasSessionFolder: string
): Promise<string> => { ): Promise<string> => {
let uploadSasCode = '' let uploadSasCode = ''
let fileCount = 0 let fileCount = 0
let uploadedFilesMap: { const uploadedFiles: UploadedFiles[] = []
fileref: string
filepath: string
filename: string
count: number
}[] = []
const sasSessionFolderList: string[] = await listFilesInFolder( const sasSessionFolderList: string[] = await listFilesInFolder(
sasSessionFolder sasSessionFolder
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
if (fileName.includes('req_file')) { if (fileName.includes('req_file')) {
fileCount++ fileCount++
uploadedFilesMap.push({ uploadedFiles.push({
fileref: `_sjs${fileCountString}`, fileref: `_sjs${fileCountString}`,
filepath: `${sasSessionFolder}/${fileName}`, filepath: `${sasSessionFolder}/${fileName}`,
filename: filesNamesMap[fileName], originalName: filesNamesMap[fileName].originalName,
fieldName: filesNamesMap[fileName].fieldName,
count: fileCount count: fileCount
}) })
} }
}) })
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";` uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
} }
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};` uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filename};` uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
} }
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};` uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
} }
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filepath};` uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
} }
if (fileCount > 0) { if (fileCount > 0) {

View File

@@ -3,6 +3,8 @@ import Joi from 'joi'
const usernameSchema = Joi.string().alphanum().min(6).max(20) const usernameSchema = Joi.string().alphanum().min(6).max(20)
const passwordSchema = Joi.string().min(6).max(1024) const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const authorizeValidation = (data: any): Joi.ValidationResult => export const authorizeValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
username: usernameSchema.required(), username: usernameSchema.required(),
@@ -69,21 +71,36 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
export const deployValidation = (data: any): Joi.ValidationResult => export const deployValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
appLoc: Joi.string().pattern(/^\//).required().min(2), appLoc: Joi.string().pattern(/^\//).required().min(2),
streamServiceName: Joi.string(),
streamWebFolder: Joi.string(),
streamLogo: Joi.string(),
fileTree: Joi.any().required() fileTree: Joi.any().required()
}).validate(data) }).validate(data)
const filePathSchema = Joi.string()
.custom((value, helpers) => {
if (blockFileRegex.test(value)) return helpers.error('string.pattern.base')
return value
})
.required()
.messages({
'string.pattern.base': `Invalid file extension`
})
export const fileBodyValidation = (data: any): Joi.ValidationResult => export const fileBodyValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
filePath: Joi.string().pattern(/.sas$/).required().messages({ filePath: filePathSchema
'string.pattern.base': `Valid extensions for filePath: .sas`
})
}).validate(data) }).validate(data)
export const fileParamValidation = (data: any): Joi.ValidationResult => export const fileParamValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
_filePath: Joi.string().pattern(/.sas$/).required().messages({ _filePath: filePathSchema
'string.pattern.base': `Valid extensions for filePath: .sas` }).validate(data)
})
export const folderParamValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_folderPath: Joi.string()
}).validate(data) }).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult => export const runSASValidation = (data: any): Joi.ValidationResult =>

20
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "server", "name": "server",
"version": "0.0.34", "version": "0.0.47",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "0.0.34", "version": "0.0.47",
"devDependencies": { "devDependencies": {
"prettier": "^2.3.1", "prettier": "^2.3.1",
"standard-version": "^9.3.2" "standard-version": "^9.3.2"
@@ -404,14 +404,14 @@
} }
}, },
"node_modules/conventional-changelog-writer": { "node_modules/conventional-changelog-writer": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==", "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"conventional-commits-filter": "^2.0.7", "conventional-commits-filter": "^2.0.7",
"dateformat": "^3.0.0", "dateformat": "^3.0.0",
"handlebars": "^4.7.6", "handlebars": "^4.7.7",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"meow": "^8.0.0", "meow": "^8.0.0",
@@ -2433,14 +2433,14 @@
"dev": true "dev": true
}, },
"conventional-changelog-writer": { "conventional-changelog-writer": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==", "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"conventional-commits-filter": "^2.0.7", "conventional-commits-filter": "^2.0.7",
"dateformat": "^3.0.0", "dateformat": "^3.0.0",
"handlebars": "^4.7.6", "handlebars": "^4.7.7",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"meow": "^8.0.0", "meow": "^8.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.0.34", "version": "0.0.47",
"description": "NodeJS wrapper for calling the SAS binary executable", "description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server", "repository": "https://github.com/sasjs/server",
"scripts": { "scripts": {

View File

@@ -1,3 +1,6 @@
### Get contents of folder
GET http://localhost:5000/SASjsApi/drive/folder?_path=/Public/app/react-seed-app/services/web
### ###
POST http://localhost:5000/SASjsApi/drive/deploy POST http://localhost:5000/SASjsApi/drive/deploy
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I

View File

@@ -1,16 +1,14 @@
### testing upload file example ### testing upload file example
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
------WebKitFormBoundarynkYOqevUMKZrXeAy ------WebKitFormBoundarynkYOqevUMKZrXeAy
Content-Disposition: form-data; name="file"; filename="DCCONFIG.MPE_X_TEST.xlsx" Content-Disposition: form-data; name="fileSome11"; filename="DCCONFIG.MPE_X_TEST.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
------WebKitFormBoundarynkYOqevUMKZrXeAy ------WebKitFormBoundarynkYOqevUMKZrXeAy
Content-Disposition: form-data; name="file"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv" Content-Disposition: form-data; name="fileSome22"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
Content-Type: application/csv Content-Type: application/csv
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM _____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM

24
web/package-lock.json generated
View File

@@ -8472,9 +8472,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "0.5.5", "version": "0.5.5",
@@ -8581,9 +8581,9 @@
} }
}, },
"node_modules/node-forge": { "node_modules/node-forge": {
"version": "1.2.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">= 6.13.0" "node": ">= 6.13.0"
@@ -17622,9 +17622,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"mkdirp": { "mkdirp": {
"version": "0.5.5", "version": "0.5.5",
@@ -17715,9 +17715,9 @@
} }
}, },
"node-forge": { "node-forge": {
"version": "1.2.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"dev": true "dev": true
}, },
"node-releases": { "node-releases": {

View File

@@ -66,11 +66,21 @@ const Header = (props: any) => {
variant="contained" variant="contained"
color="primary" color="primary"
size="large" size="large"
startIcon={<OpenInNewIcon />} endIcon={<OpenInNewIcon />}
style={{ marginLeft: '50px' }}
> >
API Docs API Docs
</Button> </Button>
<Button
href={`${baseUrl}/AppStream`}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
size="large"
endIcon={<OpenInNewIcon />}
>
App Stream
</Button>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
) )

View File

@@ -50,26 +50,13 @@ const Studio = () => {
.map((logLine: any) => logLine.line) .map((logLine: any) => logLine.line)
.join('\n') .join('\n')
setLog(`<div><h2>SAS Log</h2><pre>${parsedLog}</pre></div>`) setLog(parsedLog)
let weboutString: string setWebout(`${res.data?._webout}`)
try {
weboutString = res.data.webout
.split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0]
} catch (_) {
weboutString = res?.data?.webout ?? ''
}
let webout: string
try {
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
} catch (_) {
webout = weboutString
}
setWebout(`<pre><code>${webout}</code></pre>`)
setTab('2') setTab('2')
// Scroll to bottom of log
window.scrollTo(0, document.body.scrollHeight)
}) })
.catch((err) => console.log(err)) .catch((err) => console.log(err))
} }
@@ -97,6 +84,7 @@ const Studio = () => {
}, [location.search]) }, [location.search])
const classes = useStyles() const classes = useStyles()
return ( return (
<> <>
<br /> <br />
@@ -149,17 +137,15 @@ const Studio = () => {
</Stack> </Stack>
</TabPanel> </TabPanel>
<TabPanel value="2"> <TabPanel value="2">
<div <div style={{ marginTop: '50px' }}>
id="sas_log" <h2>SAS Log</h2>
style={{ marginTop: '50px' }} <pre>{log}</pre>
dangerouslySetInnerHTML={{ __html: log }} </div>
/>
</TabPanel> </TabPanel>
<TabPanel value="3"> <TabPanel value="3">
<div <div style={{ marginTop: '50px' }}>
style={{ marginTop: '50px' }} <pre>{webout}</pre>
dangerouslySetInnerHTML={{ __html: webout }} </div>
/>
</TabPanel> </TabPanel>
</TabContext> </TabContext>
</Box> </Box>