mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
UBERF-6490: Rework backup tool (#5386)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
4e1ca00fe1
commit
b64dc7b54f
@ -56,6 +56,9 @@ dependencies:
|
||||
'@rush-temp/auth-providers':
|
||||
specifier: file:./projects/auth-providers.tgz
|
||||
version: file:projects/auth-providers.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
'@rush-temp/backup-service':
|
||||
specifier: file:./projects/backup-service.tgz
|
||||
version: file:projects/backup-service.tgz(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
'@rush-temp/bitrix':
|
||||
specifier: file:./projects/bitrix.tgz
|
||||
version: file:projects/bitrix.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
@ -17038,7 +17041,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/account.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-gdOrX67T6n/Km9z3E9vHU90AXEZ8QwyOYLrLQfO0w+EanVjFErtvCNomVUIsgTkfjWk4diMN/FscChJ2FfO0lw==, tarball: file:projects/account.tgz}
|
||||
resolution: {integrity: sha512-9RuPhqNNHTYjQwezirzcJ6WZpMJTzbjc72jZSJC6dQFG7WqW0a2QZ8VTaa32IM902stPP950kaRUDGGEKlxEwg==, tarball: file:projects/account.tgz}
|
||||
id: file:projects/account.tgz
|
||||
name: '@rush-temp/account'
|
||||
version: 0.0.0
|
||||
@ -17416,6 +17419,38 @@ packages:
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/backup-service.tgz(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-V3tol7QGRHUEOi2hp9fv+nnjYVJJiMo/Pvfl5aRoL/CySk9vq/8KdQ6dBI2cFTLsRVjHiS4F4tzgsxcgZ09DQw==, tarball: file:projects/backup-service.tgz}
|
||||
id: file:projects/backup-service.tgz
|
||||
name: '@rush-temp/backup-service'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@types/node': 20.11.19
|
||||
'@types/tar-stream': 2.2.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)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
tar-stream: 2.2.0
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- esbuild
|
||||
- node-notifier
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/bitrix-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-aQD6a0vO8LgVaG8WlZUbCBOj/Al75h7pzI3u4zDr9QTqcCi4mj302yoJzPj7dRHBoTtmE0U/kqZsfVZaeOURxw==, tarball: file:projects/bitrix-assets.tgz}
|
||||
id: file:projects/bitrix-assets.tgz
|
||||
@ -18081,7 +18116,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/contact-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-VgYcfJpD4o2gzdBvypvWGaC7aOMeMYNL+Mc7+CnvxKI7mwSu1urHvxzrJCsKnptub6dBNxarnj0ESfdb4Gr+ww==, tarball: file:projects/contact-resources.tgz}
|
||||
resolution: {integrity: sha512-IFjZO0UGQ83ccZ4ki0quTRNpYBaVMpRyv5gf7QoOm85RbUzxhBm7jJOH2HXGBfL+XSSlmgt8TFxhLJDoGBLw0g==, tarball: file:projects/contact-resources.tgz}
|
||||
id: file:projects/contact-resources.tgz
|
||||
name: '@rush-temp/contact-resources'
|
||||
version: 0.0.0
|
||||
@ -18840,7 +18875,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/hr-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-82/nL+Vm0JAnfzAJV1hW8bQWYYeOFHhjYh/rXvkOJDttpW+m/J/b4uE9xIsSAmP+g2SSfzFEWysCr6p5gfEmmQ==, tarball: file:projects/hr-resources.tgz}
|
||||
resolution: {integrity: sha512-qkvCJBEeNFe/BPRkLU3SasE4DMHQBxotTd4FahDfLz8x/gDDbphklWmXIJh5rOdV+XLiCfpKGyWg9yYUELcuRg==, tarball: file:projects/hr-resources.tgz}
|
||||
id: file:projects/hr-resources.tgz
|
||||
name: '@rush-temp/hr-resources'
|
||||
version: 0.0.0
|
||||
@ -22371,7 +22406,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/server-notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-9ctaiwEU+M3RZ4Qz0/OuQ1lLh4Os9V8vlpf2FK/cpGOvAt5RUfV0qwiJLaImsLVbS62uz6hbZ1oy7Q3T+qex8Q==, tarball: file:projects/server-notification-resources.tgz}
|
||||
resolution: {integrity: sha512-uhmmKoMg/lF5f1B/DlmNUTfo8sDauKZSZEgqSNbNw8SsYoRJG9/xQHlXRPHs71tKeskt+jvwgs/z2385w5R3IA==, tarball: file:projects/server-notification-resources.tgz}
|
||||
id: file:projects/server-notification-resources.tgz
|
||||
name: '@rush-temp/server-notification-resources'
|
||||
version: 0.0.0
|
||||
@ -23007,7 +23042,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/server-tracker-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-sSU5k2FJCNeg9Jsmbkv3MskBYBjaYD+QOJBZCWaN5z0wpj2Gr3+dzhInFV3NMNn1wT8jjx91i2lQ3DOxK1CdJA==, tarball: file:projects/server-tracker-resources.tgz}
|
||||
resolution: {integrity: sha512-XBDFV1uHjmsarrrNZIixKXw9ov021Xq0yqDIwNWtJ3pLLXK1gFYPTWDARljpQx1DKFvB6Hy+qUxTmOj4xunNAQ==, tarball: file:projects/server-tracker-resources.tgz}
|
||||
id: file:projects/server-tracker-resources.tgz
|
||||
name: '@rush-temp/server-tracker-resources'
|
||||
version: 0.0.0
|
||||
@ -23929,7 +23964,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/text-editor.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(prosemirror-model@1.19.4)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-JbV3dbxXXUQ8HmfqUkI/XcmvHmhOOAwJcwkFybbg8pNHPO8jTRKmYQB6HyXNMgoe4XFfxKDwWRFG/DS0QBaLzg==, tarball: file:projects/text-editor.tgz}
|
||||
resolution: {integrity: sha512-G6/SsJ7NNINfdGwdI+6F9alZ+ow8Kmv+48UNx08Jh7Xz6lwvHuXWKRgh16LpzR7b5IPZJLyK3uz4zFo50JMTxA==, tarball: file:projects/text-editor.tgz}
|
||||
id: file:projects/text-editor.tgz
|
||||
name: '@rush-temp/text-editor'
|
||||
version: 0.0.0
|
||||
|
@ -25,7 +25,6 @@ import {
|
||||
getAccount,
|
||||
getWorkspaceById,
|
||||
listAccounts,
|
||||
listWorkspaces,
|
||||
listWorkspacesPure,
|
||||
listWorkspacesRaw,
|
||||
replacePassword,
|
||||
@ -58,6 +57,7 @@ import core, {
|
||||
MeasureMetricsContext,
|
||||
metricsToString,
|
||||
RateLimiter,
|
||||
versionToString,
|
||||
type AccountRole,
|
||||
type Data,
|
||||
type Tx,
|
||||
@ -68,6 +68,7 @@ import contact from '@hcengineering/model-contact'
|
||||
import { getMongoClient, getWorkspaceDB } from '@hcengineering/mongo'
|
||||
import { openAIConfigDefaults } from '@hcengineering/openai'
|
||||
import { type StorageAdapter } from '@hcengineering/server-core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import path from 'path'
|
||||
import { benchmark } from './benchmark'
|
||||
import {
|
||||
@ -86,6 +87,18 @@ import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
|
||||
import { openAIConfig } from './openai'
|
||||
import { fixAccountEmails, renameAccount } from './renameAccount'
|
||||
|
||||
const colorConstants = {
|
||||
colorRed: '\u001b[31m',
|
||||
colorBlue: '\u001b[34m',
|
||||
colorWhiteCyan: '\u001b[37;46m',
|
||||
colorRedYellow: '\u001b[31;43m',
|
||||
colorPing: '\u001b[38;5;201m',
|
||||
colorLavander: '\u001b[38;5;147m',
|
||||
colorAqua: '\u001b[38;2;145;231;255m',
|
||||
colorPencil: '\u001b[38;2;253;182;0m',
|
||||
reset: '\u001b[0m'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -468,11 +481,42 @@ export function devTool (
|
||||
program
|
||||
.command('list-workspaces')
|
||||
.description('List workspaces')
|
||||
.action(async () => {
|
||||
.option('-e|--expired [expired]', 'Show only expired', false)
|
||||
.action(async (cmd: { expired: boolean }) => {
|
||||
const { mongodbUri, version } = prepareTools()
|
||||
await withDatabase(mongodbUri, async (db) => {
|
||||
const workspacesJSON = JSON.stringify(await listWorkspaces(toolCtx, db, productId), null, 2)
|
||||
console.info(workspacesJSON)
|
||||
const workspacesJSON = await listWorkspacesPure(db, productId)
|
||||
for (const ws of workspacesJSON) {
|
||||
let lastVisit = Math.floor((Date.now() - ws.lastVisit) / 1000 / 3600 / 24)
|
||||
if (cmd.expired && lastVisit <= 7) {
|
||||
continue
|
||||
}
|
||||
console.log(
|
||||
colorConstants.colorBlue +
|
||||
'####################################################################################################' +
|
||||
colorConstants.reset
|
||||
)
|
||||
console.log('id:', colorConstants.colorWhiteCyan + ws.workspace + colorConstants.reset)
|
||||
console.log('url:', ws.workspaceUrl, 'name:', ws.workspaceName)
|
||||
console.log(
|
||||
'version:',
|
||||
ws.version !== undefined ? versionToString(ws.version) : 'not-set',
|
||||
!deepEqual(ws.version, version) ? `upgrade to ${versionToString(version)} is required` : ''
|
||||
)
|
||||
console.log('disabled:', ws.disabled)
|
||||
console.log('created by:', ws.createdBy)
|
||||
console.log('members:', (ws.accounts ?? []).length)
|
||||
if (Number.isNaN(lastVisit)) {
|
||||
lastVisit = 365
|
||||
}
|
||||
if (lastVisit > 30) {
|
||||
console.log(colorConstants.colorRed + `last visit: ${lastVisit} days ago` + colorConstants.reset)
|
||||
} else if (lastVisit > 7) {
|
||||
console.log(colorConstants.colorRedYellow + `last visit: ${lastVisit} days ago` + colorConstants.reset)
|
||||
} else {
|
||||
console.log('last visit:', lastVisit, 'days ago')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('latest model version:', JSON.stringify(version))
|
||||
})
|
||||
@ -481,7 +525,7 @@ export function devTool (
|
||||
program.command('fix-person-accounts').action(async () => {
|
||||
const { mongodbUri, version } = prepareTools()
|
||||
await withDatabase(mongodbUri, async (db, client) => {
|
||||
const ws = await listWorkspaces(toolCtx, db, productId)
|
||||
const ws = await listWorkspacesPure(db, productId)
|
||||
for (const w of ws) {
|
||||
const wsDb = getWorkspaceDB(client, { name: w.workspace, productId })
|
||||
await wsDb.collection('tx').updateMany(
|
||||
@ -534,6 +578,7 @@ export function devTool (
|
||||
.action(async (dirName: string, workspace: string, cmd: { skip: string, force: boolean }) => {
|
||||
const storage = await createFileBackupStorage(dirName)
|
||||
await backup(
|
||||
toolCtx,
|
||||
transactorUrl,
|
||||
getWorkspaceId(workspace, productId),
|
||||
storage,
|
||||
@ -548,7 +593,7 @@ export function devTool (
|
||||
.option('-f, --force', 'Force compact.', false)
|
||||
.action(async (dirName: string, cmd: { force: boolean }) => {
|
||||
const storage = await createFileBackupStorage(dirName)
|
||||
await compactBackup(storage, cmd.force)
|
||||
await compactBackup(toolCtx, storage, cmd.force)
|
||||
})
|
||||
|
||||
program
|
||||
@ -557,7 +602,14 @@ export function devTool (
|
||||
.description('dump workspace transactions and minio resources')
|
||||
.action(async (dirName: string, workspace: string, date, cmd: { merge: boolean }) => {
|
||||
const storage = await createFileBackupStorage(dirName)
|
||||
await restore(transactorUrl, getWorkspaceId(workspace, productId), storage, parseInt(date ?? '-1'), cmd.merge)
|
||||
await restore(
|
||||
toolCtx,
|
||||
transactorUrl,
|
||||
getWorkspaceId(workspace, productId),
|
||||
storage,
|
||||
parseInt(date ?? '-1'),
|
||||
cmd.merge
|
||||
)
|
||||
})
|
||||
|
||||
program
|
||||
@ -579,7 +631,7 @@ export function devTool (
|
||||
getWorkspaceId(bucketName, productId),
|
||||
dirName
|
||||
)
|
||||
await backup(transactorUrl, getWorkspaceId(workspace, productId), storage)
|
||||
await backup(toolCtx, transactorUrl, getWorkspaceId(workspace, productId), storage)
|
||||
})
|
||||
|
||||
program
|
||||
@ -594,7 +646,7 @@ export function devTool (
|
||||
getWorkspaceId(bucketName, productId),
|
||||
dirName
|
||||
)
|
||||
await compactBackup(storage, cmd.force)
|
||||
await compactBackup(toolCtx, storage, cmd.force)
|
||||
})
|
||||
|
||||
program
|
||||
@ -611,11 +663,11 @@ export function devTool (
|
||||
getWorkspaceId(bucketName, productId),
|
||||
dirName
|
||||
)
|
||||
const workspaces = await listWorkspaces(toolCtx, db, productId)
|
||||
const workspaces = await listWorkspacesPure(db, productId)
|
||||
|
||||
for (const w of workspaces) {
|
||||
console.log(`clearing ${w.workspace} history:`)
|
||||
await compactBackup(storage, cmd.force)
|
||||
await compactBackup(toolCtx, storage, cmd.force)
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -625,7 +677,7 @@ export function devTool (
|
||||
.action(async (bucketName: string, dirName: string, workspace: string, date, cmd) => {
|
||||
const { storageAdapter } = prepareTools()
|
||||
const storage = await createStorageBackupStorage(toolCtx, storageAdapter, getWorkspaceId(bucketName), dirName)
|
||||
await restore(transactorUrl, getWorkspaceId(workspace, productId), storage, parseInt(date ?? '-1'))
|
||||
await restore(toolCtx, transactorUrl, getWorkspaceId(workspace, productId), storage, parseInt(date ?? '-1'))
|
||||
})
|
||||
program
|
||||
.command('backup-s3-list <bucketName> <dirName>')
|
||||
@ -695,7 +747,7 @@ export function devTool (
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const workspaces = await listWorkspaces(toolCtx, db, productId)
|
||||
const workspaces = await listWorkspacesPure(db, productId)
|
||||
|
||||
for (const w of workspaces) {
|
||||
console.log(`clearing ${w.workspace} history:`)
|
||||
|
@ -650,3 +650,20 @@ export interface DomainIndexConfiguration extends Doc {
|
||||
|
||||
skip?: string[]
|
||||
}
|
||||
|
||||
export interface BaseWorkspaceInfo {
|
||||
workspace: string // An uniq workspace name, Database names
|
||||
productId: string
|
||||
disabled?: boolean
|
||||
version?: Data<Version>
|
||||
|
||||
workspaceUrl?: string | null // An optional url to the workspace, if not set workspace will be used
|
||||
workspaceName?: string // An displayed workspace name
|
||||
createdOn: number
|
||||
lastVisit: number
|
||||
|
||||
createdBy: string
|
||||
|
||||
creating?: boolean
|
||||
createProgress?: number // Some progress
|
||||
}
|
||||
|
@ -79,6 +79,8 @@ class Connection implements ClientConnection {
|
||||
private sessionId: string | undefined
|
||||
private closed = false
|
||||
|
||||
private upgrading: boolean = false
|
||||
|
||||
private pingResponse: number = Date.now()
|
||||
|
||||
constructor (
|
||||
@ -161,7 +163,11 @@ class Connection implements ClientConnection {
|
||||
throw new Error('connection closed')
|
||||
}
|
||||
this.pending = undefined
|
||||
console.log('failed to connect', err)
|
||||
if (!this.upgrading) {
|
||||
console.log('connection: failed to connect', this.lastId)
|
||||
} else {
|
||||
console.log('connection: workspace during upgrade', this.lastId)
|
||||
}
|
||||
if (err?.code === UNAUTHORIZED.code) {
|
||||
Analytics.handleError(err)
|
||||
this.onUnauthorized?.()
|
||||
@ -169,7 +175,9 @@ class Connection implements ClientConnection {
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(`delay ${this.delay} second`)
|
||||
if (!this.upgrading) {
|
||||
console.log(`delay ${this.delay} second`)
|
||||
}
|
||||
resolve(null)
|
||||
if (this.delay < 5) {
|
||||
this.delay++
|
||||
@ -220,7 +228,12 @@ class Connection implements ClientConnection {
|
||||
|
||||
websocket.onmessage = (event: MessageEvent) => {
|
||||
const resp = readResponse<any>(event.data, binaryResponse)
|
||||
if (resp.id === -1 && resp.result === 'upgrading') {
|
||||
this.upgrading = true
|
||||
return
|
||||
}
|
||||
if (resp.id === -1 && resp.result === 'hello') {
|
||||
this.upgrading = false
|
||||
if ((resp as HelloResponse).alreadyConnected === true) {
|
||||
this.sessionId = generateId()
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
|
@ -26,11 +26,11 @@
|
||||
{#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')}
|
||||
<Loading />
|
||||
{:then client}
|
||||
{#if !client && versionError}
|
||||
{#if $versionError}
|
||||
<div class="version-wrapper">
|
||||
<div class="antiPopup version-popup">
|
||||
<h1><Label label={workbench.string.ServerUnderMaintenance} /></h1>
|
||||
{versionError}
|
||||
{$versionError}
|
||||
</div>
|
||||
</div>
|
||||
{:else if client}
|
||||
|
@ -2,18 +2,19 @@ import { Analytics } from '@hcengineering/analytics'
|
||||
import client from '@hcengineering/client'
|
||||
import core, {
|
||||
ClientConnectEvent,
|
||||
setCurrentAccount,
|
||||
versionToString,
|
||||
type AccountClient,
|
||||
type Client,
|
||||
type Version,
|
||||
setCurrentAccount
|
||||
type Version
|
||||
} from '@hcengineering/core'
|
||||
import login, { loginId } from '@hcengineering/login'
|
||||
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||
import presentation, { closeClient, refreshClient, setClient } from '@hcengineering/presentation'
|
||||
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export let versionError: string | undefined = ''
|
||||
export const versionError = writable<string | undefined>(undefined)
|
||||
|
||||
let _token: string | undefined
|
||||
let _client: AccountClient | undefined
|
||||
@ -93,7 +94,7 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
if (currentVersionStr !== reconnectVersionStr) {
|
||||
// It seems upgrade happened
|
||||
// location.reload()
|
||||
versionError = `${currentVersionStr} != ${reconnectVersionStr}`
|
||||
versionError.set(`${currentVersionStr} != ${reconnectVersionStr}`)
|
||||
}
|
||||
const serverVersion: { version: string } = await (
|
||||
await fetch(serverEndpoint + '/api/v1/version', {})
|
||||
@ -101,7 +102,7 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
|
||||
console.log('Server version', serverVersion.version)
|
||||
if (serverVersion.version !== '' && serverVersion.version !== currentVersionStr) {
|
||||
versionError = `${currentVersionStr} => ${serverVersion.version}`
|
||||
versionError.set(`${currentVersionStr} => ${serverVersion.version}`)
|
||||
}
|
||||
}
|
||||
})()
|
||||
@ -131,7 +132,7 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
const versionStr = versionToString(version)
|
||||
|
||||
if (version === undefined || requiredVersion !== versionStr) {
|
||||
versionError = `${versionStr} => ${requiredVersion}`
|
||||
versionError.set(`${versionStr} => ${requiredVersion}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -139,17 +140,17 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
try {
|
||||
const serverVersion: { version: string } = await (await fetch(serverEndpoint + '/api/v1/version', {})).json()
|
||||
|
||||
console.log('Server version', serverVersion.version)
|
||||
console.log('Server version', serverVersion.version, version !== undefined ? versionToString(version) : '')
|
||||
if (
|
||||
serverVersion.version !== '' &&
|
||||
(version === undefined || serverVersion.version !== versionToString(version))
|
||||
) {
|
||||
const versionStr = version !== undefined ? versionToString(version) : 'unknown'
|
||||
versionError = `${versionStr} => ${serverVersion.version}`
|
||||
versionError.set(`${versionStr} => ${serverVersion.version}`)
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
versionError = 'server version not available'
|
||||
versionError.set('server version not available')
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
@ -158,11 +159,12 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
const requirdVersion = getMetadata(presentation.metadata.RequiredVersion)
|
||||
console.log('checking min model version', requirdVersion)
|
||||
if (requirdVersion !== undefined) {
|
||||
versionError = `'unknown' => ${requirdVersion}`
|
||||
versionError.set(`'unknown' => ${requirdVersion}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
versionError.set(undefined)
|
||||
// Update window title
|
||||
document.title = [ws, title].filter((it) => it).join(' - ')
|
||||
_clientSet = true
|
||||
|
@ -206,13 +206,13 @@
|
||||
{/if}
|
||||
<ComponentExtensions
|
||||
extension={tracker.extensions.EditIssueTitle}
|
||||
props={{ size: 'medium', kind: 'ghost', space: issue.space, issue, readonly }}
|
||||
props={{ size: 'medium', kind: 'ghost', space: issue.space, value: issue, readonly }}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="pre-utils">
|
||||
<ComponentExtensions
|
||||
extension={tracker.extensions.EditIssueHeader}
|
||||
props={{ size: 'medium', kind: 'ghost', space: issue.space, readonly, issue }}
|
||||
props={{ size: 'medium', kind: 'ghost', space: issue.space, readonly, value: issue }}
|
||||
/>
|
||||
{#if saved}
|
||||
<Label label={presentation.string.Saved} />
|
||||
|
@ -64,7 +64,7 @@
|
||||
{/if}
|
||||
</Loading>
|
||||
{:then client}
|
||||
{#if !client && versionError}
|
||||
{#if $versionError}
|
||||
<div class="version-wrapper">
|
||||
<div class="antiPopup version-popup">
|
||||
{#if isNeedUpgrade}
|
||||
@ -73,7 +73,7 @@
|
||||
{:else}
|
||||
<h1><Label label={workbench.string.ServerUnderMaintenance} /></h1>
|
||||
{/if}
|
||||
{versionError}
|
||||
{$versionError}
|
||||
</div>
|
||||
</div>
|
||||
{:else if client}
|
||||
|
@ -22,10 +22,11 @@ import {
|
||||
networkStatus,
|
||||
setMetadataLocalStorage
|
||||
} from '@hcengineering/ui'
|
||||
import { writable } from 'svelte/store'
|
||||
import plugin from './plugin'
|
||||
import { workspaceCreating } from './utils'
|
||||
|
||||
export let versionError: string | undefined = ''
|
||||
export const versionError = writable<string | undefined>(undefined)
|
||||
|
||||
let _token: string | undefined
|
||||
let _client: AccountClient | undefined
|
||||
@ -176,7 +177,7 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
if (currentVersionStr !== reconnectVersionStr) {
|
||||
// It seems upgrade happened
|
||||
// location.reload()
|
||||
versionError = `${currentVersionStr} != ${reconnectVersionStr}`
|
||||
versionError.set(`${currentVersionStr} != ${reconnectVersionStr}`)
|
||||
}
|
||||
const serverVersion: { version: string } = await ctx.with(
|
||||
'fetch-server-version',
|
||||
@ -184,9 +185,15 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
async () => await (await fetch(serverEndpoint + '/api/v1/version', {})).json()
|
||||
)
|
||||
|
||||
console.log('Server version', serverVersion.version)
|
||||
console.log(
|
||||
'Server version',
|
||||
serverVersion.version,
|
||||
version !== undefined ? versionToString(version) : ''
|
||||
)
|
||||
if (serverVersion.version !== '' && serverVersion.version !== currentVersionStr) {
|
||||
versionError = `${currentVersionStr} => ${serverVersion.version}`
|
||||
versionError.set(`${currentVersionStr} => ${serverVersion.version}`)
|
||||
} else {
|
||||
versionError.set(undefined)
|
||||
}
|
||||
}
|
||||
})()
|
||||
@ -237,7 +244,7 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
const versionStr = versionToString(version)
|
||||
|
||||
if (version === undefined || requiredVersion !== versionStr) {
|
||||
versionError = `${versionStr} => ${requiredVersion}`
|
||||
versionError.set(`${versionStr} => ${requiredVersion}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -249,30 +256,32 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
async () => await (await fetch(serverEndpoint + '/api/v1/version', {})).json()
|
||||
)
|
||||
|
||||
console.log('Server version', serverVersion.version)
|
||||
console.log('Server version', serverVersion.version, version !== undefined ? versionToString(version) : '')
|
||||
if (
|
||||
serverVersion.version !== '' &&
|
||||
(version === undefined || serverVersion.version !== versionToString(version))
|
||||
) {
|
||||
const versionStr = version !== undefined ? versionToString(version) : 'unknown'
|
||||
versionError = `${versionStr} => ${serverVersion.version}`
|
||||
versionError.set(`${versionStr} => ${serverVersion.version}`)
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
versionError = 'server version not available'
|
||||
versionError.set('server version not available')
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
Analytics.handleError(err)
|
||||
const requirdVersion = getMetadata(presentation.metadata.RequiredVersion)
|
||||
console.log('checking min model version', requirdVersion)
|
||||
if (requirdVersion !== undefined) {
|
||||
versionError = `'unknown' => ${requirdVersion}`
|
||||
const requiredVersion = getMetadata(presentation.metadata.RequiredVersion)
|
||||
console.log('checking min model version', requiredVersion)
|
||||
if (requiredVersion !== undefined) {
|
||||
versionError.set(`'unknown' => ${requiredVersion}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
versionError.set(undefined)
|
||||
|
||||
// Update window title
|
||||
document.title = [ws, title].filter((it) => it).join(' - ')
|
||||
_clientSet = true
|
||||
|
@ -47,16 +47,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"mongodb": "^6.3.0",
|
||||
"@hcengineering/server-tool": "^0.6.0",
|
||||
"@hcengineering/server-token": "^0.6.7",
|
||||
"@hcengineering/client": "^0.6.14",
|
||||
"@hcengineering/client-resources": "^0.6.23",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"dotenv": "~16.0.0",
|
||||
"got": "^11.8.3",
|
||||
"@hcengineering/server-backup": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/minio": "^0.6.0"
|
||||
"@hcengineering/backup-service": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -13,24 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { PlatformWorker } from './platform'
|
||||
import { MeasureMetricsContext } from '@hcengineering/core'
|
||||
import { startBackup } from '@hcengineering/backup-service'
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const platformWorker = await PlatformWorker.create()
|
||||
|
||||
const shutdown = (): void => {
|
||||
void platformWorker.close().then(() => {
|
||||
process.exit()
|
||||
})
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('uncaughtException', (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
process.on('unhandledRejection', (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
void main()
|
||||
const ctx = new MeasureMetricsContext('backup-service', {})
|
||||
startBackup(ctx)
|
||||
|
@ -1,119 +0,0 @@
|
||||
//
|
||||
// Copyright © 2022 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 { getWorkspaceId, MeasureMetricsContext } from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import { backup, createStorageBackupStorage } from '@hcengineering/server-backup'
|
||||
import { type StorageAdapter } from '@hcengineering/server-core'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import toolPlugin from '@hcengineering/server-tool'
|
||||
import got from 'got'
|
||||
import { type ObjectId } from 'mongodb'
|
||||
import config from './config'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Workspace {
|
||||
_id: ObjectId
|
||||
workspace: string
|
||||
organisation: string
|
||||
accounts: ObjectId[]
|
||||
productId: string
|
||||
}
|
||||
|
||||
async function getWorkspaces (): Promise<Workspace[]> {
|
||||
const { body }: { body: { error?: string, result?: any[] } } = await got.post(config.AccountsURL, {
|
||||
json: {
|
||||
method: 'listWorkspaces',
|
||||
params: []
|
||||
},
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
if (body.error !== undefined) {
|
||||
throw Error(body.error)
|
||||
}
|
||||
|
||||
return (body.result as Workspace[]) ?? []
|
||||
}
|
||||
|
||||
export class PlatformWorker {
|
||||
storageAdapter!: StorageAdapter
|
||||
|
||||
async close (): Promise<void> {}
|
||||
|
||||
async init (): Promise<void> {
|
||||
setMetadata(serverToken.metadata.Secret, config.Secret)
|
||||
let minioPort = 9000
|
||||
let minioEndpoint = config.MinioEndpoint
|
||||
const sp = minioEndpoint.split(':')
|
||||
if (sp.length > 1) {
|
||||
minioEndpoint = sp[0]
|
||||
minioPort = parseInt(sp[1])
|
||||
}
|
||||
|
||||
this.storageAdapter = new MinioService({
|
||||
endPoint: minioEndpoint,
|
||||
port: minioPort,
|
||||
useSSL: false,
|
||||
accessKey: config.MinioAccessKey,
|
||||
secretKey: config.MinioSecretKey
|
||||
})
|
||||
|
||||
setMetadata(toolPlugin.metadata.UserAgent, config.ServiceID)
|
||||
|
||||
await this.backup().then(() => {
|
||||
void this.schedule()
|
||||
})
|
||||
}
|
||||
|
||||
async schedule (): Promise<void> {
|
||||
console.log('schedule timeout for', config.Interval, ' seconds')
|
||||
setTimeout(() => {
|
||||
void this.backup().then(() => {
|
||||
void this.schedule()
|
||||
})
|
||||
}, config.Interval * 1000)
|
||||
}
|
||||
|
||||
async backup (): Promise<void> {
|
||||
const workspaces = await getWorkspaces()
|
||||
const ctx = new MeasureMetricsContext('backup', {})
|
||||
for (const ws of workspaces) {
|
||||
console.log('\n\nBACKUP WORKSPACE ', ws.workspace, ws.productId)
|
||||
try {
|
||||
const storage = await createStorageBackupStorage(
|
||||
ctx,
|
||||
this.storageAdapter,
|
||||
getWorkspaceId('backups', ws.productId),
|
||||
ws.workspace
|
||||
)
|
||||
await backup(config.TransactorURL, getWorkspaceId(ws.workspace, ws.productId), storage)
|
||||
} catch (err: any) {
|
||||
console.error('\n\nFAILED to BACKUP', ws, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async create (): Promise<PlatformWorker> {
|
||||
const worker = new PlatformWorker()
|
||||
await worker.init()
|
||||
return worker
|
||||
}
|
||||
|
||||
private constructor () {}
|
||||
}
|
@ -1367,6 +1367,11 @@
|
||||
"projectFolder": "server/backup",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/backup-service",
|
||||
"projectFolder": "server/backup-service",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/pod-backup",
|
||||
"projectFolder": "pods/backup",
|
||||
|
@ -26,6 +26,7 @@ import contact, {
|
||||
} from '@hcengineering/contact'
|
||||
import core, {
|
||||
AccountRole,
|
||||
BaseWorkspaceInfo,
|
||||
Client,
|
||||
concatLink,
|
||||
Data,
|
||||
@ -100,23 +101,9 @@ export interface Account {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Workspace {
|
||||
export interface Workspace extends BaseWorkspaceInfo {
|
||||
_id: ObjectId
|
||||
workspace: string // An uniq workspace name, Database names
|
||||
accounts: ObjectId[]
|
||||
productId: string
|
||||
disabled?: boolean
|
||||
version?: Data<Version>
|
||||
|
||||
workspaceUrl?: string | null // An optional url to the workspace, if not set workspace will be used
|
||||
workspaceName?: string // An displayed workspace name
|
||||
createdOn: number
|
||||
lastVisit: number
|
||||
|
||||
createdBy: string
|
||||
|
||||
creating?: boolean
|
||||
createProgress?: number // Some progress
|
||||
}
|
||||
|
||||
/**
|
||||
@ -664,7 +651,13 @@ export async function createAccount (
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function listWorkspaces (ctx: MeasureContext, db: Db, productId: string): Promise<WorkspaceInfo[]> {
|
||||
export async function listWorkspaces (
|
||||
ctx: MeasureContext,
|
||||
db: Db,
|
||||
productId: string,
|
||||
token: string
|
||||
): Promise<WorkspaceInfo[]> {
|
||||
decodeToken(token) // Just verify token is valid
|
||||
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray())
|
||||
.map((it) => ({ ...it, productId }))
|
||||
.filter((it) => it.disabled !== true)
|
||||
|
7
server/backup-service/.eslintrc.js
Normal file
7
server/backup-service/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
4
server/backup-service/.npmignore
Normal file
4
server/backup-service/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
4
server/backup-service/config/rig.json
Normal file
4
server/backup-service/config/rig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
7
server/backup-service/jest.config.js
Normal file
7
server/backup-service/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
51
server/backup-service/package.json
Normal file
51
server/backup-service/package.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@hcengineering/backup-service",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"format": "format src",
|
||||
"run": "ts-node",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/node": "~20.11.16",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/client-resources": "^0.6.23",
|
||||
"@hcengineering/client": "^0.6.14",
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"tar-stream": "^2.2.0",
|
||||
"@hcengineering/server-tool": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-backup": "^0.6.0",
|
||||
"@hcengineering/minio": "^0.6.0",
|
||||
"@hcengineering/server-token": "^0.6.7"
|
||||
}
|
||||
}
|
@ -13,13 +13,16 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
interface Config {
|
||||
import { type BackupConfig } from '@hcengineering/server-backup'
|
||||
|
||||
interface Config extends Omit<BackupConfig, 'Token'> {
|
||||
TransactorURL: string
|
||||
AccountsURL: string
|
||||
ServiceID: string
|
||||
Secret: string
|
||||
|
||||
Interval: number
|
||||
Interval: number // Timeout in seconds
|
||||
Timeout: number // Timeout in seconds
|
||||
BucketName: string
|
||||
|
||||
MinioEndpoint: string
|
||||
@ -36,7 +39,8 @@ const envMap: { [key in keyof Config]: string } = {
|
||||
Interval: 'INTERVAL',
|
||||
MinioEndpoint: 'MINIO_ENDPOINT',
|
||||
MinioAccessKey: 'MINIO_ACCESS_KEY',
|
||||
MinioSecretKey: 'MINIO_SECRET_KEY'
|
||||
MinioSecretKey: 'MINIO_SECRET_KEY',
|
||||
Timeout: 'TIMEOUT'
|
||||
}
|
||||
|
||||
const required: Array<keyof Config> = [
|
||||
@ -55,12 +59,13 @@ const config: Config = (() => {
|
||||
TransactorURL: process.env[envMap.TransactorURL],
|
||||
AccountsURL: process.env[envMap.AccountsURL],
|
||||
Secret: process.env[envMap.Secret],
|
||||
BucketName: process.env[envMap.BucketName],
|
||||
BucketName: process.env[envMap.BucketName] ?? 'backups',
|
||||
ServiceID: process.env[envMap.ServiceID] ?? 'backup-service',
|
||||
Interval: parseInt(process.env[envMap.Interval] ?? '3600'),
|
||||
MinioEndpoint: process.env[envMap.MinioEndpoint],
|
||||
MinioAccessKey: process.env[envMap.MinioAccessKey],
|
||||
MinioSecretKey: process.env[envMap.MinioSecretKey]
|
||||
MinioSecretKey: process.env[envMap.MinioSecretKey],
|
||||
Timeout: parseInt(process.env[envMap.Timeout] ?? '3600')
|
||||
}
|
||||
|
||||
const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key])
|
58
server/backup-service/src/index.ts
Normal file
58
server/backup-service/src/index.ts
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright © 2022 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 { MeasureContext, systemAccountEmail } from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import { backupService } from '@hcengineering/server-backup'
|
||||
import serverToken, { generateToken } from '@hcengineering/server-token'
|
||||
import toolPlugin from '@hcengineering/server-tool'
|
||||
import config from './config'
|
||||
|
||||
export function startBackup (ctx: MeasureContext): void {
|
||||
setMetadata(serverToken.metadata.Secret, config.Secret)
|
||||
|
||||
let minioPort = 9000
|
||||
let minioEndpoint = config.MinioEndpoint
|
||||
const sp = minioEndpoint.split(':')
|
||||
if (sp.length > 1) {
|
||||
minioEndpoint = sp[0]
|
||||
minioPort = parseInt(sp[1])
|
||||
}
|
||||
|
||||
const storageAdapter = new MinioService({
|
||||
endPoint: minioEndpoint,
|
||||
port: minioPort,
|
||||
useSSL: false,
|
||||
accessKey: config.MinioAccessKey,
|
||||
secretKey: config.MinioSecretKey
|
||||
})
|
||||
|
||||
setMetadata(toolPlugin.metadata.UserAgent, config.ServiceID)
|
||||
|
||||
// A token to access account service
|
||||
const token = generateToken(systemAccountEmail, { name: 'backup', productId: '' })
|
||||
|
||||
const shutdown = backupService(ctx, storageAdapter, { ...config, Token: token })
|
||||
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('uncaughtException', (e) => {
|
||||
void ctx.error('uncaughtException', { err: e })
|
||||
})
|
||||
process.on('unhandledRejection', (e) => {
|
||||
void ctx.error('unhandledRejection', { err: e })
|
||||
})
|
||||
}
|
10
server/backup-service/tsconfig.json
Normal file
10
server/backup-service/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
1322
server/backup/src/backup.ts
Normal file
1322
server/backup/src/backup.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
115
server/backup/src/service.ts
Normal file
115
server/backup/src/service.ts
Normal file
@ -0,0 +1,115 @@
|
||||
//
|
||||
// Copyright © 2024 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 { BaseWorkspaceInfo, getWorkspaceId, type MeasureContext } from '@hcengineering/core'
|
||||
import { type StorageAdapter } from '@hcengineering/server-core'
|
||||
import { backup } from '.'
|
||||
import { createStorageBackupStorage } from './storage'
|
||||
|
||||
async function getWorkspaces (accounts: string, token: string): Promise<BaseWorkspaceInfo[]> {
|
||||
const workspaces = await (
|
||||
await fetch(accounts, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'listWorkspaces',
|
||||
params: [token]
|
||||
})
|
||||
})
|
||||
).json()
|
||||
|
||||
return (workspaces.result as BaseWorkspaceInfo[]) ?? []
|
||||
}
|
||||
|
||||
export interface BackupConfig {
|
||||
TransactorURL: string
|
||||
AccountsURL: string
|
||||
Token: string
|
||||
|
||||
Interval: number // Timeout in seconds
|
||||
Timeout: number // Timeout in seconds
|
||||
BucketName: string
|
||||
}
|
||||
|
||||
class BackupWorker {
|
||||
constructor (
|
||||
readonly storageAdapter: StorageAdapter,
|
||||
readonly config: BackupConfig
|
||||
) {}
|
||||
|
||||
canceled = false
|
||||
interval: any
|
||||
|
||||
async close (): Promise<void> {
|
||||
this.canceled = true
|
||||
clearTimeout(this.interval)
|
||||
}
|
||||
|
||||
async schedule (ctx: MeasureContext): Promise<void> {
|
||||
console.log('schedule timeout for', this.config.Interval, ' seconds')
|
||||
this.interval = setTimeout(() => {
|
||||
void this.backup(ctx).then(() => {
|
||||
void this.schedule(ctx)
|
||||
})
|
||||
}, this.config.Interval * 1000)
|
||||
}
|
||||
|
||||
async backup (ctx: MeasureContext): Promise<void> {
|
||||
const workspaces = await getWorkspaces(this.config.AccountsURL, this.config.Token)
|
||||
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
|
||||
for (const ws of workspaces) {
|
||||
if (this.canceled) {
|
||||
return
|
||||
}
|
||||
await ctx.info('\n\nBACKUP WORKSPACE ', { workspace: ws.workspace, productId: ws.productId })
|
||||
try {
|
||||
const storage = await createStorageBackupStorage(
|
||||
ctx,
|
||||
this.storageAdapter,
|
||||
getWorkspaceId(this.config.BucketName, ws.productId),
|
||||
ws.workspace
|
||||
)
|
||||
await ctx.with('backup', { workspace: ws.workspace }, async (ctx) => {
|
||||
await backup(
|
||||
ctx,
|
||||
this.config.TransactorURL,
|
||||
getWorkspaceId(ws.workspace, ws.productId),
|
||||
storage,
|
||||
[],
|
||||
false,
|
||||
this.config.Timeout * 1000
|
||||
)
|
||||
})
|
||||
} catch (err: any) {
|
||||
await ctx.error('\n\nFAILED to BACKUP', { workspace: ws.workspace, err })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function backupService (ctx: MeasureContext, storage: StorageAdapter, config: BackupConfig): () => void {
|
||||
const backupWorker = new BackupWorker(storage, config)
|
||||
|
||||
const shutdown = (): void => {
|
||||
void backupWorker.close()
|
||||
}
|
||||
|
||||
void backupWorker.backup(ctx).then(() => {
|
||||
void backupWorker.schedule(ctx)
|
||||
})
|
||||
return shutdown
|
||||
}
|
@ -20,6 +20,8 @@ import core, {
|
||||
generateId,
|
||||
systemAccountEmail,
|
||||
toWorkspaceString,
|
||||
versionToString,
|
||||
type BaseWorkspaceInfo,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type Space,
|
||||
@ -43,10 +45,7 @@ import {
|
||||
type Workspace
|
||||
} from './types'
|
||||
|
||||
interface WorkspaceLoginInfo {
|
||||
workspaceName?: string // A company name
|
||||
workspace: string
|
||||
}
|
||||
interface WorkspaceLoginInfo extends BaseWorkspaceInfo {}
|
||||
|
||||
function timeoutPromise (time: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
@ -75,6 +74,8 @@ class TSessionManager implements SessionManager {
|
||||
maintenanceTimer: any
|
||||
timeMinutes = 0
|
||||
|
||||
modelVersion = process.env.MODEL_VERSION ?? ''
|
||||
|
||||
constructor (
|
||||
readonly ctx: MeasureContext,
|
||||
readonly sessionFactory: (token: Token, pipeline: Pipeline, broadcast: BroadcastCall) => Session,
|
||||
@ -179,15 +180,7 @@ class TSessionManager implements SessionManager {
|
||||
return this.sessionFactory(token, pipeline, this.broadcast.bind(this))
|
||||
}
|
||||
|
||||
async getWorkspaceInfo (
|
||||
accounts: string,
|
||||
token: string
|
||||
): Promise<{
|
||||
workspace: string
|
||||
workspaceUrl?: string | null
|
||||
workspaceName?: string
|
||||
creating?: boolean
|
||||
}> {
|
||||
async getWorkspaceInfo (accounts: string, token: string): Promise<BaseWorkspaceInfo> {
|
||||
const userInfo = await (
|
||||
await fetch(accounts, {
|
||||
method: 'POST',
|
||||
@ -230,10 +223,25 @@ class TSessionManager implements SessionManager {
|
||||
if (workspaceInfo === undefined && token.extra?.admin !== 'true') {
|
||||
// No access to workspace for token.
|
||||
return { error: new Error(`No access to workspace for token ${token.email} ${token.workspace.name}`) }
|
||||
} else {
|
||||
} else if (workspaceInfo === undefined) {
|
||||
workspaceInfo = this.wsFromToken(token)
|
||||
}
|
||||
|
||||
if (
|
||||
this.modelVersion !== '' &&
|
||||
workspaceInfo.version !== undefined &&
|
||||
this.modelVersion !== versionToString(workspaceInfo.version) &&
|
||||
token.extra?.model !== 'upgrade' &&
|
||||
token.extra?.mode !== 'backup'
|
||||
) {
|
||||
await ctx.info('model version mismatch', {
|
||||
version: this.modelVersion,
|
||||
workspaceVersion: versionToString(workspaceInfo.version)
|
||||
})
|
||||
// Version mismatch, return upgrading.
|
||||
return { upgrade: true }
|
||||
}
|
||||
|
||||
let workspace = this.workspaces.get(wsString)
|
||||
if (workspace?.closeTimeout !== undefined) {
|
||||
await ctx.info('Cancel workspace warm close', { wsString })
|
||||
@ -311,16 +319,18 @@ class TSessionManager implements SessionManager {
|
||||
})
|
||||
}
|
||||
|
||||
private wsFromToken (token: Token): {
|
||||
workspace: string
|
||||
workspaceUrl?: string | null
|
||||
workspaceName?: string
|
||||
creating?: boolean
|
||||
} {
|
||||
private wsFromToken (token: Token): BaseWorkspaceInfo {
|
||||
return {
|
||||
workspace: token.workspace.name,
|
||||
workspaceUrl: token.workspace.name,
|
||||
workspaceName: token.workspace.name
|
||||
workspaceName: token.workspace.name,
|
||||
createdBy: '',
|
||||
createdOn: Date.now(),
|
||||
lastVisit: Date.now(),
|
||||
productId: '',
|
||||
createProgress: 100,
|
||||
creating: false,
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { generateId, type MeasureContext } from '@hcengineering/core'
|
||||
import { UNAUTHORIZED } from '@hcengineering/platform'
|
||||
import { serialize, type Response } from '@hcengineering/rpc'
|
||||
@ -30,7 +31,6 @@ import {
|
||||
type PipelineFactory,
|
||||
type SessionManager
|
||||
} from './types'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -223,7 +223,11 @@ export function startHttpServer (
|
||||
if ('error' in session) {
|
||||
void ctx.error('error', { error: session.error?.message, stack: session.error?.stack })
|
||||
}
|
||||
cs.close()
|
||||
await cs.send(ctx, { id: -1, result: 'upgrading' }, false, false)
|
||||
// Wait 1 second before closing the connection
|
||||
setTimeout(() => {
|
||||
cs.close()
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
|
Loading…
Reference in New Issue
Block a user