├── sasjs ├── macros │ └── .gitkeep ├── utils │ ├── copysas9.sh │ └── copyviya.sh ├── services │ └── common │ │ ├── appinit.sas │ │ ├── append.sas │ │ └── upload.sas └── sasjsconfig.json ├── .gitignore ├── src ├── logo.png ├── table.css ├── switch.css ├── style.css ├── index.html └── scripts.js ├── .gitpod.dockerfile ├── .gitpod.yml ├── stream.sas ├── package.json └── README.md /sasjs/macros/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | sasjsbuild/ 3 | sasjsresults/ 4 | .env* 5 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/fileuploader/master/src/logo.png -------------------------------------------------------------------------------- /sasjs/utils/copysas9.sh: -------------------------------------------------------------------------------- 1 | echo "sasjs: copying deploy script to repo root" 2 | cp sasjsbuild/sas9.sas streamsas9.sas -------------------------------------------------------------------------------- /sasjs/utils/copyviya.sh: -------------------------------------------------------------------------------- 1 | echo "sasjs: copying deploy script to repo root" 2 | cp sasjsbuild/viya.sas streamviya.sas -------------------------------------------------------------------------------- /.gitpod.dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | RUN sudo apt-get update \ 4 | && sudo apt-get install -y \ 5 | doxygen \ 6 | && sudo rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /sasjs/services/common/appinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file appinit.sas 3 | @brief Initialisation service - runs on app startup 4 | @details This is always the first service called when the app is opened. 5 | 6 | **/ 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/config-start-tasks/ 2 | tasks: 3 | - init: npm install -g npm && npm i -g @sasjs/cli 4 | 5 | vscode: 6 | extensions: 7 | - sasjs.sasjs-for-vscode 8 | 9 | -------------------------------------------------------------------------------- /src/table.css: -------------------------------------------------------------------------------- 1 | table { 2 | font-family: arial, sans-serif; 3 | border-collapse: collapse; 4 | width: 100%; 5 | } 6 | 7 | td, 8 | th { 9 | border: 1px solid #dddddd; 10 | text-align: left; 11 | padding: 8px; 12 | } 13 | 14 | tr:nth-child(even) { 15 | background-color: #dddddd; 16 | } 17 | -------------------------------------------------------------------------------- /stream.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief Stream the SAS 9 or Viya version 4 | @details Depending on server type, will execute either the SAS 9 or the Viya 5 | version of the fileuploader app 6 | **/ 7 | 8 | %global sysprocessmode; 9 | data _null_; 10 | mode=symget('sysprocessmode'); 11 | if mode in ("SAS Object Server","SAS Compute Server") 12 | then call symputx('streamer','streamviya.sas'); 13 | else call symputx('streamer','streamsas9.sas'); 14 | run; 15 | 16 | filename stream url 17 | "https://raw.githubusercontent.com/sasjs/fileuploader/master/&streamer"; 18 | %inc stream; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sasjs/fileuploader", 3 | "repository": "https://github.com/sasjs/fileuploader", 4 | "version": "1.0.0", 5 | "description": "A plain javascript app to demonstrate the process of uploading a file (with a parameter) and dealing with the response from SAS", 6 | "main": "index.js", 7 | "scripts": { 8 | "deploy": "rsync -avhe ssh ./src/* --delete $SSH_ACCOUNT:$DEPLOY_PATH", 9 | "deploywin": "scp -i ~/.ssh/private_key -r ./src/* $SSH_ACCOUNT:$DEPLOY_PATH", 10 | "prepare": "cpy-t node_modules/@sasjs/adapter/index.js src/sasjs.js" 11 | }, 12 | "keywords": [ 13 | "SAS", 14 | "SASViya", 15 | "SASjs" 16 | ], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@sasjs/adapter": "4.1.6", 21 | "@sasjs/core": "4.45.11" 22 | }, 23 | "devDependencies": { 24 | "@types/tough-cookie": "^4.0.1", 25 | "cpy-t": "^1.1.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/switch.css: -------------------------------------------------------------------------------- 1 | .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 60px; 5 | height: 34px; 6 | } 7 | 8 | .switch input { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .slider { 15 | position: absolute; 16 | cursor: pointer; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background-color: #ccc; 22 | -webkit-transition: 0.4s; 23 | transition: 0.4s; 24 | } 25 | 26 | .slider:before { 27 | position: absolute; 28 | content: ""; 29 | height: 26px; 30 | width: 26px; 31 | left: 4px; 32 | bottom: 4px; 33 | background-color: white; 34 | -webkit-transition: 0.4s; 35 | transition: 0.4s; 36 | } 37 | 38 | input:checked + .slider { 39 | background-color: #2196f3; 40 | } 41 | 42 | input:focus + .slider { 43 | box-shadow: 0 0 1px #2196f3; 44 | } 45 | 46 | input:checked + .slider:before { 47 | -webkit-transform: translateX(26px); 48 | -ms-transform: translateX(26px); 49 | transform: translateX(26px); 50 | } 51 | 52 | /* Rounded sliders */ 53 | .slider.round { 54 | border-radius: 34px; 55 | } 56 | 57 | .slider.round:before { 58 | border-radius: 50%; 59 | } 60 | -------------------------------------------------------------------------------- /sasjs/services/common/append.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief appends a file from frontend to a user provided location 4 | @details Returns the file size or -1 in case of error. 5 | 6 |

SAS Macros

7 | @li mf_getfilesize.sas 8 | @li mf_isdir.sas 9 | @li mp_abort.sas 10 | @li mp_binarycopy.sas 11 | @li mp_webin.sas 12 | 13 | **/ 14 | 15 | %mp_abort(iftrue= (%mf_isdir(&path) = 0) 16 | ,mac=&_program..sas 17 | ,msg=%str(File path (&path) is not a valid directory) 18 | ) 19 | 20 | /* 21 | Straighten up the _webin_xxx variables 22 | */ 23 | %mp_webin() 24 | 25 | /* setup the output destination */ 26 | %let outloc=&path/&_webin_filename1; 27 | filename fileout "&outloc"; 28 | 29 | /* send the data in APPEND mode */ 30 | %mp_binarycopy(inref=&_webin_fileref1, outref=fileout, mode=APPEND) 31 | 32 | %mp_abort(iftrue= (&syscc ge 4) 33 | ,mac=&_program..sas 34 | ,msg=%str(Error occurred reading &_webin_fileref1 and writing to &outloc) 35 | ) 36 | 37 | /* success - lets create a directory listing */ 38 | data fromsas; 39 | filesize="%mf_getfilesize(fpath=&path/&_webin_filename1,format=yes)"; 40 | run; 41 | 42 | /* now send it back to the frontend */ 43 | %webout(OPEN) 44 | %webout(OBJ,fromsas) 45 | %webout(CLOSE) 46 | -------------------------------------------------------------------------------- /sasjs/services/common/upload.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief Loads a file from frontend to a user provided location 4 | @details Returns a directory listing if successful. 5 | 6 | The macros shown below are compiled from the SASjs CORE library (or the 7 | sasjs/macros project directory) when running the `sasjs cb` command. This is 8 | why you see them in the service, but not in the file in the GIT repository. 9 | 10 |

SAS Macros

11 | @li mp_abort.sas 12 | @li mf_isdir.sas 13 | @li mp_dirlist.sas 14 | @li mp_binarycopy.sas 15 | @li mp_webin.sas 16 | 17 | **/ 18 | 19 | %mp_abort(iftrue= (%mf_isdir(&path) = 0) 20 | ,mac=&_program..sas 21 | ,msg=%str(File path (&path) is not a valid directory) 22 | ) 23 | 24 | /* 25 | Straighten up the _webin_xxx variables 26 | */ 27 | %mp_webin() 28 | 29 | /* setup the output destination */ 30 | %let outloc=&path/&_webin_filename1; 31 | filename fileout "&outloc"; 32 | 33 | /* send the data */ 34 | %mp_binarycopy(inref=&_webin_fileref1, outref=fileout) 35 | 36 | %mp_abort(iftrue= (&syscc ge 4) 37 | ,mac=&_program..sas 38 | ,msg=%str(Error occurred reading &_webin_fileref1 and writing to &outloc) 39 | ) 40 | 41 | /* success - lets create a directory listing */ 42 | %mp_dirlist(path=&path,outds=dirlist) 43 | proc sort data=dirlist; 44 | by filepath; 45 | run; 46 | 47 | /* now send it back to the frontend */ 48 | %webout(OPEN) 49 | %webout(OBJ,dirlist) 50 | %webout(CLOSE) 51 | -------------------------------------------------------------------------------- /sasjs/sasjsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cli.sasjs.io/sasjsconfig-schema.json", 3 | "macroFolders": [ 4 | "sasjs/macros" 5 | ], 6 | "serviceConfig": { 7 | "serviceFolders": [ 8 | "sasjs/services/common" 9 | ] 10 | }, 11 | "streamConfig": { 12 | "streamLogo": "logo.png", 13 | "streamServiceName": "FileUploader", 14 | "streamWeb": true, 15 | "streamWebFolder": "web", 16 | "webSourcePath": "src" 17 | }, 18 | "defaultTarget": "viya", 19 | "targets": [ 20 | { 21 | "name": "viya", 22 | "serverUrl": "https://sasviya.com", 23 | "serverType": "SASVIYA", 24 | "httpsAgentOptions": { 25 | "allowInsecureRequests": false 26 | }, 27 | "appLoc": "/Public/app/fileuploader", 28 | "deployConfig": { 29 | "deployServicePack": true, 30 | "deployScripts": [ 31 | "sasjs/utils/copyviya.sh" 32 | ] 33 | }, 34 | "contextName": "SAS Job Execution compute context" 35 | }, 36 | { 37 | "name": "sas9", 38 | "serverType": "SAS9", 39 | "serverUrl": "https://sas.analytium.co.uk:8343", 40 | "appLoc": "/30.SASApps/3030.Projects/fileuploader", 41 | "deployConfig": { 42 | "deployServicePack": true, 43 | "deployScripts": [ 44 | "sasjs/utils/copysas9.sh" 45 | ] 46 | } 47 | }, 48 | { 49 | "name": "server", 50 | "serverType": "SASJS", 51 | "serverUrl": "", 52 | "appLoc": "/Public/app/fileuploader" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Roboto', sans-serif; 3 | } 4 | 5 | body { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | min-height: 100vh; 11 | margin: 0; 12 | padding-bottom: 30px; 13 | } 14 | 15 | hr { 16 | clear: both; 17 | display: block; 18 | width: 100%; 19 | background-color: black; 20 | height: 1px; 21 | } 22 | 23 | .code { 24 | font-family: Monaco, Courier, monospace; 25 | border: 1px solid #d9d9d9; 26 | padding: 5px; 27 | border-radius: 3px; 28 | background-color: #000000; 29 | color: #f6e30f; 30 | } 31 | 32 | button { 33 | background-color: #3f51b5; 34 | color: #ffffff; 35 | border: none; 36 | padding: 10px 30px; 37 | border-radius: 5px; 38 | font-size: 16px; 39 | cursor: pointer; 40 | margin: 10px 0; 41 | } 42 | 43 | #upload[disabled] { 44 | background-color: gray; 45 | } 46 | 47 | #cancel { 48 | background-color: red; 49 | } 50 | 51 | #cancel[disabled] { 52 | background-color: gray; 53 | } 54 | 55 | button:focus, 56 | input:focus { 57 | outline: none; 58 | } 59 | 60 | button:hover { 61 | box-shadow: inset 0 0 100px 100px rgba(255, 255, 255, 0.1); 62 | } 63 | button[disabled]:hover { 64 | box-shadow: none; 65 | } 66 | 67 | .form { 68 | display: flex; 69 | flex-direction: column; 70 | } 71 | 72 | .form input { 73 | padding: 10px; 74 | border-radius: 5px; 75 | font-size: 20px; 76 | border: 1px solid #d9d9d9; 77 | margin-top: 5px; 78 | } 79 | 80 | select { 81 | font-size: 16px; 82 | padding: 20px; 83 | width: 30%; 84 | } 85 | 86 | .debug { 87 | position: absolute; 88 | top: 0; 89 | right: 0; 90 | padding: 20px; 91 | } 92 | .debug span { 93 | font-size: 20px; 94 | vertical-align: -webkit-baseline-middle; 95 | } 96 | 97 | .display-none { 98 | display: none; 99 | } 100 | 101 | #progressBar { 102 | position: relative; 103 | width: 100%; 104 | background-color: #ddd; 105 | } 106 | 107 | #barStatus { 108 | position: absolute; 109 | height: 100%; 110 | background-color: #25f; 111 | } 112 | 113 | #fileUploadStatus { 114 | display: flex; 115 | justify-content: center; 116 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 |
24 | Debug 25 | 26 | 30 |
31 | 32 |

This is a template SASjs app

33 |

34 | You can use it to upload a local file to a directory on your SAS server 35 |

36 |
37 | 38 | 39 |
40 | 41 | 42 | 51 |
52 | 59 | 66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 |

Logs

79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sasjs/fileuploader) 2 | 3 | # Demo SASjs File Uploader app 4 | 5 | This is a vanilla JS demo app to show how to build a SAS-Powered file upload process. The app has a logon screen, then an input box (so the user can provide a target directory path), and a file picker. 6 | 7 | The file is sent to SAS where the path is verified, the file is written, and a directory listing is returned. 8 | 9 | If the file is greater than 5mb it is sent to SAS in chunks, with a progress bar displayed. A video demonstrating this process is available here: https://www.youtube.com/watch?v=rf9myXovrsk 10 | 11 | ![screenshot of sasjs file uploader](https://i.imgur.com/alHXcTK.png) 12 | 13 | If the user logs out of SAS whilst an upload is in progress, then the app will prompt for credentials on the next request. The "redirect" flow is implemented, which is compatible with 2FA and other more complex authentication mechanisms. 14 | 15 | ## Fast Deploy 16 | This app can be deployed as a streaming SAS app by running the code below. Be 17 | sure to set the value of `appLoc` to your preferred parent folder location in 18 | metadata (SAS 9) or SAS Drive (Viya). 19 | 20 | ```sas 21 | %let apploc=/Public/app/fileuploader; 22 | filename mc url "https://raw.githubusercontent.com/sasjs/fileuploader/master/stream.sas"; 23 | %inc mc; 24 | ``` 25 | 26 | The link to open the app will be shown in the log. 27 | 28 | 29 | ## Alternatives 30 | It is also possible to upload files using SAS Studio, into your home directory (you can set symlinks to other locations). 31 | 32 | If your use case is simply about loading data into SAS, you might want to consider https://datacontroller.io (lets you load excel and CSV files into any SAS table or database with full audit trail - it's also free for up to 5 users). 33 | 34 | ## Building from Source 35 | 36 | To deploy this app, first install the SASjs CLI - full instructions [here](https://cli.sasjs.io/installation/). 37 | 38 | Next, run `sasjs add` to prepare your target ([instructions](https://cli.sasjs.io/add/)). 39 | 40 | Then run the below to deploy the backend (SAS) services: 41 | 42 | ``` 43 | npm install 44 | sasjs cbd -t YOURTARGET 45 | ``` 46 | 47 | If you don't have the ability to `sasjs add` due to not having access to a client / secret, you can instead run `sasjs cb` and execute the resulting `sasjsbuild/build.sas` script in SAS Studio V to create the backend services. 48 | 49 | ## Frontend 50 | 51 | ### Configuration 52 | 53 | Open `index.html` and make sure the value for `appLoc` is the same as that used when deploying the backend (`sasjsconfig.json`). 54 | 55 | Also, update the `