From 8fd1adfe0a11ba0b2ff6c0e6bd15428746a5773f Mon Sep 17 00:00:00 2001
From: Andrey Platov <andrey@hardcoreeng.com>
Date: Mon, 25 Oct 2021 17:11:48 +0200
Subject: [PATCH] get rid of `upload` service

Signed-off-by: Andrey Platov <andrey@hardcoreeng.com>
---
 common/config/rush/pnpm-lock.yaml |  15 ++-
 dev/prod/.env-prod                |   2 +-
 dev/prod/webpack.config.js        |   2 +-
 server/front/kube/front.yml       |  23 ++++
 server/front/package.json         |  17 ++-
 server/front/src/__start.ts       |  58 +++++++++
 server/front/src/app.ts           | 190 ++++++++++++++++++++++++++++++
 7 files changed, 301 insertions(+), 6 deletions(-)
 create mode 100644 server/front/src/__start.ts
 create mode 100644 server/front/src/app.ts

diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index 4b0642a81f..c28de2e19f 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -112,6 +112,7 @@ specifiers:
   express: ^4.17.1
   express-fileupload: ^1.2.1
   file-loader: ^6.2.0
+  filesize: ^8.0.3
   intl-messageformat: ^9.7.1
   just-clone: ^3.2.1
   koa: ^2.13.1
@@ -250,6 +251,7 @@ dependencies:
   express: 4.17.1
   express-fileupload: 1.2.1
   file-loader: 6.2.0_webpack@5.57.1
+  filesize: 8.0.3
   intl-messageformat: 9.7.1
   just-clone: 3.2.1
   koa: 2.13.3
@@ -9319,7 +9321,7 @@ packages:
     dev: false
 
   file:projects/chunter-resources.tgz_e1367da94684b005adf08f025c517b1a:
-    resolution: {integrity: sha512-ffIxP0yesLoUxgDnGwHBhD0MRgzq5REyfoon10k7s3nlwDJzC3/yjPgMdFc9p+rZUfcRdhA17vF+AkuOD5vycA==, tarball: file:projects/chunter-resources.tgz}
+    resolution: {integrity: sha512-zq2XzUCiMo5EZ7KfbLCGceDlQIyplRUNg8hPJP/7npIY/3YTgWDoc8svgCPltsen4PNMioIjAWWP1t+g62i6Mg==, tarball: file:projects/chunter-resources.tgz}
     id: file:projects/chunter-resources.tgz
     name: '@rush-temp/chunter-resources'
     version: 0.0.0
@@ -9608,21 +9610,30 @@ packages:
     dev: false
 
   file:projects/front.tgz_b4fae2aaf9a34e02c9acb1cfc4c88710:
-    resolution: {integrity: sha512-XmgU22kXGEisU6bW4q3GuuqhzjWazpokQ+ASxb2Nu7M/cTM+xy9EY/fvessk79EXRTGFvoH4hM2jrbleWKP9ug==, tarball: file:projects/front.tgz}
+    resolution: {integrity: sha512-qKmnx2yFt1hFJRZWJNaPPbb+p/Q0lnyA6u9AAgeqDbOjmWjp48NxqX6jIbiEyQi56IDHViE3w4/NQb0c/mp/rA==, tarball: file:projects/front.tgz}
     id: file:projects/front.tgz
     name: '@rush-temp/front'
     version: 0.0.0
     dependencies:
+      '@types/cors': 2.8.12
       '@types/express': 4.17.13
+      '@types/express-fileupload': 1.1.7
       '@types/heft-jest': 1.0.2
+      '@types/minio': 7.0.10
       '@types/node': 16.10.3
+      '@types/uuid': 8.3.1
       '@typescript-eslint/eslint-plugin': 4.33.0_d753869925cce96d3eb2141eeedafe57
+      cors: 2.8.5
       esbuild: 0.12.29
       eslint: 7.32.0
       eslint-plugin-import: 2.24.2_eslint@7.32.0
       eslint-plugin-node: 11.1.0_eslint@7.32.0
       eslint-plugin-promise: 4.3.1
       express: 4.17.1
+      express-fileupload: 1.2.1
+      jwt-simple: 0.5.6
+      minio: 7.0.19
+      uuid: 8.3.2
     transitivePeerDependencies:
       - '@typescript-eslint/parser'
       - supports-color
diff --git a/dev/prod/.env-prod b/dev/prod/.env-prod
index 1188498b80..f169c875ee 100644
--- a/dev/prod/.env-prod
+++ b/dev/prod/.env-prod
@@ -2,4 +2,4 @@
 
 #ACCOUNTS_URL=https://ftwm71rwag.execute-api.us-west-2.amazonaws.com/stage/
 ACCOUNTS_URL=https://account.hc.engineering/
-UPLOAD_URL=https://upload.hc.engineering/
+UPLOAD_URL=/files
diff --git a/dev/prod/webpack.config.js b/dev/prod/webpack.config.js
index 0b8c1bce98..808e0945c9 100644
--- a/dev/prod/webpack.config.js
+++ b/dev/prod/webpack.config.js
@@ -171,7 +171,7 @@ module.exports = {
       '/upload': {
         // target: 'https://anticrm-upload.herokuapp.com/',
         // target: 'http://localhost:3000/',
-        target: 'https://upload.hc.engineering/',
+        target: 'https://front.hc.engineering/files',
         changeOrigin: true,
         pathRewrite: { '^/upload': '' },
         logLevel: 'debug'
diff --git a/server/front/kube/front.yml b/server/front/kube/front.yml
index 1e22c08c51..0eb6c3eadf 100644
--- a/server/front/kube/front.yml
+++ b/server/front/kube/front.yml
@@ -19,6 +19,29 @@ spec:
           ports:
             - containerPort: 8080
           imagePullPolicy: Always
+          env:
+          - name: TRANSACTOR_URL
+            value: ws://transactor/
+          - name: ELASTIC_URL
+            valueFrom:
+              secretKeyRef:
+                name: elastic
+                key: url
+          - name: MINIO_ENDPOINT
+            valueFrom:
+              secretKeyRef:
+                name: minio
+                key: endpoint
+          - name: MINIO_ACCESS_KEY
+            valueFrom:
+              secretKeyRef:
+                name: minio
+                key: accessKey
+          - name: MINIO_SECRET_KEY
+            valueFrom:
+              secretKeyRef:
+                name: minio
+                key: secretKey
 ---
 apiVersion: v1
 kind: Service
diff --git a/server/front/package.json b/server/front/package.json
index cd2e3a7599..96ac4e010c 100644
--- a/server/front/package.json
+++ b/server/front/package.json
@@ -7,7 +7,7 @@
   "scripts": {
     "build": "heft build",
     "lint:fix": "eslint --fix src",
-    "bundle": "esbuild src/index.ts --bundle --minify --platform=node > bundle.js & rm -rf ./dist && cp -r ../../dev/prod/dist . && cp -r ../../dev/prod/public/* ./dist/",
+    "bundle": "esbuild src/__start.ts --bundle --minify --platform=node > bundle.js & rm -rf ./dist && cp -r ../../dev/prod/dist . && cp -r ../../dev/prod/public/* ./dist/",
     "docker:build": "docker build -t anticrm/front .",
     "docker:push": "docker push anticrm/front"
   },
@@ -21,11 +21,24 @@
     "eslint-plugin-node":"11",
     "eslint":"^7.32.0",
     "@types/express":"^4.17.13",
+    "@types/express-fileupload":"^1.1.7",
+    "@types/uuid":"^8.3.1",
+    "@types/cors":"^2.8.12",
+    "@types/minio":"^7.0.10",
     "esbuild":"^0.12.24"
   },
   "dependencies": {
     "@anticrm/core": "~0.6.11",
     "@anticrm/platform": "~0.6.5",
-    "express": "^4.17.1"
+    "express": "^4.17.1",
+    "express-fileupload":"^1.2.1",
+    "uuid":"^8.3.2",
+    "cors":"^2.8.5",
+    "@anticrm/elastic":"~0.6.0",
+    "jwt-simple":"^0.5.6",
+    "@anticrm/server-core":"~0.6.1",
+    "@anticrm/chunter":"~0.6.0",
+    "@anticrm/contrib":"~0.6.0",
+    "minio":"^7.0.19"
   }
 }
diff --git a/server/front/src/__start.ts b/server/front/src/__start.ts
new file mode 100644
index 0000000000..7f9dacb089
--- /dev/null
+++ b/server/front/src/__start.ts
@@ -0,0 +1,58 @@
+//
+// Copyright © 2020, 2021 Anticrm Platform Contributors.
+// Copyright © 2021 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import { start } from './app'
+import { Client } from 'minio'
+
+const url = process.env.TRANSACTOR_URL
+if (url === undefined) {
+  console.error('please provide transactor url')
+  process.exit(1)
+}
+
+const elasticUrl = process.env.ELASTIC_URL
+if (elasticUrl === undefined) {
+  console.error('please provide elastic url')
+  process.exit(1)
+}
+
+const minioEndpoint = process.env.MINIO_ENDPOINT
+if (minioEndpoint === undefined) {
+  console.error('please provide minio endpoint')
+  process.exit(1)
+}
+
+const minioAccessKey = process.env.MINIO_ACCESS_KEY
+if (minioAccessKey === undefined) {
+  console.error('please provide minio access key')
+  process.exit(1)
+}
+
+const minioSecretKey = process.env.MINIO_SECRET_KEY
+if (minioSecretKey === undefined) {
+  console.error('please provide minio secret key')
+  process.exit(1)
+}
+
+const minio = new Client({
+  endPoint: minioEndpoint,
+  port: 9000,
+  useSSL: false,
+  accessKey: minioAccessKey,
+  secretKey: minioSecretKey
+})
+
+start(url, elasticUrl, minio, 8080)
diff --git a/server/front/src/app.ts b/server/front/src/app.ts
new file mode 100644
index 0000000000..cbf267b6f5
--- /dev/null
+++ b/server/front/src/app.ts
@@ -0,0 +1,190 @@
+//
+// Copyright © 2020, 2021 Anticrm Platform Contributors.
+// Copyright © 2021 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import { resolve, join } from 'path'
+import express from 'express'
+import fileUpload, { UploadedFile } from 'express-fileupload'
+import cors from 'cors'
+import { v4 as uuid } from 'uuid'
+import { decode } from 'jwt-simple'
+
+import { Space, Ref, Doc, Account, generateId } from '@anticrm/core'
+// import { TxFactory } from '@anticrm/core'
+import type { Token, IndexedDoc } from '@anticrm/server-core'
+import { createElasticAdapter } from '@anticrm/elastic'
+import chunter from '@anticrm/chunter'
+// import { createContributingClient } from '@anticrm/contrib'
+
+import { Client, ItemBucketMetadata } from 'minio'
+
+// import { createElasticAdapter } from '@anticrm/elastic'
+
+// const BUCKET = 'anticrm-upload-9e4e89c'
+
+// async function awsUpload (file: UploadedFile): Promise<string> {
+//   const id = uuid()
+//   const s3 = new S3()
+//   const resp = await s3.upload({
+//     Bucket: BUCKET,
+//     Key: id,
+//     Body: file.data,
+//     ContentType: file.mimetype,
+//     ACL: 'public-read'
+//   }).promise()
+//   console.log(resp)
+//   return id
+// }
+
+async function minioUpload (minio: Client, workspace: string, file: UploadedFile): Promise<string> {
+  const id = uuid()
+  const meta: ItemBucketMetadata = {
+    'Content-Type': file.mimetype
+  }
+
+  const resp = await minio.putObject(workspace, id, file.data, file.size, meta)
+
+  console.log(resp)
+  return id
+}
+
+// async function createAttachment (endpoint: string, token: string, account: Ref<Account>, space: Ref<Space>, attachedTo: Ref<Doc>, collection: string, name: string, file: string): Promise<void> {
+//   const txFactory = new TxFactory(account)
+//   const tx = txFactory.createTxCreateDoc(chunter.class.Attachment, space, {
+//     attachedTo,
+//     collection,
+//     name,
+//     file
+//   })
+//   const url = new URL(`/${token}`, endpoint)
+//   const client = await createContributingClient(url.href)
+//   await client.tx(tx)
+//   client.close()
+// }
+
+/**
+ * @public
+ * @param port -
+ */
+export function start (transactorEndpoint: string, elasticUrl: string, minio: Client, port: number): void {
+  const app = express()
+
+  app.use(cors())
+  app.use(fileUpload())
+
+  const dist = resolve(__dirname, 'dist')
+  console.log('serving static files from', dist)
+  app.use(express.static(dist, { maxAge: '10m' }))
+
+  // eslint-disable-next-line @typescript-eslint/no-misused-promises
+  app.get('/files', async (req, res) => {
+    try {
+      const token = req.query.token as string
+      const payload = decode(token, 'secret', false) as Token
+      const uuid = req.query.file as string
+
+      const stat = await minio.statObject(payload.workspace, uuid)
+      minio.getObject(payload.workspace, uuid, function (err, dataStream) {
+        if (err !== null) {
+          return console.log(err)
+        }
+        res.status(200)
+
+        const contentType = stat.metaData['content-type']
+        if (contentType !== undefined) {
+          res.setHeader('Content-Type', contentType)
+        }
+
+        dataStream.on('data', function (chunk) {
+          res.write(chunk)
+        })
+        dataStream.on('end', function () {
+          res.end()
+        })
+        dataStream.on('error', function (err) {
+          console.log(err)
+        })
+      })
+    } catch (error) {
+      console.log(error)
+      res.status(500).send()
+    }
+  })
+
+  // eslint-disable-next-line @typescript-eslint/no-misused-promises
+  app.post('/files', async (req, res) => {
+    const file = req.files?.file as UploadedFile
+
+    if (file === undefined) {
+      res.status(400).send()
+      return
+    }
+
+    const authHeader = req.headers.authorization
+    if (authHeader === undefined) {
+      res.status(403).send()
+      return
+    }
+
+    try {
+      const token = authHeader.split(' ')[1]
+      const payload = decode(token ?? '', 'secret', false) as Token
+      // const fileId = await awsUpload(file as UploadedFile)
+      const uuid = await minioUpload(minio, payload.workspace, file)
+      console.log('uploaded uuid', uuid)
+
+      const name = req.query.name as string
+      const space = req.query.space as Ref<Space>
+      const attachedTo = req.query.attachedTo as Ref<Doc>
+      // const name = req.query.name as string
+
+      // await createAttachment(
+      //   transactorEndpoint,
+      //   token,
+      //   'core:account:System' as Ref<Account>,
+      //   space,
+      //   attachedTo,
+      //   collection,
+      //   name,
+      //   fileId
+      // )
+
+      const elastic = await createElasticAdapter(elasticUrl, payload.workspace)
+
+      const indexedDoc: IndexedDoc = {
+        id: generateId() + '/attachments/' + name,
+        _class: chunter.class.Attachment,
+        space,
+        modifiedOn: Date.now(),
+        modifiedBy: 'core:account:System' as Ref<Account>,
+        attachedTo,
+        data: file.data.toString('base64')
+      }
+
+      await elastic.index(indexedDoc)
+
+      res.status(200).send(uuid)
+    } catch (error) {
+      console.log(error)
+      res.status(500).send()
+    }
+  })
+
+  app.get('*', function (request, response) {
+    response.sendFile(join(dist, 'index.html'))
+  })
+
+  app.listen(port)
+}