mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-18 05:57:32 +02:00
296 lines
8.0 KiB
TypeScript
296 lines
8.0 KiB
TypeScript
import core, {
|
|
AccountRole,
|
|
WorkspaceEvent,
|
|
generateId,
|
|
getTypeOf,
|
|
systemAccountEmail,
|
|
type Account,
|
|
type BackupClient,
|
|
type Branding,
|
|
type BrandingMap,
|
|
type BulkUpdateEvent,
|
|
type Class,
|
|
type Client,
|
|
type Doc,
|
|
type DocInfo,
|
|
type MeasureContext,
|
|
type ModelDb,
|
|
type Ref,
|
|
type SessionData,
|
|
type TxWorkspaceEvent,
|
|
type WorkspaceIdWithUrl
|
|
} from '@hcengineering/core'
|
|
import platform, { PlatformError, Severity, Status, unknownError } from '@hcengineering/platform'
|
|
import { createHash, type Hash } from 'crypto'
|
|
import fs from 'fs'
|
|
import { BackupClientOps } from './storage'
|
|
import type { Pipeline } from './types'
|
|
|
|
/**
|
|
* Return some estimation for object size
|
|
*/
|
|
export function estimateDocSize (_obj: any): number {
|
|
let result = 0
|
|
const toProcess = [_obj]
|
|
while (toProcess.length > 0) {
|
|
const obj = toProcess.shift()
|
|
if (typeof obj === 'undefined') {
|
|
continue
|
|
}
|
|
if (typeof obj === 'function') {
|
|
continue
|
|
}
|
|
for (const key in obj) {
|
|
// include prototype properties
|
|
const value = obj[key]
|
|
const type = getTypeOf(value)
|
|
result += key.length
|
|
|
|
switch (type) {
|
|
case 'Array':
|
|
result += 4
|
|
toProcess.push(value)
|
|
break
|
|
case 'Object':
|
|
toProcess.push(value)
|
|
break
|
|
case 'Date':
|
|
result += 24 // Some value
|
|
break
|
|
case 'string':
|
|
result += (value as string).length
|
|
break
|
|
case 'number':
|
|
result += 8
|
|
break
|
|
case 'boolean':
|
|
result += 1
|
|
break
|
|
case 'symbol':
|
|
result += (value as symbol).toString().length
|
|
break
|
|
case 'bigint':
|
|
result += (value as bigint).toString().length
|
|
break
|
|
case 'undefined':
|
|
result += 1
|
|
break
|
|
case 'null':
|
|
result += 1
|
|
break
|
|
default:
|
|
result += value.toString().length
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
/**
|
|
* Return some estimation for object size
|
|
*/
|
|
export function updateHashForDoc (hash: Hash, _obj: any): void {
|
|
const toProcess = [_obj]
|
|
while (toProcess.length > 0) {
|
|
const obj = toProcess.shift()
|
|
if (typeof obj === 'undefined') {
|
|
continue
|
|
}
|
|
if (typeof obj === 'function') {
|
|
continue
|
|
}
|
|
for (const key in obj) {
|
|
// include prototype properties
|
|
const value = obj[key]
|
|
const type = getTypeOf(value)
|
|
hash.update(key)
|
|
|
|
switch (type) {
|
|
case 'Array':
|
|
toProcess.push(value)
|
|
break
|
|
case 'Object':
|
|
toProcess.push(value)
|
|
break
|
|
case 'Date':
|
|
hash.update(value.toString())
|
|
break
|
|
case 'string':
|
|
hash.update(value)
|
|
break
|
|
case 'number':
|
|
hash.update((value as number).toString(16))
|
|
break
|
|
case 'boolean':
|
|
hash.update((value as boolean) ? '1' : '0')
|
|
break
|
|
case 'symbol':
|
|
hash.update((value as symbol).toString())
|
|
break
|
|
case 'bigint':
|
|
hash.update((value as bigint).toString())
|
|
break
|
|
case 'undefined':
|
|
hash.update('und')
|
|
break
|
|
case 'null':
|
|
hash.update('null')
|
|
break
|
|
default:
|
|
hash.update(value.toString())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getUser (modelDb: ModelDb, userEmail: string | undefined, admin?: boolean): Account {
|
|
if (userEmail === undefined) {
|
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
|
}
|
|
const account = modelDb.getAccountByEmail(userEmail)
|
|
if (account === undefined) {
|
|
if (userEmail === systemAccountEmail || admin === true) {
|
|
return {
|
|
_id: core.account.System,
|
|
_class: core.class.Account,
|
|
role: AccountRole.Owner,
|
|
email: systemAccountEmail,
|
|
space: core.space.Model,
|
|
modifiedBy: core.account.System,
|
|
modifiedOn: 0
|
|
}
|
|
}
|
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
|
}
|
|
return account
|
|
}
|
|
|
|
export class SessionDataImpl implements SessionData {
|
|
_account: Account | undefined
|
|
|
|
constructor (
|
|
readonly userEmail: string,
|
|
readonly sessionId: string,
|
|
readonly admin: boolean | undefined,
|
|
readonly broadcast: SessionData['broadcast'],
|
|
readonly workspace: WorkspaceIdWithUrl,
|
|
readonly branding: Branding | null,
|
|
readonly isAsyncContext: boolean,
|
|
readonly removedMap: Map<Ref<Doc>, Doc>,
|
|
readonly contextCache: Map<string, any>,
|
|
readonly modelDb: ModelDb,
|
|
readonly rawAccount?: Account
|
|
) {}
|
|
|
|
get account (): Account {
|
|
this._account = this.rawAccount ?? this._account ?? getUser(this.modelDb, this.userEmail, this.admin)
|
|
return this._account
|
|
}
|
|
|
|
getAccount (account: Ref<Account>): Account | undefined {
|
|
return this.modelDb.findObject(account)
|
|
}
|
|
}
|
|
|
|
export function createBroadcastEvent (classes: Ref<Class<Doc>>[]): TxWorkspaceEvent<BulkUpdateEvent> {
|
|
return {
|
|
_class: core.class.TxWorkspaceEvent,
|
|
_id: generateId(),
|
|
event: WorkspaceEvent.BulkUpdate,
|
|
params: {
|
|
_class: classes
|
|
},
|
|
modifiedBy: core.account.System,
|
|
modifiedOn: Date.now(),
|
|
objectSpace: core.space.DerivedTx,
|
|
space: core.space.DerivedTx
|
|
}
|
|
}
|
|
|
|
export function loadBrandingMap (brandingPath?: string): BrandingMap {
|
|
let brandings: BrandingMap = {}
|
|
if (brandingPath !== undefined && brandingPath !== '') {
|
|
brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8'))
|
|
|
|
for (const [host, value] of Object.entries(brandings)) {
|
|
const protocol = value.protocol ?? 'https'
|
|
value.front = `${protocol}://${host}/`
|
|
}
|
|
}
|
|
|
|
return brandings
|
|
}
|
|
|
|
export function toDocInfo (d: Doc, bulkUpdate: Map<Ref<Doc>, string>, recheck?: boolean): DocInfo {
|
|
let digest: string | null = (d as any)['%hash%']
|
|
if ('%hash%' in d) {
|
|
delete d['%hash%']
|
|
}
|
|
const pos = (digest ?? '').indexOf('|')
|
|
const oldDigest = digest
|
|
if (digest == null || digest === '' || recheck === true) {
|
|
const size = estimateDocSize(d)
|
|
|
|
const hash = createHash('sha256')
|
|
updateHashForDoc(hash, d)
|
|
digest = hash.digest('base64')
|
|
const newDigest = `${digest}|${size.toString(16)}`
|
|
|
|
if (recheck !== true || oldDigest !== newDigest) {
|
|
bulkUpdate.set(d._id, `${digest}|${size.toString(16)}`)
|
|
}
|
|
return {
|
|
id: d._id,
|
|
hash: digest,
|
|
size
|
|
}
|
|
} else {
|
|
return {
|
|
id: d._id,
|
|
hash: pos >= 0 ? digest.slice(0, pos) : digest,
|
|
size: pos >= 0 ? parseInt(digest.slice(pos + 1), 16) : 0
|
|
}
|
|
}
|
|
}
|
|
|
|
export function wrapPipeline (
|
|
ctx: MeasureContext,
|
|
pipeline: Pipeline,
|
|
wsUrl: WorkspaceIdWithUrl
|
|
): Client & BackupClient {
|
|
const contextData = new SessionDataImpl(
|
|
systemAccountEmail,
|
|
'pipeline',
|
|
true,
|
|
{ targets: {}, txes: [] },
|
|
wsUrl,
|
|
null,
|
|
true,
|
|
new Map(),
|
|
new Map(),
|
|
pipeline.context.modelDb
|
|
)
|
|
ctx.contextData = contextData
|
|
if (pipeline.context.lowLevelStorage === undefined) {
|
|
throw new PlatformError(unknownError('Low level storage is not available'))
|
|
}
|
|
const backupOps = new BackupClientOps(pipeline.context.lowLevelStorage)
|
|
|
|
return {
|
|
findAll: (_class, query, options) => pipeline.findAll(ctx, _class, query, options),
|
|
findOne: async (_class, query, options) =>
|
|
(await pipeline.findAll(ctx, _class, query, { ...options, limit: 1 })).shift(),
|
|
clean: (domain, docs) => backupOps.clean(ctx, domain, docs),
|
|
close: () => pipeline.close(),
|
|
closeChunk: (idx) => backupOps.closeChunk(ctx, idx),
|
|
getHierarchy: () => pipeline.context.hierarchy,
|
|
getModel: () => pipeline.context.modelDb,
|
|
loadChunk: (domain, idx, recheck) => backupOps.loadChunk(ctx, domain, idx, recheck),
|
|
loadDocs: (domain, docs) => backupOps.loadDocs(ctx, domain, docs),
|
|
upload: (domain, docs) => backupOps.upload(ctx, domain, docs),
|
|
searchFulltext: async (query, options) => ({ docs: [], total: 0 }),
|
|
sendForceClose: async () => {},
|
|
tx: (tx) => pipeline.tx(ctx, [tx]),
|
|
notify: (...tx) => {}
|
|
}
|
|
}
|