Use storage adapter instead of fetch in services (#6269)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-08-08 18:59:04 +07:00 committed by GitHub
parent 73cc6b8dd1
commit a966e184c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 251 additions and 316 deletions

3
.vscode/launch.json vendored
View File

@ -280,7 +280,8 @@
"SERVER_SECRET": "secret", "SERVER_SECRET": "secret",
"ACCOUNTS_URL": "http://localhost:3000", "ACCOUNTS_URL": "http://localhost:3000",
"COLLABORATOR_API_URL": "http://localhost:3078", "COLLABORATOR_API_URL": "http://localhost:3078",
"FRONT_URL": "http://localhost:8080" "STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"MONGO_URL": "mongodb://localhost:27017"
}, },
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20", "runtimeVersion": "20",

View File

@ -1538,9 +1538,6 @@ dependencies:
fork-ts-checker-webpack-plugin: fork-ts-checker-webpack-plugin:
specifier: ~7.3.0 specifier: ~7.3.0
version: 7.3.0(typescript@5.3.3)(webpack@5.90.3) version: 7.3.0(typescript@5.3.3)(webpack@5.90.3)
form-data:
specifier: ^4.0.0
version: 4.0.0
gaxios: gaxios:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.3 version: 5.1.3
@ -29133,7 +29130,7 @@ packages:
dev: false dev: false
file:projects/pod-calendar.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4): file:projects/pod-calendar.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-rnCLCkBGWm2o9YTk6AnoYlNCACtncXiYw+CUkhVFJmBNTE1l90C74Uci4ldJBpxEOyCHVBzJIjsp6sMbQwafkw==, tarball: file:projects/pod-calendar.tgz} resolution: {integrity: sha512-1kg3vKf6VEBcDutX3v29cjqDtrRghg7rXglm3OHuMwXC6WewfD+aM6xQ8hlUZjcVIq09Hv0nVr/n2SxUtal8LQ==, tarball: file:projects/pod-calendar.tgz}
id: file:projects/pod-calendar.tgz id: file:projects/pod-calendar.tgz
name: '@rush-temp/pod-calendar' name: '@rush-temp/pod-calendar'
version: 0.0.0 version: 0.0.0
@ -29363,7 +29360,7 @@ packages:
dev: false dev: false
file:projects/pod-gmail.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4): file:projects/pod-gmail.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-qeyt7Pl1TwEo/JojBg4R4LR3uWEdxOfqfz6tYB5kB27oEdE+jBJx0LGgsrDYoQSsv7A0LLnaFVfS+XT17EoKeQ==, tarball: file:projects/pod-gmail.tgz} resolution: {integrity: sha512-o5Ml8egkEHpCTaZ/hYgdA/5naofYkU52SAHnIZhyeFgIYKttpuNqi0wb25P36h3xhAA5WFdyytNPHOf7OBL4VA==, tarball: file:projects/pod-gmail.tgz}
id: file:projects/pod-gmail.tgz id: file:projects/pod-gmail.tgz
name: '@rush-temp/pod-gmail' name: '@rush-temp/pod-gmail'
version: 0.0.0 version: 0.0.0
@ -29374,6 +29371,7 @@ packages:
'@types/jest': 29.5.12 '@types/jest': 29.5.12
'@types/node': 20.11.19 '@types/node': 20.11.19
'@types/node-fetch': 2.6.11 '@types/node-fetch': 2.6.11
'@types/uuid': 8.3.4
'@types/ws': 8.5.11 '@types/ws': 8.5.11
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
@ -29401,6 +29399,7 @@ packages:
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3)
typescript: 5.3.3 typescript: 5.3.3
uuid: 8.3.2
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@aws-sdk/credential-providers' - '@aws-sdk/credential-providers'
@ -29710,7 +29709,7 @@ packages:
dev: false dev: false
file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4): file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-MF+eEeVhFR4XQj2YaAP6gjvm1tijtnRUMDrQt48vZBD8mwQA8B0drOVHCkOQ+NJOYqi1c3pRG/+kPKWqswbplQ==, tarball: file:projects/pod-telegram.tgz} resolution: {integrity: sha512-GCgA0Jny/OVcKWLkF1oA4awBVXQhFnHrfWujxfxke9g9S5FXNKrzZe/DCTlK/d/hRJ1XFj0Zw03rvBDhAKfOsg==, tarball: file:projects/pod-telegram.tgz}
id: file:projects/pod-telegram.tgz id: file:projects/pod-telegram.tgz
name: '@rush-temp/pod-telegram' name: '@rush-temp/pod-telegram'
version: 0.0.0 version: 0.0.0
@ -29722,6 +29721,7 @@ packages:
'@types/mime': 3.0.4 '@types/mime': 3.0.4
'@types/node': 20.11.19 '@types/node': 20.11.19
'@types/node-fetch': 2.6.11 '@types/node-fetch': 2.6.11
'@types/uuid': 8.3.4
'@types/ws': 8.5.11 '@types/ws': 8.5.11
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
@ -29749,6 +29749,7 @@ packages:
ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3)
ts-standard: 12.0.2(typescript@5.3.3) ts-standard: 12.0.2(typescript@5.3.3)
typescript: 5.3.3 typescript: 5.3.3
uuid: 8.3.2
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@aws-sdk/credential-providers' - '@aws-sdk/credential-providers'
@ -29760,7 +29761,6 @@ packages:
- babel-jest - babel-jest
- babel-plugin-macros - babel-plugin-macros
- bufferutil - bufferutil
- encoding
- gcp-metadata - gcp-metadata
- kerberos - kerberos
- mongodb-client-encryption - mongodb-client-encryption
@ -30161,7 +30161,7 @@ packages:
dev: false dev: false
file:projects/qms-doc-import-tool.tgz: file:projects/qms-doc-import-tool.tgz:
resolution: {integrity: sha512-jxeLHsk5jNj+ABvXoCiX9VTv7ovVKEdkwddcmwpdTGjt7hBgtYsirnVxkryV8tfWMs5y99wjroKGfwSqtyzzOg==, tarball: file:projects/qms-doc-import-tool.tgz} resolution: {integrity: sha512-mzTjks1peZbU8165Sd1xaoYs9H9f37XszY2zoxtggnaJrP1GeEktuX0TtNTz1XJRsMOg0+0K3s3CRYUEM9+cYw==, tarball: file:projects/qms-doc-import-tool.tgz}
name: '@rush-temp/qms-doc-import-tool' name: '@rush-temp/qms-doc-import-tool'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
@ -30171,6 +30171,7 @@ packages:
'@types/minio': 7.0.18 '@types/minio': 7.0.18
'@types/node': 20.11.19 '@types/node': 20.11.19
'@types/node-fetch': 2.6.11 '@types/node-fetch': 2.6.11
'@types/uuid': 8.3.4
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
commander: 8.3.0 commander: 8.3.0
@ -30193,6 +30194,7 @@ packages:
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
typescript: 5.3.3 typescript: 5.3.3
uuid: 8.3.2
zod: 3.23.8 zod: 3.23.8
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@ -30201,7 +30203,6 @@ packages:
- '@swc/wasm' - '@swc/wasm'
- babel-jest - babel-jest
- babel-plugin-macros - babel-plugin-macros
- encoding
- node-notifier - node-notifier
- supports-color - supports-color
dev: false dev: false

View File

@ -15,7 +15,7 @@
"build:watch": "compile", "build:watch": "compile",
"_phase:bundle": "rushx bundle", "_phase:bundle": "rushx bundle",
"bundle": "mkdir -p bundle && node esbuild.js", "bundle": "mkdir -p bundle && node esbuild.js",
"run-local": "cross-env SERVER_SECRET=secret COLLABORATOR_URL=ws://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 FRONT_URL=http://localhost:8087 PRODUCT_ID=ezqms node --nolazy -r ts-node/register ./src/__start.ts", "run-local": "cross-env SERVER_SECRET=secret MONGO_URL=mongodb://localhost:27017 COLLABORATOR_URL=ws://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin PRODUCT_ID=ezqms node --nolazy -r ts-node/register ./src/__start.ts",
"run": "cross-env node -r ts-node/register --max-old-space-size=8000 ./src/__start.ts", "run": "cross-env node -r ts-node/register --max-old-space-size=8000 ./src/__start.ts",
"format": "format src", "format": "format src",
"test": "jest --passWithNoTests --silent", "test": "jest --passWithNoTests --silent",
@ -36,7 +36,6 @@
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"@types/minio": "~7.0.11", "@types/minio": "~7.0.11",
"@types/node": "~20.11.16", "@types/node": "~20.11.16",
"@types/node-fetch": "~2.6.2",
"@typescript-eslint/parser": "^6.11.0", "@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0", "eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0", "prettier": "^3.1.0",
@ -55,6 +54,7 @@
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-storage": "^0.6.0",
"@hcengineering/server-token": "^0.6.11", "@hcengineering/server-token": "^0.6.11",
"@hcengineering/server-tool": "^0.6.0", "@hcengineering/server-tool": "^0.6.0",
"@hcengineering/server-client": "^0.6.0", "@hcengineering/server-client": "^0.6.0",
@ -62,11 +62,9 @@
"commander": "^8.1.0", "commander": "^8.1.0",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"domutils": "^3.1.0", "domutils": "^3.1.0",
"form-data": "^4.0.0",
"htmlparser2": "^9.0.0", "htmlparser2": "^9.0.0",
"mammoth": "^1.6.0", "mammoth": "^1.6.0",
"docx4js": "^3.2.20", "docx4js": "^3.2.20",
"node-fetch": "^2.6.6",
"zod": "^3.22.4" "zod": "^3.22.4"
} }
} }

View File

@ -1,3 +1,4 @@
import { MeasureContext } from '@hcengineering/core'
import docx4js from 'docx4js' import docx4js from 'docx4js'
import { AnyNode } from 'domhandler' import { AnyNode } from 'domhandler'
@ -7,7 +8,7 @@ import importExtractedFile from './import'
import convert from './convert/convert' import convert from './convert/convert'
import { Config } from './config' import { Config } from './config'
export async function importDoc (config: Config): Promise<void> { export async function importDoc (ctx: MeasureContext, config: Config): Promise<void> {
const { specFile, doc, backend } = config const { specFile, doc, backend } = config
const spec = await read(specFile) const spec = await read(specFile)
@ -23,5 +24,5 @@ export async function importDoc (config: Config): Promise<void> {
const contents = await convert(doc, backend) const contents = await convert(doc, backend)
const extractedFile = await extract(contents, spec, headerRoot) const extractedFile = await extract(contents, spec, headerRoot)
await importExtractedFile(config, extractedFile) await importExtractedFile(ctx, config, extractedFile)
} }

View File

@ -1,6 +1,7 @@
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { Ref, WorkspaceId } from '@hcengineering/core' import { Ref, WorkspaceId } from '@hcengineering/core'
import { DocumentSpace } from '@hcengineering/controlled-documents' import { DocumentSpace } from '@hcengineering/controlled-documents'
import { StorageAdapter } from '@hcengineering/server-core'
import { HtmlConversionBackend } from './convert/convert' import { HtmlConversionBackend } from './convert/convert'
@ -13,5 +14,6 @@ export interface Config {
owner: Ref<Employee> owner: Ref<Employee>
backend: HtmlConversionBackend backend: HtmlConversionBackend
space: Ref<DocumentSpace> space: Ref<DocumentSpace>
storageAdapter: StorageAdapter
specFile?: string specFile?: string
} }

View File

@ -1,6 +1,4 @@
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import fetch from 'node-fetch'
import FormData from 'form-data'
export async function readFile (doc: string): Promise<string> { export async function readFile (doc: string): Promise<string> {
const buffer = await fs.readFile(doc) const buffer = await fs.readFile(doc)
@ -21,40 +19,3 @@ export function compareStrExact (a: string, b: string): boolean {
export function clean (s: string): string { export function clean (s: string): string {
return s.replaceAll('\n', ' ').trim() return s.replaceAll('\n', ' ').trim()
} }
export async function uploadFile (
contents: string,
name: string,
type: string,
uploadURL: string,
token: string
): Promise<string> {
const data = new FormData()
const buffer = Buffer.from(contents, 'base64')
data.append(
'file',
Object.assign(buffer, {
lastModified: Date.now(),
name,
type
})
)
const resp = await fetch(uploadURL, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
},
body: data
})
if (resp.status !== 200) {
if (resp.status === 413) {
throw new Error('File is too large')
} else {
throw Error(`Failed to upload file: ${resp.statusText}`)
}
}
return await resp.text()
}

View File

@ -20,6 +20,7 @@ import core, {
BackupClient, BackupClient,
Client as CoreClient, Client as CoreClient,
Data, Data,
MeasureContext,
Ref, Ref,
TxOperations, TxOperations,
generateId, generateId,
@ -27,19 +28,21 @@ import core, {
systemAccountEmail, systemAccountEmail,
type Blob type Blob
} from '@hcengineering/core' } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform' import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
import serverCore from '@hcengineering/server-core' import { generateToken } from '@hcengineering/server-token'
import { findAll, getOuterHTML } from 'domutils' import { findAll, getOuterHTML } from 'domutils'
import { parseDocument } from 'htmlparser2' import { parseDocument } from 'htmlparser2'
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import { Config } from './config' import { Config } from './config'
import { ExtractedFile } from './extract/extract' import { ExtractedFile } from './extract/extract'
import { ExtractedSection } from './extract/sections' import { ExtractedSection } from './extract/sections'
import { compareStrExact, uploadFile } from './helpers' import { compareStrExact } from './helpers'
export default async function importExtractedFile (config: Config, extractedFile: ExtractedFile): Promise<void> { export default async function importExtractedFile (
ctx: MeasureContext,
config: Config,
extractedFile: ExtractedFile
): Promise<void> {
const { workspaceId } = config const { workspaceId } = config
const token = generateToken(systemAccountEmail, workspaceId) const token = generateToken(systemAccountEmail, workspaceId)
const transactorUrl = await getTransactorEndpoint(token, 'external') const transactorUrl = await getTransactorEndpoint(token, 'external')
@ -53,7 +56,7 @@ export default async function importExtractedFile (config: Config, extractedFile
try { try {
const docId = await createDocument(txops, extractedFile, config) const docId = await createDocument(txops, extractedFile, config)
const createdDoc = await txops.findOne(documents.class.Document, { _id: docId }) const createdDoc = await txops.findOne(documents.class.Document, { _id: docId })
await createSections(txops, extractedFile, config, createdDoc) await createSections(ctx, txops, extractedFile, config, createdDoc)
} finally { } finally {
await txops.close() await txops.close()
} }
@ -199,6 +202,7 @@ async function createTemplateIfNotExist (
} }
async function createSections ( async function createSections (
ctx: MeasureContext,
txops: TxOperations, txops: TxOperations,
extractedFile: ExtractedFile, extractedFile: ExtractedFile,
config: Config, config: Config,
@ -263,7 +267,7 @@ async function createSections (
prevSection = existingSection prevSection = existingSection
} }
await processImages(txops, section, config) await processImages(ctx, txops, section, config)
const collabSectionId = const collabSectionId =
existingSection != null && h.isDerived(existingSection._class, documents.class.CollaborativeDocumentSection) existingSection != null && h.isDerived(existingSection._class, documents.class.CollaborativeDocumentSection)
@ -296,13 +300,16 @@ async function createSections (
} }
} }
export async function processImages (txops: TxOperations, section: ExtractedSection, config: Config): Promise<void> { export async function processImages (
ctx: MeasureContext,
txops: TxOperations,
section: ExtractedSection,
config: Config
): Promise<void> {
const dom = parseDocument(section.content) const dom = parseDocument(section.content)
const imageNodes = findAll((n) => n.tagName === 'img', dom.children) const imageNodes = findAll((n) => n.tagName === 'img', dom.children)
const { uploadURL, token, space } = config const { storageAdapter, workspaceId, uploadURL, space } = config
const frontUrl = getMetadata(serverCore.metadata.FrontUrl) ?? ''
const fullUploadURL = `${frontUrl}${uploadURL}`
const imageUploads = imageNodes.map(async (img) => { const imageUploads = imageNodes.map(async (img) => {
const src = img.attribs.src const src = img.attribs.src
@ -313,14 +320,16 @@ export async function processImages (txops: TxOperations, section: ExtractedSect
return return
} }
const fileContents = extracted[2] const fileContentsBase64 = extracted[2]
const fileSize = Buffer.from(fileContents, 'base64').length const fileContents = Buffer.from(fileContentsBase64, 'base64')
const fileSize = fileContents.length
const mimeType = extracted[1] const mimeType = extracted[1]
const ext = mimeType.split('/')[1] const ext = mimeType.split('/')[1]
const fileName = `${generateId()}.${ext}` const fileName = `${generateId()}.${ext}`
// upload // upload
const uuid = await uploadFile(fileContents, fileName, mimeType, fullUploadURL, token) const uuid = generateId()
await storageAdapter.put(ctx, workspaceId, uuid, fileContents, mimeType, fileSize)
// attachment // attachment
const attachmentId: Ref<Attachment> = generateId() const attachmentId: Ref<Attachment> = generateId()

View File

@ -14,13 +14,14 @@
// //
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import documents, { DocumentSpace } from '@hcengineering/controlled-documents' import documents, { DocumentSpace } from '@hcengineering/controlled-documents'
import { Ref, getWorkspaceId, systemAccountEmail } from '@hcengineering/core' import { MeasureMetricsContext, Ref, getWorkspaceId, systemAccountEmail } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform' import { setMetadata } from '@hcengineering/platform'
import serverCore from '@hcengineering/server-core' import serverClientPlugin from '@hcengineering/server-client'
import { type StorageAdapter } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { generateToken } from '@hcengineering/server-token' import serverToken, { generateToken } from '@hcengineering/server-token'
import { program } from 'commander' import { program } from 'commander'
import serverClientPlugin from '@hcengineering/server-client'
import { importDoc } from './commands' import { importDoc } from './commands'
import { Config } from './config' import { Config } from './config'
import { getBackend } from './convert/convert' import { getBackend } from './convert/convert'
@ -29,6 +30,8 @@ import { getBackend } from './convert/convert'
* @public * @public
*/ */
export function docImportTool (): void { export function docImportTool (): void {
const ctx = new MeasureMetricsContext('doc-import-tool', {})
const serverSecret = process.env.SERVER_SECRET const serverSecret = process.env.SERVER_SECRET
if (serverSecret === undefined) { if (serverSecret === undefined) {
console.error('please provide server secret') console.error('please provide server secret')
@ -49,15 +52,24 @@ export function docImportTool (): void {
const uploadUrl = process.env.UPLOAD_URL ?? '/files' const uploadUrl = process.env.UPLOAD_URL ?? '/files'
const frontUrl = process.env.FRONT_URL const mongodbUri = process.env.MONGO_URL
if (frontUrl === undefined) { if (mongodbUri === undefined) {
console.log('Please provide front url') console.log('Please provide mongodb url')
process.exit(1) process.exit(1)
} }
setMetadata(serverClientPlugin.metadata.Endpoint, accountUrl) setMetadata(serverClientPlugin.metadata.Endpoint, accountUrl)
setMetadata(serverToken.metadata.Secret, serverSecret) setMetadata(serverToken.metadata.Secret, serverSecret)
setMetadata(serverCore.metadata.FrontUrl, frontUrl)
async function withStorage (mongodbUri: string, f: (storageAdapter: StorageAdapter) => Promise<any>): Promise<void> {
const adapter = buildStorageFromConfig(storageConfigFromEnv(), mongodbUri)
try {
await f(adapter)
} catch (err: any) {
console.error(err)
}
await adapter.close()
}
program.version('0.0.1') program.version('0.0.1')
@ -79,7 +91,8 @@ export function docImportTool (): void {
cmd.spec cmd.spec
}, space: ${cmd.space}, backend: ${cmd.backend}` }, space: ${cmd.space}, backend: ${cmd.backend}`
) )
try {
await withStorage(mongodbUri, async (storageAdapter) => {
const workspaceId = getWorkspaceId(workspace) const workspaceId = getWorkspaceId(workspace)
const config: Config = { const config: Config = {
@ -90,14 +103,13 @@ export function docImportTool (): void {
specFile: cmd.spec, specFile: cmd.spec,
space: cmd.space, space: cmd.space,
uploadURL: uploadUrl, uploadURL: uploadUrl,
storageAdapter,
collaboratorApiURL: collaboratorApiUrl, collaboratorApiURL: collaboratorApiUrl,
token: generateToken(systemAccountEmail, workspaceId) token: generateToken(systemAccountEmail, workspaceId)
} }
await importDoc(config) await importDoc(ctx, config)
} catch (err: any) { })
console.trace(err)
}
} }
) )

View File

@ -30,7 +30,7 @@ export async function removeDocument (
params: RpcMethodParams params: RpcMethodParams
): Promise<RemoveDocumentResponse> { ): Promise<RemoveDocumentResponse> {
const { documentId } = payload const { documentId } = payload
const { hocuspocus, minio } = params const { hocuspocus, storageAdapter } = params
const { workspaceId } = context const { workspaceId } = context
const document = hocuspocus.documents.get(documentId) const document = hocuspocus.documents.get(documentId)
@ -40,11 +40,11 @@ export async function removeDocument (
} }
const { collaborativeDoc } = parseDocumentId(documentId) const { collaborativeDoc } = parseDocumentId(documentId)
const { documentId: minioDocumentId } = collaborativeDocParse(collaborativeDoc) const { documentId: contentDocumentId } = collaborativeDocParse(collaborativeDoc)
const historyDocumentId = collaborativeHistoryDocId(minioDocumentId) const historyDocumentId = collaborativeHistoryDocId(contentDocumentId)
try { try {
await minio.remove(ctx, workspaceId, [minioDocumentId, historyDocumentId]) await storageAdapter.remove(ctx, workspaceId, [contentDocumentId, historyDocumentId])
} catch (err) { } catch (err) {
ctx.error('failed to remove document', { documentId, error: err }) ctx.error('failed to remove document', { documentId, error: err })
} }

View File

@ -37,7 +37,7 @@ export async function takeSnapshot (
params: RpcMethodParams params: RpcMethodParams
): Promise<TakeSnapshotResponse> { ): Promise<TakeSnapshotResponse> {
const { documentId, snapshotName, createdBy } = payload const { documentId, snapshotName, createdBy } = payload
const { hocuspocus, minio } = params const { hocuspocus, storageAdapter } = params
const { workspaceId } = context const { workspaceId } = context
const version: YDocVersion = { const version: YDocVersion = {
@ -48,7 +48,7 @@ export async function takeSnapshot (
} }
const { collaborativeDoc } = parseDocumentId(documentId) const { collaborativeDoc } = parseDocumentId(documentId)
const { documentId: minioDocumentId, versionId } = collaborativeDocParse(collaborativeDoc) const { documentId: contentDocumentId, versionId } = collaborativeDocParse(collaborativeDoc)
if (versionId !== CollaborativeDocVersionHead) { if (versionId !== CollaborativeDocVersionHead) {
throw new Error('invalid document version') throw new Error('invalid document version')
} }
@ -58,11 +58,11 @@ export async function takeSnapshot (
}) })
try { try {
// load history document directly from minio // load history document directly from storage
const historyDocumentId = collaborativeHistoryDocId(minioDocumentId) const historyDocumentId = collaborativeHistoryDocId(contentDocumentId)
const yHistory = const yHistory =
(await ctx.with('yDocFromMinio', {}, async () => { (await ctx.with('yDocFromStorage', {}, async () => {
return await yDocFromStorage(ctx, minio, workspaceId, historyDocumentId) return await yDocFromStorage(ctx, storageAdapter, workspaceId, historyDocumentId)
})) ?? new YDoc() })) ?? new YDoc()
await ctx.with('createYdocSnapshot', {}, async () => { await ctx.with('createYdocSnapshot', {}, async () => {
@ -71,8 +71,8 @@ export async function takeSnapshot (
}) })
}) })
await ctx.with('yDocToMinio', {}, async () => { await ctx.with('yDocToStorage', {}, async () => {
await yDocToStorage(ctx, minio, workspaceId, historyDocumentId, yHistory) await yDocToStorage(ctx, storageAdapter, workspaceId, historyDocumentId, yHistory)
}) })
return { ...version } return { ...version }

View File

@ -39,6 +39,6 @@ export type RpcMethod = (
export interface RpcMethodParams { export interface RpcMethodParams {
hocuspocus: Hocuspocus hocuspocus: Hocuspocus
minio: StorageAdapter storageAdapter: StorageAdapter
transformer: Transformer transformer: Transformer
} }

View File

@ -15,6 +15,8 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { MeasureContext, generateId, metricsAggregate } from '@hcengineering/core' import { MeasureContext, generateId, metricsAggregate } from '@hcengineering/core'
import type { MongoClientReference } from '@hcengineering/mongo'
import type { StorageAdapter } from '@hcengineering/server-core'
import { Token, decodeToken } from '@hcengineering/server-token' import { Token, decodeToken } from '@hcengineering/server-token'
import { ServerKit } from '@hcengineering/text' import { ServerKit } from '@hcengineering/text'
import { Hocuspocus } from '@hocuspocus/server' import { Hocuspocus } from '@hocuspocus/server'
@ -24,8 +26,6 @@ import express from 'express'
import { IncomingMessage, createServer } from 'http' import { IncomingMessage, createServer } from 'http'
import { WebSocket, WebSocketServer } from 'ws' import { WebSocket, WebSocketServer } from 'ws'
import type { MongoClientReference } from '@hcengineering/mongo'
import type { StorageAdapter } from '@hcengineering/server-core'
import { Config } from './config' import { Config } from './config'
import { Context } from './context' import { Context } from './context'
import { AuthenticationExtension } from './extensions/authentication' import { AuthenticationExtension } from './extensions/authentication'
@ -46,7 +46,7 @@ export type Shutdown = () => Promise<void>
export async function start ( export async function start (
ctx: MeasureContext, ctx: MeasureContext,
config: Config, config: Config,
minio: StorageAdapter, storageAdapter: StorageAdapter,
mongoClient: MongoClientReference mongoClient: MongoClientReference
): Promise<Shutdown> { ): Promise<Shutdown> {
const port = config.Port const port = config.Port
@ -116,7 +116,7 @@ export async function start (
}), }),
new StorageExtension({ new StorageExtension({
ctx: extensionsCtx.newChild('storage', {}), ctx: extensionsCtx.newChild('storage', {}),
adapter: new PlatformStorageAdapter({ minio }, mongo, transformer) adapter: new PlatformStorageAdapter(storageAdapter, mongo, transformer)
}) })
] ]
}) })
@ -179,7 +179,7 @@ export async function start (
await rpcCtx.with('/rpc', { method: request.method }, async (ctx) => { await rpcCtx.with('/rpc', { method: request.method }, async (ctx) => {
try { try {
const response: RpcResponse = await rpcCtx.with(request.method, {}, async (ctx) => { const response: RpcResponse = await rpcCtx.with(request.method, {}, async (ctx) => {
return await method(ctx, context, request.payload, { hocuspocus, minio, transformer }) return await method(ctx, context, request.payload, { hocuspocus, storageAdapter, transformer })
}) })
res.status(200).send(response) res.status(200).send(response)
} catch (err: any) { } catch (err: any) {

View File

@ -42,11 +42,9 @@ import { Context } from '../context'
import { CollabStorageAdapter } from './adapter' import { CollabStorageAdapter } from './adapter'
export type StorageAdapters = Record<string, StorageAdapter>
export class PlatformStorageAdapter implements CollabStorageAdapter { export class PlatformStorageAdapter implements CollabStorageAdapter {
constructor ( constructor (
private readonly adapters: StorageAdapters, private readonly storage: StorageAdapter,
private readonly mongodb: MongoClient, private readonly mongodb: MongoClient,
private readonly transformer: Transformer private readonly transformer: Transformer
) {} ) {}
@ -149,27 +147,16 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
} }
} }
getStorageAdapter (storage: string): StorageAdapter {
const adapter = this.adapters[storage]
if (adapter === undefined) {
throw new Error(`unknown storage adapter ${storage}`)
}
return adapter
}
async loadDocumentFromStorage ( async loadDocumentFromStorage (
ctx: MeasureContext, ctx: MeasureContext,
documentId: DocumentId, documentId: DocumentId,
context: Context context: Context
): Promise<YDoc | undefined> { ): Promise<YDoc | undefined> {
const { storage, collaborativeDoc } = parseDocumentId(documentId) const { collaborativeDoc } = parseDocumentId(documentId)
const adapter = this.getStorageAdapter(storage)
return await ctx.with('load-document', { storage }, async (ctx) => { return await ctx.with('load-document', {}, async (ctx) => {
return await withRetry(ctx, 5, async () => { return await withRetry(ctx, 5, async () => {
return await loadCollaborativeDoc(adapter, context.workspaceId, collaborativeDoc, ctx) return await loadCollaborativeDoc(this.storage, context.workspaceId, collaborativeDoc, ctx)
}) })
}) })
} }
@ -180,12 +167,11 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
document: YDoc, document: YDoc,
context: Context context: Context
): Promise<void> { ): Promise<void> {
const { storage, collaborativeDoc } = parseDocumentId(documentId) const { collaborativeDoc } = parseDocumentId(documentId)
const adapter = this.getStorageAdapter(storage)
await ctx.with('save-document', {}, async (ctx) => { await ctx.with('save-document', {}, async (ctx) => {
await withRetry(ctx, 5, async () => { await withRetry(ctx, 5, async () => {
await saveCollaborativeDoc(adapter, context.workspaceId, collaborativeDoc, document, ctx) await saveCollaborativeDoc(this.storage, context.workspaceId, collaborativeDoc, document, ctx)
}) })
}) })
} }
@ -197,8 +183,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
document: YDoc, document: YDoc,
context: Context context: Context
): Promise<YDocVersion | undefined> { ): Promise<YDocVersion | undefined> {
const { storage, collaborativeDoc } = parseDocumentId(documentId) const { collaborativeDoc } = parseDocumentId(documentId)
const adapter = this.getStorageAdapter(storage)
const { workspaceId } = context const { workspaceId } = context
@ -212,7 +197,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
} }
await ctx.with('take-snapshot', {}, async (ctx) => { await ctx.with('take-snapshot', {}, async (ctx) => {
await takeCollaborativeDocSnapshot(adapter, workspaceId, collaborativeDoc, document, yDocVersion, ctx) await takeCollaborativeDocSnapshot(this.storage, workspaceId, collaborativeDoc, document, yDocVersion, ctx)
}) })
return yDocVersion return yDocVersion

View File

@ -34,7 +34,6 @@
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/node": "~20.11.16", "@types/node": "~20.11.16",
"@types/node-fetch": "~2.6.2",
"@types/ws": "^8.5.11", "@types/ws": "^8.5.11",
"@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/eslint-plugin": "^6.11.0",
"@hcengineering/platform-rig": "^0.6.0", "@hcengineering/platform-rig": "^0.6.0",
@ -71,12 +70,10 @@
"express": "^4.19.2", "express": "^4.19.2",
"fast-equals": "^5.0.1", "fast-equals": "^5.0.1",
"file-api": "^0.10.4", "file-api": "^0.10.4",
"form-data": "^4.0.0",
"googleapis": "^122.0.0", "googleapis": "^122.0.0",
"google-auth-library": "^8.0.2", "google-auth-library": "^8.0.2",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"node-fetch": "^2.6.6",
"ws": "^8.18.0" "ws": "^8.18.0"
} }
} }

View File

@ -19,7 +19,6 @@ interface Config {
MongoURI: string MongoURI: string
MongoDB: string MongoDB: string
AccountsURL: string AccountsURL: string
UploadUrl: string
ServiceID: string ServiceID: string
Secret: string Secret: string
Credentials: string Credentials: string
@ -34,7 +33,6 @@ const envMap: { [key in keyof Config]: string } = {
MongoDB: 'MONGO_DB', MongoDB: 'MONGO_DB',
AccountsURL: 'ACCOUNTS_URL', AccountsURL: 'ACCOUNTS_URL',
UploadUrl: 'UPLOAD_URL',
ServiceID: 'SERVICE_ID', ServiceID: 'SERVICE_ID',
Secret: 'SECRET', Secret: 'SECRET',
Credentials: 'Credentials', Credentials: 'Credentials',
@ -50,7 +48,6 @@ const config: Config = (() => {
MongoDB: process.env[envMap.MongoDB] ?? 'calendar-service', MongoDB: process.env[envMap.MongoDB] ?? 'calendar-service',
MongoURI: process.env[envMap.MongoURI], MongoURI: process.env[envMap.MongoURI],
AccountsURL: process.env[envMap.AccountsURL], AccountsURL: process.env[envMap.AccountsURL],
UploadUrl: process.env[envMap.UploadUrl],
ServiceID: process.env[envMap.ServiceID] ?? 'calendar-service', ServiceID: process.env[envMap.ServiceID] ?? 'calendar-service',
Secret: process.env[envMap.Secret], Secret: process.env[envMap.Secret],
SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering', SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering',

View File

@ -35,7 +35,6 @@
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/node": "~20.11.16", "@types/node": "~20.11.16",
"@types/node-fetch": "~2.6.2",
"@types/ws": "^8.5.11", "@types/ws": "^8.5.11",
"@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0", "@typescript-eslint/parser": "^6.11.0",
@ -51,7 +50,8 @@
"@types/jest": "^29.5.5", "@types/jest": "^29.5.5",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"@types/uuid": "^8.3.1"
}, },
"dependencies": { "dependencies": {
"@hcengineering/attachment": "^0.6.14", "@hcengineering/attachment": "^0.6.14",
@ -61,22 +61,23 @@
"@hcengineering/mongo": "^0.6.1", "@hcengineering/mongo": "^0.6.1",
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/gmail": "^0.6.22", "@hcengineering/gmail": "^0.6.22",
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/setting": "^0.6.17", "@hcengineering/setting": "^0.6.17",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-client": "^0.6.0", "@hcengineering/server-client": "^0.6.0",
"@hcengineering/server-storage": "^0.6.0",
"@hcengineering/server-token": "^0.6.11",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "~16.0.0", "dotenv": "~16.0.0",
"express": "^4.19.2", "express": "^4.19.2",
"fast-equals": "^5.0.1", "fast-equals": "^5.0.1",
"file-api": "^0.10.4", "file-api": "^0.10.4",
"form-data": "^4.0.0",
"googleapis": "^122.0.0", "googleapis": "^122.0.0",
"google-auth-library": "^8.0.2", "google-auth-library": "^8.0.2",
"gaxios": "^5.0.1", "gaxios": "^5.0.1",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"node-fetch": "^2.6.6", "ws": "^8.18.0",
"ws": "^8.18.0" "uuid": "^8.3.2"
} }
} }

View File

@ -20,7 +20,6 @@ interface Config {
MongoURI: string MongoURI: string
MongoDB: string MongoDB: string
AccountsURL: string AccountsURL: string
UploadUrl: string
ServiceID: string ServiceID: string
Secret: string Secret: string
Credentials: string Credentials: string
@ -36,7 +35,6 @@ const envMap: { [key in keyof Config]: string } = {
MongoDB: 'MONGO_DB', MongoDB: 'MONGO_DB',
AccountsURL: 'ACCOUNTS_URL', AccountsURL: 'ACCOUNTS_URL',
UploadUrl: 'UPLOAD_URL',
ServiceID: 'SERVICE_ID', ServiceID: 'SERVICE_ID',
Secret: 'SECRET', Secret: 'SECRET',
Credentials: 'Credentials', Credentials: 'Credentials',
@ -53,7 +51,6 @@ const config: Config = (() => {
MongoDB: process.env[envMap.MongoDB] ?? 'gmail-service', MongoDB: process.env[envMap.MongoDB] ?? 'gmail-service',
MongoURI: process.env[envMap.MongoURI], MongoURI: process.env[envMap.MongoURI],
AccountsURL: process.env[envMap.AccountsURL], AccountsURL: process.env[envMap.AccountsURL],
UploadUrl: process.env[envMap.UploadUrl],
ServiceID: process.env[envMap.ServiceID] ?? 'gmail-service', ServiceID: process.env[envMap.ServiceID] ?? 'gmail-service',
Secret: process.env[envMap.Secret], Secret: process.env[envMap.Secret],
SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering', SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering',

View File

@ -16,11 +16,11 @@
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import core, { import core, {
AttachedData, AttachedData,
Blob,
Client, Client,
Data, Data,
Doc, MeasureContext,
Ref, Ref,
Space,
Timestamp, Timestamp,
TxCollectionCUD, TxCollectionCUD,
TxCreateDoc, TxCreateDoc,
@ -28,16 +28,16 @@ import core, {
TxOperations, TxOperations,
TxProcessor, TxProcessor,
TxUpdateDoc, TxUpdateDoc,
Blob WorkspaceId
} from '@hcengineering/core' } from '@hcengineering/core'
import gmail, { type Message, type NewMessage } from '@hcengineering/gmail' import gmail, { type Message, type NewMessage } from '@hcengineering/gmail'
import { type StorageAdapter } from '@hcengineering/server-core'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import FormData from 'form-data'
import type { GaxiosResponse } from 'gaxios' import type { GaxiosResponse } from 'gaxios'
import type { Credentials, OAuth2Client } from 'google-auth-library' import type { Credentials, OAuth2Client } from 'google-auth-library'
import { gmail_v1, google } from 'googleapis' import { gmail_v1, google } from 'googleapis'
import type { Collection, Db } from 'mongodb' import type { Collection, Db } from 'mongodb'
import fetch from 'node-fetch' import { v4 as uuid } from 'uuid'
import { arrayBufferToBase64, decode64, encode64 } from './base64' import { arrayBufferToBase64, decode64, encode64 } from './base64'
import config from './config' import config from './config'
import { GmailController } from './gmailController' import { GmailController } from './gmailController'
@ -51,55 +51,6 @@ const SCOPES = ['https://www.googleapis.com/auth/gmail.modify']
const EMAIL_REGEX = const EMAIL_REGEX =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/ /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
async function uploadFile (
file: AttachedFile,
token: string,
opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }
): Promise<string> {
const uploadUrl = config.UploadUrl
if (uploadUrl === undefined) {
throw Error('UploadURL is not defined')
}
const data = new FormData()
const buffer = Buffer.from(file.file, 'base64')
data.append(
'file',
Object.assign(buffer, {
lastModified: file.lastModified,
name: file.name,
type: file.type
})
)
const params =
opts !== undefined
? [
['space', opts.space],
['attachedTo', opts.attachedTo]
]
.filter((x): x is [string, Ref<any>] => x[1] !== undefined)
.map(([name, value]) => `${name}=${value}`)
.join('&')
: ''
const suffix = params === '' ? params : `?${params}`
const url = `${uploadUrl}${suffix}`
const resp = await fetch(url, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: data
})
if (resp.status !== 200) {
throw Error(`Failed to upload file: ${resp.statusText}`)
}
return (await resp.text()) as Ref<Blob>
}
function makeHTMLBody (message: NewMessage, from: string): string { function makeHTMLBody (message: NewMessage, from: string): string {
const str = [ const str = [
'Content-Type: text/html; charset="UTF-8"\n', 'Content-Type: text/html; charset="UTF-8"\n',
@ -207,10 +158,13 @@ export class GmailClient {
private readonly rateLimiter = new RateLimiter(1000, 200) // in fact 250, but let's make reserve private readonly rateLimiter = new RateLimiter(1000, 200) // in fact 250, but let's make reserve
private constructor ( private constructor (
private readonly ctx: MeasureContext,
credentials: ProjectCredentials, credentials: ProjectCredentials,
private readonly user: User, private readonly user: User,
mongo: Db, mongo: Db,
client: Client, client: Client,
private readonly workspaceId: WorkspaceId,
private readonly storageAdapter: StorageAdapter,
private readonly workspace: WorkspaceClient private readonly workspace: WorkspaceClient
) { ) {
const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line
@ -222,13 +176,16 @@ export class GmailClient {
} }
static async create ( static async create (
ctx: MeasureContext,
credentials: ProjectCredentials, credentials: ProjectCredentials,
user: User | Token, user: User | Token,
mongo: Db, mongo: Db,
client: Client, client: Client,
workspace: WorkspaceClient workspace: WorkspaceClient,
workspaceId: WorkspaceId,
storageAdapter: StorageAdapter
): Promise<GmailClient> { ): Promise<GmailClient> {
const gmailClient = new GmailClient(credentials, user, mongo, client, workspace) const gmailClient = new GmailClient(ctx, credentials, user, mongo, client, workspaceId, storageAdapter, workspace)
if (isToken(user)) { if (isToken(user)) {
console.log('Setting token while creating', user.workspace, user.userId, user) console.log('Setting token while creating', user.workspace, user.userId, user)
await gmailClient.setToken(user) await gmailClient.setToken(user)
@ -665,14 +622,15 @@ export class GmailClient {
if (currentAttachemtns.findIndex((p) => p.name === file.name && p.lastModified === file.lastModified) !== -1) { if (currentAttachemtns.findIndex((p) => p.name === file.name && p.lastModified === file.lastModified) !== -1) {
return return
} }
const uuid = await uploadFile(file, this.user.token, { space: message.space, attachedTo: message._id }) const id = uuid()
const data: AttachedData<Attachment> = { const data: AttachedData<Attachment> = {
name: file.name, name: file.name,
file: uuid as any, file: id as Ref<Blob>,
type: file.type ?? 'undefined', type: file.type ?? 'undefined',
size: file.size ?? Buffer.from(file.file, 'base64').length, size: file.size ?? Buffer.from(file.file, 'base64').length,
lastModified: file.lastModified lastModified: file.lastModified
} }
await this.storageAdapter.put(this.ctx, this.workspaceId, id, file.file, data.type, data.size)
await this.client.addCollection( await this.client.addCollection(
attachment.class.Attachment, attachment.class.Attachment,
message.space, message.space,
@ -858,7 +816,8 @@ export class GmailClient {
} }
private async makeAttachmentPart (attachment: Attachment): Promise<string[]> { private async makeAttachmentPart (attachment: Attachment): Promise<string[]> {
const data = await this.getAttachmentData(attachment.file) const buffer = await this.storageAdapter.read(this.ctx, this.workspaceId, attachment.file)
const data = arrayBufferToBase64(Buffer.concat(buffer))
const res: string[] = [] const res: string[] = []
res.push('--mail\n') res.push('--mail\n')
res.push(`Content-Type: ${attachment.type}\n`) res.push(`Content-Type: ${attachment.type}\n`)
@ -870,16 +829,6 @@ export class GmailClient {
return res return res
} }
private async getAttachmentData (fileId: string): Promise<string> {
const uploadUrl = config.UploadUrl
if (uploadUrl === undefined) {
throw Error('UploadURL is not defined')
}
const url = `${uploadUrl}?file=${fileId}&token=${this.user.token}`
const res = await fetch(url)
return arrayBufferToBase64(await res.arrayBuffer())
}
async close (): Promise<void> { async close (): Promise<void> {
if (this.watchTimer !== undefined) clearInterval(this.watchTimer) if (this.watchTimer !== undefined) clearInterval(this.watchTimer)
if (this.refreshTimer !== undefined) clearTimeout(this.refreshTimer) if (this.refreshTimer !== undefined) clearTimeout(this.refreshTimer)

View File

@ -13,6 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import { MeasureContext } from '@hcengineering/core'
import type { StorageAdapter } from '@hcengineering/server-core'
import { type Db } from 'mongodb' import { type Db } from 'mongodb'
import { decode64 } from './base64' import { decode64 } from './base64'
import config from './config' import config from './config'
@ -28,17 +31,27 @@ export class GmailController {
protected static _instance: GmailController protected static _instance: GmailController
private constructor (private readonly mongo: Db) { private constructor (
private readonly ctx: MeasureContext,
private readonly mongo: Db,
private readonly storageAdapter: StorageAdapter
) {
this.credentials = JSON.parse(config.Credentials) this.credentials = JSON.parse(config.Credentials)
GmailController._instance = this GmailController._instance = this
} }
static getGmailController (mongo?: Db): GmailController { static create (ctx: MeasureContext, mongo: Db, storageAdapter: StorageAdapter): GmailController {
if (GmailController._instance !== undefined) {
throw new Error('GmailController already exists')
}
return new GmailController(ctx, mongo, storageAdapter)
}
static getGmailController (): GmailController {
if (GmailController._instance !== undefined) { if (GmailController._instance !== undefined) {
return GmailController._instance return GmailController._instance
} }
if (mongo === undefined) throw new Error('GmailController not exist') throw new Error('GmailController not exist')
return new GmailController(mongo)
} }
async startAll (): Promise<void> { async startAll (): Promise<void> {
@ -108,7 +121,7 @@ export class GmailController {
let res = this.workspaces.get(workspace) let res = this.workspaces.get(workspace)
if (res === undefined) { if (res === undefined) {
try { try {
res = await WorkspaceClient.create(this.credentials, this.mongo, workspace) res = await WorkspaceClient.create(this.ctx, this.credentials, this.mongo, this.storageAdapter, workspace)
this.workspaces.set(workspace, res) this.workspaces.set(workspace, res)
} catch (err) { } catch (err) {
console.error(`Couldn't create workspace worker for ${workspace}, reason: `, err) console.error(`Couldn't create workspace worker for ${workspace}, reason: `, err)

View File

@ -14,8 +14,11 @@
// limitations under the License. // limitations under the License.
// //
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform' import { setMetadata } from '@hcengineering/platform'
import serverClient from '@hcengineering/server-client' import serverClient from '@hcengineering/server-client'
import { type StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { decodeToken } from '@hcengineering/server-token' import serverToken, { decodeToken } from '@hcengineering/server-token'
import { type IncomingHttpHeaders } from 'http' import { type IncomingHttpHeaders } from 'http'
import { decode64 } from './base64' import { decode64 } from './base64'
@ -34,11 +37,17 @@ const extractToken = (header: IncomingHttpHeaders): any => {
} }
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
const ctx = new MeasureMetricsContext('gmail', {}, {}, newMetrics())
setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) setMetadata(serverClient.metadata.Endpoint, config.AccountsURL)
setMetadata(serverClient.metadata.UserAgent, config.ServiceID) setMetadata(serverClient.metadata.UserAgent, config.ServiceID)
setMetadata(serverToken.metadata.Secret, config.Secret) setMetadata(serverToken.metadata.Secret, config.Secret)
const storageConfig: StorageConfiguration = storageConfigFromEnv()
const storageAdapter = buildStorageFromConfig(storageConfig, config.MongoURI)
const db = await getDB() const db = await getDB()
const gmailController = GmailController.getGmailController(db) const gmailController = GmailController.create(ctx, db, storageAdapter)
await gmailController.startAll() await gmailController.startAll()
const endpoints: Endpoint[] = [ const endpoints: Endpoint[] = [
{ {
@ -114,14 +123,15 @@ export const main = async (): Promise<void> => {
const server = listen(createServer(endpoints), config.Port) const server = listen(createServer(endpoints), config.Port)
const asyncClose = async (): Promise<void> => {
await gmailController.close()
await storageAdapter.close()
await closeDB()
}
const shutdown = (): void => { const shutdown = (): void => {
server.close(() => { server.close(() => {
void gmailController void asyncClose().then(() => process.exit())
.close()
.then(async () => {
await closeDB()
})
.then(() => process.exit())
}) })
} }

View File

@ -25,9 +25,11 @@ import core, {
type TxCreateDoc, type TxCreateDoc,
TxProcessor, TxProcessor,
type TxRemoveDoc, type TxRemoveDoc,
type TxUpdateDoc type TxUpdateDoc,
MeasureContext
} from '@hcengineering/core' } from '@hcengineering/core'
import gmailP, { type NewMessage } from '@hcengineering/gmail' import gmailP, { type NewMessage } from '@hcengineering/gmail'
import type { StorageAdapter } from '@hcengineering/server-core'
import { generateToken } from '@hcengineering/server-token' import { generateToken } from '@hcengineering/server-token'
import { type Db } from 'mongodb' import { type Db } from 'mongodb'
import { getClient } from './client' import { getClient } from './client'
@ -45,13 +47,21 @@ export class WorkspaceClient {
private readonly clients: Map<Ref<Account>, GmailClient> = new Map<Ref<Account>, GmailClient>() private readonly clients: Map<Ref<Account>, GmailClient> = new Map<Ref<Account>, GmailClient>()
private constructor ( private constructor (
private readonly ctx: MeasureContext,
private readonly credentials: ProjectCredentials, private readonly credentials: ProjectCredentials,
private readonly mongo: Db, private readonly mongo: Db,
private readonly storageAdapter: StorageAdapter,
private readonly workspace: string private readonly workspace: string
) {} ) {}
static async create (credentials: ProjectCredentials, mongo: Db, workspace: string): Promise<WorkspaceClient> { static async create (
const instance = new WorkspaceClient(credentials, mongo, workspace) ctx: MeasureContext,
credentials: ProjectCredentials,
mongo: Db,
storageAdapter: StorageAdapter,
workspace: string
): Promise<WorkspaceClient> {
const instance = new WorkspaceClient(ctx, credentials, mongo, storageAdapter, workspace)
await instance.initClient(workspace) await instance.initClient(workspace)
return instance return instance
} }
@ -59,7 +69,16 @@ export class WorkspaceClient {
async createGmailClient (user: User): Promise<GmailClient> { async createGmailClient (user: User): Promise<GmailClient> {
const current = this.getGmailClient(user.userId) const current = this.getGmailClient(user.userId)
if (current !== undefined) return current if (current !== undefined) return current
const newClient = await GmailClient.create(this.credentials, user, this.mongo, this.client, this) const newClient = await GmailClient.create(
this.ctx,
this.credentials,
user,
this.mongo,
this.client,
this,
{ name: this.workspace, productId: '' },
this.storageAdapter
)
this.clients.set(user.userId, newClient) this.clients.set(user.userId, newClient)
return newClient return newClient
} }

View File

@ -36,7 +36,6 @@
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/mime": "^3.0.1", "@types/mime": "^3.0.1",
"@types/node": "~20.11.16", "@types/node": "~20.11.16",
"@types/node-fetch": "~2.6.2",
"@types/ws": "^8.5.11", "@types/ws": "^8.5.11",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"prettier": "^3.1.0", "prettier": "^3.1.0",
@ -53,7 +52,8 @@
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0", "eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint": "^8.54.0" "eslint": "^8.54.0",
"@types/uuid": "^8.3.1"
}, },
"dependencies": { "dependencies": {
"@hcengineering/attachment": "^0.6.14", "@hcengineering/attachment": "^0.6.14",
@ -64,20 +64,21 @@
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/setting": "^0.6.17", "@hcengineering/setting": "^0.6.17",
"@hcengineering/telegram": "^0.6.21", "@hcengineering/telegram": "^0.6.21",
"@hcengineering/server-token": "^0.6.11", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-client": "^0.6.0", "@hcengineering/server-client": "^0.6.0",
"@hcengineering/server-storage": "^0.6.0",
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/mongo": "^0.6.1", "@hcengineering/mongo": "^0.6.1",
"big-integer": "^1.6.51", "big-integer": "^1.6.51",
"dotenv": "~16.0.0", "dotenv": "~16.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.19.2", "express": "^4.19.2",
"form-data": "^4.0.0",
"htmlparser2": "^9.0.0", "htmlparser2": "^9.0.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"mime": "^3.0.0", "mime": "^3.0.0",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"node-fetch": "^2.6.6",
"telegram": "2.22.2", "telegram": "2.22.2",
"ws": "^8.18.0" "ws": "^8.18.0",
"uuid": "^8.3.2"
} }
} }

View File

@ -9,7 +9,6 @@ interface Config {
MongoURI: string MongoURI: string
MongoDB: string MongoDB: string
UploadUrl: string
AccountsURL: string AccountsURL: string
ServiceID: string ServiceID: string
Secret: string Secret: string
@ -27,7 +26,6 @@ const envMap: { [key in keyof Config]: string } = {
MongoURI: 'MONGO_URI', MongoURI: 'MONGO_URI',
MongoDB: 'MONGO_DB', MongoDB: 'MONGO_DB',
UploadUrl: 'UPLOAD_URL',
AccountsURL: 'ACCOUNTS_URL', AccountsURL: 'ACCOUNTS_URL',
ServiceID: 'SERVICE_ID', ServiceID: 'SERVICE_ID',
Secret: 'SECRET', Secret: 'SECRET',
@ -52,14 +50,7 @@ const defaults: Partial<Config> = {
Secret: undefined Secret: undefined
} }
const required: Array<keyof Config> = [ const required: Array<keyof Config> = ['TelegramApiID', 'TelegramApiHash', 'MongoURI', 'AccountsURL', 'Secret']
'TelegramApiID',
'TelegramApiHash',
'UploadUrl',
'MongoURI',
'AccountsURL',
'Secret'
]
const mergeConfigs = <T>(defaults: Partial<T>, params: Partial<T>): T => { const mergeConfigs = <T>(defaults: Partial<T>, params: Partial<T>): T => {
const result = { ...defaults } const result = { ...defaults }
@ -81,7 +72,6 @@ const config = (() => {
TelegramApiID: parseNumber(process.env[envMap.TelegramApiID]), TelegramApiID: parseNumber(process.env[envMap.TelegramApiID]),
TelegramApiHash: process.env[envMap.TelegramApiHash], TelegramApiHash: process.env[envMap.TelegramApiHash],
TelegramAuthTTL: ttl === undefined ? ttl : ttl * 1000, TelegramAuthTTL: ttl === undefined ? ttl : ttl * 1000,
UploadUrl: process.env[envMap.UploadUrl],
MongoDB: process.env[envMap.MongoDB], MongoDB: process.env[envMap.MongoDB],
MongoURI: process.env[envMap.MongoURI], MongoURI: process.env[envMap.MongoURI],
AccountsURL: process.env[envMap.AccountsURL], AccountsURL: process.env[envMap.AccountsURL],

View File

@ -3,8 +3,11 @@ import { PlatformWorker } from './platform'
import { createServer, Handler, listen } from './server' import { createServer, Handler, listen } from './server'
import { telegram } from './telegram' import { telegram } from './telegram'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform' import { setMetadata } from '@hcengineering/platform'
import serverClient from '@hcengineering/server-client' import serverClient from '@hcengineering/server-client'
import { type StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { decodeToken, type Token } from '@hcengineering/server-token' import serverToken, { decodeToken, type Token } from '@hcengineering/server-token'
import config from './config' import config from './config'
@ -17,11 +20,16 @@ const extractToken = (header: IncomingHttpHeaders): Token | undefined => {
} }
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
const ctx = new MeasureMetricsContext('telegram', {}, {}, newMetrics())
setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) setMetadata(serverClient.metadata.Endpoint, config.AccountsURL)
setMetadata(serverClient.metadata.UserAgent, config.ServiceID) setMetadata(serverClient.metadata.UserAgent, config.ServiceID)
setMetadata(serverToken.metadata.Secret, config.Secret) setMetadata(serverToken.metadata.Secret, config.Secret)
const platformWorker = await PlatformWorker.create() const storageConfig: StorageConfiguration = storageConfigFromEnv()
const storageAdapter = buildStorageFromConfig(storageConfig, config.MongoURI)
const platformWorker = await PlatformWorker.create(ctx, storageAdapter)
const endpoints: Array<[string, Handler]> = [ const endpoints: Array<[string, Handler]> = [
[ [
'/signin', '/signin',
@ -187,10 +195,15 @@ export const main = async (): Promise<void> => {
const server = listen(createServer(endpoints), config.Port, config.Host) const server = listen(createServer(endpoints), config.Port, config.Host)
const asyncClose = async (): Promise<void> => {
await platformWorker.close()
await storageAdapter.close()
}
const shutdown = (): void => { const shutdown = (): void => {
server.close() server.close()
void Promise.all([platformWorker.close()]).then(() => process.exit()) void asyncClose().then(() => process.exit())
} }
process.on('SIGINT', shutdown) process.on('SIGINT', shutdown)

View File

@ -2,9 +2,13 @@ import type { Collection } from 'mongodb'
import { getDB } from './storage' import { getDB } from './storage'
import { LastMsgRecord, TgUser, User, UserRecord, WorkspaceChannel } from './types' import { LastMsgRecord, TgUser, User, UserRecord, WorkspaceChannel } from './types'
import { WorkspaceWorker } from './workspace' import { WorkspaceWorker } from './workspace'
import { StorageAdapter } from '@hcengineering/server-core'
import { MeasureContext } from '@hcengineering/core'
export class PlatformWorker { export class PlatformWorker {
private constructor ( private constructor (
private readonly ctx: MeasureContext,
private readonly storageAdapter: StorageAdapter,
private readonly clientMap: Map<string, WorkspaceWorker>, private readonly clientMap: Map<string, WorkspaceWorker>,
private readonly storage: Collection<UserRecord> private readonly storage: Collection<UserRecord>
) {} ) {}
@ -29,7 +33,14 @@ export class PlatformWorker {
if (wsWorker === undefined) { if (wsWorker === undefined) {
const [userStorage, lastMsgStorage, channelStorage] = await PlatformWorker.createStorages() const [userStorage, lastMsgStorage, channelStorage] = await PlatformWorker.createStorages()
wsWorker = await WorkspaceWorker.create(workspace, userStorage, lastMsgStorage, channelStorage) wsWorker = await WorkspaceWorker.create(
this.ctx,
this.storageAdapter,
workspace,
userStorage,
lastMsgStorage,
channelStorage
)
this.clientMap.set(workspace, wsWorker) this.clientMap.set(workspace, wsWorker)
} }
@ -80,13 +91,20 @@ export class PlatformWorker {
return [userStorage, lastMsgStorage, channelStorage] return [userStorage, lastMsgStorage, channelStorage]
} }
static async create (): Promise<PlatformWorker> { static async create (ctx: MeasureContext, storageAdapter: StorageAdapter): Promise<PlatformWorker> {
const [userStorage, lastMsgStorage, channelStorage] = await PlatformWorker.createStorages() const [userStorage, lastMsgStorage, channelStorage] = await PlatformWorker.createStorages()
const workspaces = new Set((await userStorage.find().toArray()).map((p) => p.workspace)) const workspaces = new Set((await userStorage.find().toArray()).map((p) => p.workspace))
const clients: Array<[string, WorkspaceWorker]> = [] const clients: Array<[string, WorkspaceWorker]> = []
for (const workspace of workspaces) { for (const workspace of workspaces) {
try { try {
const worker = await WorkspaceWorker.create(workspace, userStorage, lastMsgStorage, channelStorage) const worker = await WorkspaceWorker.create(
ctx,
storageAdapter,
workspace,
userStorage,
lastMsgStorage,
channelStorage
)
clients.push([workspace, worker]) clients.push([workspace, worker])
void worker.checkUsers() void worker.checkUsers()
} catch (e) { } catch (e) {
@ -97,7 +115,7 @@ export class PlatformWorker {
const res = clients.filter((client): client is [string, WorkspaceWorker] => client !== undefined) const res = clients.filter((client): client is [string, WorkspaceWorker] => client !== undefined)
const worker = new PlatformWorker(new Map(res), userStorage) const worker = new PlatformWorker(ctx, storageAdapter, new Map(res), userStorage)
return worker return worker
} }

View File

@ -1,10 +1,8 @@
import client, { ClientSocket } from '@hcengineering/client' import client, { ClientSocket } from '@hcengineering/client'
import { Client, Doc, Ref, Space } from '@hcengineering/core' import { Client } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform' import { setMetadata } from '@hcengineering/platform'
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
import FormData from 'form-data'
import mime from 'mime' import mime from 'mime'
import fetch from 'node-fetch'
import { Api } from 'telegram' import { Api } from 'telegram'
import WebSocket from 'ws' import WebSocket from 'ws'
import config from './config' import config from './config'
@ -58,55 +56,6 @@ export async function getFiles (msg: Api.Message): Promise<AttachedFile[]> {
return [obj] return [obj]
} }
export async function uploadFile (
file: AttachedFile,
token: string,
opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }
): Promise<string> {
const uploadUrl = config.UploadUrl
if (uploadUrl === undefined) {
throw Error('UploadURL is not defined')
}
const data = new FormData()
data.append(
'file',
Object.assign(file.file, {
lastModified: file.lastModified,
size: file.size,
name: file.name,
type: file.type
})
)
const params =
opts !== undefined
? [
['space', opts.space],
['attachedTo', opts.attachedTo]
]
.filter((x): x is [string, Ref<any>] => x[1] !== undefined)
.map(([name, value]) => `${name}=${value}`)
.join('&')
: ''
const suffix = params === '' ? params : `?${params}`
const url = `${uploadUrl}${suffix}`
const resp = await fetch(url, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: data
})
if (resp.status !== 200) {
throw Error(`Failed to upload file: ${resp.statusText}`)
}
return await resp.text()
}
export async function createPlatformClient (token: string): Promise<Client> { export async function createPlatformClient (token: string): Promise<Client> {
setMetadata(client.metadata.ClientSocketFactory, (url) => { setMetadata(client.metadata.ClientSocketFactory, (url) => {
return new WebSocket(url, { return new WebSocket(url, {

View File

@ -14,6 +14,7 @@ import core, {
Client, Client,
Doc, Doc,
Hierarchy, Hierarchy,
MeasureContext,
Ref, Ref,
Tx, Tx,
TxCollectionCUD, TxCollectionCUD,
@ -24,19 +25,20 @@ import core, {
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc TxUpdateDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import type { StorageAdapter } from '@hcengineering/server-core'
import { generateToken } from '@hcengineering/server-token' import { generateToken } from '@hcengineering/server-token'
import settingP from '@hcengineering/setting' import settingP from '@hcengineering/setting'
import telegramP, { NewTelegramMessage } from '@hcengineering/telegram' import telegramP, { NewTelegramMessage } from '@hcengineering/telegram'
import type { Collection } from 'mongodb' import type { Collection } from 'mongodb'
import fetch from 'node-fetch'
import { Api } from 'telegram' import { Api } from 'telegram'
import { v4 as uuid } from 'uuid'
import config from './config' import config from './config'
import { platformToTelegram, telegramToPlatform } from './markup' import { platformToTelegram, telegramToPlatform } from './markup'
import { MsgQueue } from './queue' import { MsgQueue } from './queue'
import type { TelegramConnectionInterface } from './telegram' import type { TelegramConnectionInterface } from './telegram'
import { telegram } from './telegram' import { telegram } from './telegram'
import { Event, LastMsgRecord, TelegramMessage, TgUser, UserRecord, WorkspaceChannel } from './types' import { Event, LastMsgRecord, TelegramMessage, TgUser, UserRecord, WorkspaceChannel } from './types'
import { createPlatformClient, getFiles, normalizeValue, uploadFile } from './utils' import { createPlatformClient, getFiles, normalizeValue } from './utils'
export class WorkspaceWorker { export class WorkspaceWorker {
private readonly clients = new Map< private readonly clients = new Map<
@ -52,8 +54,9 @@ export class WorkspaceWorker {
private readonly hierarchy: Hierarchy private readonly hierarchy: Hierarchy
private constructor ( private constructor (
private readonly ctx: MeasureContext,
private readonly client: Client, private readonly client: Client,
private readonly token: string, private readonly storageAdapter: StorageAdapter,
private readonly workspace: string, private readonly workspace: string,
private readonly userStorage: Collection<UserRecord>, private readonly userStorage: Collection<UserRecord>,
private readonly lastMsgStorage: Collection<LastMsgRecord>, private readonly lastMsgStorage: Collection<LastMsgRecord>,
@ -150,6 +153,8 @@ export class WorkspaceWorker {
} }
static async create ( static async create (
ctx: MeasureContext,
storageAdapter: StorageAdapter,
workspace: string, workspace: string,
userStorage: Collection<UserRecord>, userStorage: Collection<UserRecord>,
lastMsgStorage: Collection<LastMsgRecord>, lastMsgStorage: Collection<LastMsgRecord>,
@ -158,7 +163,15 @@ export class WorkspaceWorker {
const token = generateToken(config.SystemEmail, { name: workspace, productId: '' }) const token = generateToken(config.SystemEmail, { name: workspace, productId: '' })
const client = await createPlatformClient(token) const client = await createPlatformClient(token)
const worker = new WorkspaceWorker(client, token, workspace, userStorage, lastMsgStorage, channelsStorage) const worker = new WorkspaceWorker(
ctx,
client,
storageAdapter,
workspace,
userStorage,
lastMsgStorage,
channelsStorage
)
await worker.init() await worker.init()
return worker return worker
} }
@ -631,7 +644,8 @@ export class WorkspaceWorker {
const attachments = await this.client.findAll(attachment.class.Attachment, { attachedTo: msg._id }) const attachments = await this.client.findAll(attachment.class.Attachment, { attachedTo: msg._id })
const res: Buffer[] = [] const res: Buffer[] = []
for (const attachment of attachments) { for (const attachment of attachments) {
const buffer = await this.getAttachmentData(attachment.file) const chunks = await this.storageAdapter.read(this.ctx, { name: this.workspace, productId: '' }, attachment.file)
const buffer = Buffer.concat(chunks)
if (buffer.length > 0) { if (buffer.length > 0) {
res.push( res.push(
Object.assign(buffer, { Object.assign(buffer, {
@ -653,8 +667,16 @@ export class WorkspaceWorker {
const files = await getFiles(event.msg) const files = await getFiles(event.msg)
for (const file of files) { for (const file of files) {
try { try {
const id = uuid()
file.size = file.size ?? file.file.length file.size = file.size ?? file.file.length
const uuid = await uploadFile(file, this.token, { space: msg.space, attachedTo: msg._id }) await this.storageAdapter.put(
this.ctx,
{ name: this.workspace, productId: '' },
id,
file.file,
file.type,
file.size
)
const tx = factory.createTxCollectionCUD<TelegramMessage, Attachment>( const tx = factory.createTxCollectionCUD<TelegramMessage, Attachment>(
msg._class, msg._class,
msg._id, msg._id,
@ -662,7 +684,7 @@ export class WorkspaceWorker {
'attachments', 'attachments',
factory.createTxCreateDoc<Attachment>(attachment.class.Attachment, msg.space, { factory.createTxCreateDoc<Attachment>(attachment.class.Attachment, msg.space, {
name: file.name, name: file.name,
file: uuid as Ref<Blob>, file: id as Ref<Blob>,
type: file.type, type: file.type,
size: file.size, size: file.size,
lastModified: file.lastModified, lastModified: file.lastModified,
@ -681,17 +703,6 @@ export class WorkspaceWorker {
} }
} }
private async getAttachmentData (fileId: string): Promise<Buffer> {
const uploadUrl = config.UploadUrl
if (uploadUrl === undefined) {
throw Error('UploadURL is not defined')
}
const url = `${uploadUrl}?file=${fileId}&token=${this.token}`
const res = await fetch(url)
const buffer = await res.buffer()
return buffer
}
// #endregion // #endregion
// #endregion // #endregion