diff --git a/api/package.json b/api/package.json
index d23cfba..346bb49 100644
--- a/api/package.json
+++ b/api/package.json
@@ -88,5 +88,10 @@
},
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
+ },
+ "nodemonConfig": {
+ "ignore": [
+ "tmp/appStreamConfig.json"
+ ]
}
}
diff --git a/api/public/sasjs-logo.svg b/api/public/sasjs-logo.svg
new file mode 100644
index 0000000..2afe868
--- /dev/null
+++ b/api/public/sasjs-logo.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml
index fd3496a..5aaa786 100644
--- a/api/public/swagger.yaml
+++ b/api/public/swagger.yaml
@@ -214,6 +214,8 @@ components:
type: string
message:
type: string
+ streamServiceName:
+ type: string
example:
$ref: '#/components/schemas/FileTree'
required:
@@ -225,6 +227,8 @@ components:
properties:
appLoc:
type: string
+ streamWebFolder:
+ type: string
fileTree:
$ref: '#/components/schemas/FileTree'
required:
diff --git a/api/src/app.ts b/api/src/app.ts
index 35dfa9b..17350ee 100644
--- a/api/src/app.ts
+++ b/api/src/app.ts
@@ -8,6 +8,7 @@ import cors from 'cors'
import {
connectDB,
getWebBuildFolderPath,
+ loadAppStreamConfig,
sasJSCoreMacros,
setProcessVariables
} from './utils'
@@ -46,6 +47,8 @@ export default setProcessVariables().then(async () => {
const { setupRoutes } = await import('./routes/setupRoutes')
setupRoutes(app)
+ await loadAppStreamConfig()
+
// should be served after setting up web route
// index.html needs to be injected with some js script.
app.use(express.static(getWebBuildFolderPath()))
diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts
index 121a99d..1b11684 100644
--- a/api/src/controllers/drive.ts
+++ b/api/src/controllers/drive.ts
@@ -29,12 +29,14 @@ import { getTmpFilesFolderPath } from '../utils'
interface DeployPayload {
appLoc: string
+ streamWebFolder?: string
fileTree: FileTree
}
interface DeployResponse {
status: string
message: string
+ streamServiceName?: string
example?: FileTree
}
@@ -190,14 +192,23 @@ const getFileTree = () => {
}
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)) {
throw { code: 400, ...invalidDeployFormatResponse }
}
- await createFileTree(
- data.fileTree.members,
- data.appLoc.replace(/^\//, '').split('/')
- ).catch((err) => {
+ await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
throw { code: 500, ...execDeployErrorResponse, ...err }
})
diff --git a/api/src/middlewares/multer.ts b/api/src/middlewares/multer.ts
index 52d0b2e..d46f466 100644
--- a/api/src/middlewares/multer.ts
+++ b/api/src/middlewares/multer.ts
@@ -1,9 +1,8 @@
import path from 'path'
import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer'
-import { getTmpUploadsPath } from '../utils'
+import { blockFileRegex, getTmpUploadsPath } from '../utils'
-const acceptableExtensions = ['.sas']
const fieldNameSize = 300
const fileSize = 10485760 // 10 MB
@@ -31,15 +30,11 @@ const fileFilter: Options['fileFilter'] = (
file: Express.Multer.File,
callback: FileFilterCallback
) => {
- const fileExtension = path.extname(file.originalname).toLocaleLowerCase()
-
- if (!acceptableExtensions.includes(fileExtension)) {
+ const fileExtension = path.extname(file.originalname)
+ const shouldBlockUpload = blockFileRegex.test(file.originalname)
+ if (shouldBlockUpload) {
return callback(
- new Error(
- `File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
- ', '
- )}`
- )
+ new Error(`File extension '${fileExtension}' not acceptable.`)
)
}
diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts
index 0725696..bd6af29 100644
--- a/api/src/routes/api/drive.ts
+++ b/api/src/routes/api/drive.ts
@@ -22,9 +22,15 @@ driveRouter.post('/deploy', async (req, res) => {
try {
const response = await controller.deploy(body)
- const appLoc = body.appLoc.replace(/^\//, '')?.split('/')
-
- publishAppStream(appLoc)
+ if (body.streamWebFolder) {
+ const { streamServiceName } = await publishAppStream(
+ body.appLoc,
+ body.streamWebFolder,
+ body.streamServiceName,
+ body.streamLogo
+ )
+ response.streamServiceName = streamServiceName
+ }
res.send(response)
} catch (err: any) {
diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts
index ef6cdd5..d570de6 100644
--- a/api/src/routes/api/spec/drive.spec.ts
+++ b/api/src/routes/api/spec/drive.spec.ts
@@ -266,7 +266,7 @@ describe('files', () => {
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
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)
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
@@ -275,7 +275,7 @@ describe('files', () => {
.attach('file', fileToAttachPath)
.expect(400)
- expect(res.text).toEqual('Valid extensions for filePath: .sas')
+ expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
@@ -293,7 +293,7 @@ describe('files', () => {
})
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 res = await request(app)
@@ -303,9 +303,7 @@ describe('files', () => {
.attach('file', fileToAttachPath)
.expect(400)
- expect(res.text).toEqual(
- `File extension '.oth' not acceptable. Valid extension(s): .sas`
- )
+ expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
expect(res.body).toEqual({})
})
@@ -426,7 +424,7 @@ describe('files', () => {
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
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)
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
@@ -435,7 +433,7 @@ describe('files', () => {
.attach('file', fileToAttachPath)
.expect(400)
- expect(res.text).toEqual('Valid extensions for filePath: .sas')
+ expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
@@ -453,7 +451,7 @@ describe('files', () => {
})
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 res = await request(app)
@@ -463,9 +461,7 @@ describe('files', () => {
.attach('file', fileToAttachPath)
.expect(400)
- expect(res.text).toEqual(
- `File extension '.oth' not acceptable. Valid extension(s): .sas`
- )
+ expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
expect(res.body).toEqual({})
})
diff --git a/api/src/routes/api/spec/files/sample.oth b/api/src/routes/api/spec/files/sample.exe
similarity index 100%
rename from api/src/routes/api/spec/files/sample.oth
rename to api/src/routes/api/spec/files/sample.exe
diff --git a/api/src/routes/appStream/appStreamHtml.ts b/api/src/routes/appStream/appStreamHtml.ts
new file mode 100644
index 0000000..70f7d59
--- /dev/null
+++ b/api/src/routes/appStream/appStreamHtml.ts
@@ -0,0 +1,53 @@
+import { AppStreamConfig } from '../../types'
+
+const style = ``
+
+const defaultAppLogo = '/sasjs-logo.svg'
+
+const singleAppStreamHtml = (
+ streamServiceName: string,
+ appLoc: string,
+ logo?: string
+) =>
+ `
+
+ ${streamServiceName}
+ `
+
+export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
+
+