Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-09-10 23:00:46 +07:00
commit 95f5731c4f
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
8 changed files with 262 additions and 193 deletions

View File

@ -0,0 +1,23 @@
{
"ACCOUNTS_URL": "https://account.huly.app/",
"UPLOAD_URL": "/files",
"FILES_URL": "/files/:workspace/:filename?file=:blobId&workspace=:workspace",
"REKONI_URL": "https://rekoni.huly.app",
"TELEGRAM_URL": "https://telegram.huly.app",
"GMAIL_URL": "https://gmail.huly.app/",
"CALENDAR_URL": "https://calendar.huly.app/",
"COLLABORATOR_URL": "wss://collaborator.huly.app",
"BRANDING_URL": "https://huly.app/huly-branding_v2.json",
"PREVIEW_CONFIG": "https://huly.app/files/:workspace?file=:blobId&size=:size",
"GITHUB_APP": "huly-for-github",
"GITHUB_CLIENTID": "Iv1.e263a087de0910e0",
"INTERCOM_APP_ID": "",
"INTERCOM_API_URL": "",
"GITHUB_URL": "https://github.huly.app",
"LIVEKIT_WS": "",
"LOVE_ENDPOINT": "https://love.huly.app/",
"SIGN_URL": "https://sign.huly.app",
"PRINT_URL": "https://print.huly.app",
"DESKTOP_UPDATES_CHANNEL": "huly",
"TELEGRAM_BOT_URL": "https://telegram-bot.huly.app"
}

View File

@ -46,7 +46,14 @@ import {
createStorageBackupStorage, createStorageBackupStorage,
restore restore
} from '@hcengineering/server-backup' } from '@hcengineering/server-backup'
import serverClientPlugin, { BlobClient, createClient, getTransactorEndpoint } from '@hcengineering/server-client' import serverClientPlugin, {
BlobClient,
createClient,
getTransactorEndpoint,
getUserWorkspaces,
login,
selectWorkspace
} from '@hcengineering/server-client'
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token' import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
import toolPlugin, { connect, FileModelLogger } from '@hcengineering/server-tool' import toolPlugin, { connect, FileModelLogger } from '@hcengineering/server-tool'
import path from 'path' import path from 'path'
@ -65,14 +72,14 @@ import core, {
systemAccountEmail, systemAccountEmail,
TxOperations, TxOperations,
versionToString, versionToString,
type WorkspaceIdWithUrl,
type Client as CoreClient, type Client as CoreClient,
type Data, type Data,
type Doc, type Doc,
type Ref, type Ref,
type Tx, type Tx,
type Version, type Version,
type WorkspaceId type WorkspaceId,
concatLink
} from '@hcengineering/core' } from '@hcengineering/core'
import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model' import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model'
import contact from '@hcengineering/model-contact' import contact from '@hcengineering/model-contact'
@ -98,7 +105,7 @@ import { fixJsonMarkup, migrateMarkup } from './markup'
import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin' import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
import { fixAccountEmails, renameAccount } from './renameAccount' import { fixAccountEmails, renameAccount } from './renameAccount'
import { moveFiles, syncFiles } from './storage' import { moveFiles, syncFiles } from './storage'
import { importNotion, importToTeamspace } from './notion' import { importNotion } from './notion'
const colorConstants = { const colorConstants = {
colorRed: '\u001b[31m', colorRed: '\u001b[31m',
@ -152,6 +159,15 @@ export function devTool (
return elasticUrl return elasticUrl
} }
function getFrontUrl (): string {
const frontUrl = process.env.FRONT_URL
if (frontUrl === undefined) {
console.error('please provide front url')
process.exit(1)
}
return frontUrl
}
const initWS = process.env.INIT_WORKSPACE const initWS = process.env.INIT_WORKSPACE
if (initWS !== undefined) { if (initWS !== undefined) {
setMetadata(toolPlugin.metadata.InitWorkspace, initWS) setMetadata(toolPlugin.metadata.InitWorkspace, initWS)
@ -212,84 +228,77 @@ export function devTool (
}) })
}) })
// import-notion /home/anna/work/notion/pages/exported --workspace workspace // import-notion-with-teamspaces /home/anna/work/notion/pages/exported --workspace workspace
program program
.command('import-notion <dir>') .command('import-notion-with-teamspaces <dir>')
.description('import extracted archive exported from Notion as "Markdown & CSV"') .description('import extracted archive exported from Notion as "Markdown & CSV"')
.requiredOption('-u, --user <user>', 'user')
.requiredOption('-pw, --password <password>', 'password')
.requiredOption('-ws, --workspace <workspace>', 'workspace where the documents should be imported to') .requiredOption('-ws, --workspace <workspace>', 'workspace where the documents should be imported to')
.action(async (dir: string, cmd) => { .action(async (dir: string, cmd) => {
if (cmd.workspace === '') return await importFromNotion(dir, cmd.user, cmd.password, cmd.workspace)
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
const ws = await getWorkspaceById(db, cmd.workspace)
if (ws === null) {
console.log('Workspace not found: ', cmd.workspace)
return
}
const wsUrl: WorkspaceIdWithUrl = {
name: ws.workspace,
workspaceName: ws.workspaceName ?? '',
workspaceUrl: ws.workspaceUrl ?? ''
}
await withStorage(mongodbUri, async (storageAdapter) => {
const token = generateToken(systemAccountEmail, { name: ws.workspace })
const endpoint = await getTransactorEndpoint(token, 'external')
const connection = (await connect(endpoint, wsUrl, undefined, {
mode: 'backup'
})) as unknown as CoreClient
const client = new TxOperations(connection, core.account.System)
await importNotion(toolCtx, client, storageAdapter, dir, wsUrl)
await connection.close()
})
})
}) })
// import-notion-to-teamspace /home/anna/work/notion/pages/exported --workspace workspace --teamspace notion // import-notion-to-teamspace /home/anna/work/notion/pages/exported --workspace workspace --teamspace notion
program program
.command('import-notion-to-teamspace <dir>') .command('import-notion-to-teamspace <dir>')
.description('import extracted archive exported from Notion as "Markdown & CSV"') .description('import extracted archive exported from Notion as "Markdown & CSV"')
.requiredOption('-u, --user <user>', 'user')
.requiredOption('-pw, --password <password>', 'password')
.requiredOption('-ws, --workspace <workspace>', 'workspace where the documents should be imported to') .requiredOption('-ws, --workspace <workspace>', 'workspace where the documents should be imported to')
.requiredOption('-ts, --teamspace <teamspace>', 'teamspace where the documents should be imported to') .requiredOption('-ts, --teamspace <teamspace>', 'new teamspace name where the documents should be imported to')
.action(async (dir: string, cmd) => { .action(async (dir: string, cmd) => {
if (cmd.workspace === '') return await importFromNotion(dir, cmd.user, cmd.password, cmd.workspace, cmd.teamspace)
if (cmd.teamspace === '') return
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
const ws = await getWorkspaceById(db, cmd.workspace)
if (ws === null) {
console.log('Workspace not found: ', cmd.workspace)
return
}
const wsUrl: WorkspaceIdWithUrl = {
name: ws.workspace,
workspaceName: ws.workspaceName ?? '',
workspaceUrl: ws.workspaceUrl ?? ''
}
await withStorage(mongodbUri, async (storageAdapter) => {
const token = generateToken(systemAccountEmail, { name: ws.workspace })
const endpoint = await getTransactorEndpoint(token, 'external')
const connection = (await connect(endpoint, wsUrl, undefined, {
mode: 'backup'
})) as unknown as CoreClient
const client = new TxOperations(connection, core.account.System)
await importToTeamspace(toolCtx, client, storageAdapter, dir, wsUrl, cmd.teamspace)
await connection.close()
})
})
}) })
async function importFromNotion (
dir: string,
user: string,
password: string,
workspace: string,
teamspace?: string
): Promise<void> {
if (workspace === '' || user === '' || password === '' || teamspace === '') {
return
}
const userToken = await login(user, password, workspace)
const allWorkspaces = await getUserWorkspaces(userToken)
const workspaces = allWorkspaces.filter((ws) => ws.workspace === workspace)
if (workspaces.length < 1) {
console.log('Workspace not found: ', workspace)
return
}
const selectedWs = await selectWorkspace(userToken, workspaces[0].workspace)
console.log(selectedWs)
function uploader (token: string) {
return (id: string, data: any) => {
return fetch(concatLink(getFrontUrl(), '/files'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: data
})
}
}
const connection = (await connect(
selectedWs.endpoint,
{
name: selectedWs.workspaceId
},
undefined,
{
mode: 'backup'
}
)) as unknown as CoreClient
const client = new TxOperations(connection, core.account.System)
await importNotion(client, uploader(selectedWs.token), dir, teamspace)
await connection.close()
}
program program
.command('reset-account <email>') .command('reset-account <email>')
.description('create user and corresponding account in master database') .description('create user and corresponding account in master database')

View File

@ -2,15 +2,13 @@ import {
generateId, generateId,
type AttachedData, type AttachedData,
type Ref, type Ref,
type WorkspaceIdWithUrl,
makeCollaborativeDoc, makeCollaborativeDoc,
type MeasureMetricsContext,
type TxOperations, type TxOperations,
type Blob type Blob,
collaborativeDocParse
} from '@hcengineering/core' } from '@hcengineering/core'
import { saveCollaborativeDoc } from '@hcengineering/collaboration' import { yDocToBuffer } from '@hcengineering/collaboration'
import document, { type Document, type Teamspace } from '@hcengineering/document' import document, { type Document, type Teamspace } from '@hcengineering/document'
import { type StorageAdapter } from '@hcengineering/server-core'
import { import {
MarkupMarkType, MarkupMarkType,
type MarkupNode, type MarkupNode,
@ -58,12 +56,13 @@ enum NOTION_MD_LINK_TYPES {
UNKNOWN UNKNOWN
} }
export type FileUploader = (id: string, data: any) => Promise<any>
export async function importNotion ( export async function importNotion (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
dir: string, dir: string,
ws: WorkspaceIdWithUrl teamspace?: string
): Promise<void> { ): Promise<void> {
const files = await getFilesForImport(dir) const files = await getFilesForImport(dir)
@ -74,13 +73,17 @@ export async function importNotion (
console.log(fileMetaMap) console.log(fileMetaMap)
console.log(documentMetaMap) console.log(documentMetaMap)
const spaceIdMap = await createTeamspaces(fileMetaMap, client) if (teamspace === undefined) {
if (spaceIdMap.size === 0) { const spaceIdMap = await createTeamspaces(fileMetaMap, client)
console.error('No teamspaces found in directory: ', dir) if (spaceIdMap.size === 0) {
return console.error('No teamspaces found in directory: ', dir)
return
}
await importFiles(client, uploadFile, fileMetaMap, documentMetaMap, spaceIdMap)
} else {
const spaceId = await createTeamspace(teamspace, client)
await importFilesToSpace(client, uploadFile, fileMetaMap, documentMetaMap, spaceId)
} }
await importFiles(ctx, client, storage, fileMetaMap, documentMetaMap, spaceIdMap, ws)
} }
async function getFilesForImport (dir: string): Promise<Dirent[]> { async function getFilesForImport (dir: string): Promise<Dirent[]> {
@ -91,28 +94,6 @@ async function getFilesForImport (dir: string): Promise<Dirent[]> {
return files return files
} }
export async function importToTeamspace (
ctx: MeasureMetricsContext,
client: TxOperations,
storage: StorageAdapter,
dir: string,
ws: WorkspaceIdWithUrl,
teamspace: string
): Promise<void> {
const files = await getFilesForImport(dir)
const fileMetaMap = new Map<string, FileMetadata>()
const documentMetaMap = new Map<string, DocumentMetadata>()
await collectMetadata(dir, files, fileMetaMap, documentMetaMap)
console.log(fileMetaMap)
console.log(documentMetaMap)
const spaceId = await createTeamspace(teamspace, client)
await importFilesToSpace(ctx, client, storage, fileMetaMap, documentMetaMap, spaceId, ws)
}
async function collectMetadata ( async function collectMetadata (
root: string, root: string,
files: Dirent[], files: Dirent[],
@ -205,31 +186,27 @@ async function createTeamspace (name: string, client: TxOperations): Promise<Ref
} }
async function importFilesToSpace ( async function importFilesToSpace (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
fileMetaMap: Map<string, FileMetadata>, fileMetaMap: Map<string, FileMetadata>,
documentMetaMap: Map<string, DocumentMetadata>, documentMetaMap: Map<string, DocumentMetadata>,
spaceId: Ref<Teamspace>, spaceId: Ref<Teamspace>
ws: WorkspaceIdWithUrl
): Promise<void> { ): Promise<void> {
for (const [notionId, fileMeta] of fileMetaMap) { for (const [notionId, fileMeta] of fileMetaMap) {
if (!fileMeta.isFolder) { if (!fileMeta.isFolder) {
const docMeta = documentMetaMap.get(notionId) const docMeta = documentMetaMap.get(notionId)
if (docMeta === undefined) throw new Error('Cannot find metadata for entry: ' + fileMeta.fileName) if (docMeta === undefined) throw new Error('Cannot find metadata for entry: ' + fileMeta.fileName)
await importFile(ctx, client, storage, fileMeta, docMeta, spaceId, documentMetaMap, ws) await importFile(client, uploadFile, fileMeta, docMeta, spaceId, documentMetaMap)
} }
} }
} }
async function importFiles ( async function importFiles (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
fileMetaMap: Map<string, FileMetadata>, fileMetaMap: Map<string, FileMetadata>,
documentMetaMap: Map<string, DocumentMetadata>, documentMetaMap: Map<string, DocumentMetadata>,
spaceIdMap: Map<string, Ref<Teamspace>>, spaceIdMap: Map<string, Ref<Teamspace>>
ws: WorkspaceIdWithUrl
): Promise<void> { ): Promise<void> {
for (const [notionId, fileMeta] of fileMetaMap) { for (const [notionId, fileMeta] of fileMetaMap) {
if (!fileMeta.isFolder) { if (!fileMeta.isFolder) {
@ -241,20 +218,18 @@ async function importFiles (
throw new Error('Teamspace not found for document: ' + docMeta.name) throw new Error('Teamspace not found for document: ' + docMeta.name)
} }
await importFile(ctx, client, storage, fileMeta, docMeta, spaceId, documentMetaMap, ws) await importFile(client, uploadFile, fileMeta, docMeta, spaceId, documentMetaMap)
} }
} }
} }
async function importFile ( async function importFile (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
fileMeta: FileMetadata, fileMeta: FileMetadata,
docMeta: DocumentMetadata, docMeta: DocumentMetadata,
spaceId: Ref<Teamspace>, spaceId: Ref<Teamspace>,
documentMetaMap: Map<string, DocumentMetadata>, documentMetaMap: Map<string, DocumentMetadata>
ws: WorkspaceIdWithUrl
): Promise<void> { ): Promise<void> {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
if (fileMeta.isFolder) throw new Error('Importing folder entry is not supported: ' + fileMeta.fileName) if (fileMeta.isFolder) throw new Error('Importing folder entry is not supported: ' + fileMeta.fileName)
@ -268,7 +243,7 @@ async function importFile (
notionParentId !== undefined && notionParentId !== '' ? documentMetaMap.get(notionParentId) : undefined notionParentId !== undefined && notionParentId !== '' ? documentMetaMap.get(notionParentId) : undefined
const processFileData = getDataProcessor(fileMeta, docMeta) const processFileData = getDataProcessor(fileMeta, docMeta)
processFileData(ctx, client, storage, ws, data, docMeta, spaceId, parentMeta, documentMetaMap) processFileData(client, uploadFile, data, docMeta, spaceId, parentMeta, documentMetaMap)
.then(() => { .then(() => {
console.log('IMPORT SUCCEED:', docMeta.name) console.log('IMPORT SUCCEED:', docMeta.name)
console.log('------------------------------------------------------------------') console.log('------------------------------------------------------------------')
@ -292,10 +267,8 @@ async function importFile (
} }
type DataProcessor = ( type DataProcessor = (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
ws: WorkspaceIdWithUrl,
data: Buffer, data: Buffer,
docMeta: DocumentMetadata, docMeta: DocumentMetadata,
space: Ref<Teamspace>, space: Ref<Teamspace>,
@ -328,10 +301,8 @@ function getDataProcessor (fileMeta: FileMetadata, docMeta: DocumentMetadata): D
} }
async function createDBPageWithAttachments ( async function createDBPageWithAttachments (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
ws: WorkspaceIdWithUrl,
data: Buffer, data: Buffer,
docMeta: DocumentMetadata, docMeta: DocumentMetadata,
space: Ref<Teamspace>, space: Ref<Teamspace>,
@ -380,14 +351,12 @@ async function createDBPageWithAttachments (
size: docMeta.size size: docMeta.size
} }
await importAttachment(ctx, client, storage, ws, data, attachment, space, dbPage) await importAttachment(client, uploadFile, data, attachment, space, dbPage)
} }
async function importDBAttachment ( async function importDBAttachment (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
ws: WorkspaceIdWithUrl,
data: Buffer, data: Buffer,
docMeta: DocumentMetadata, docMeta: DocumentMetadata,
space: Ref<Teamspace>, space: Ref<Teamspace>,
@ -413,14 +382,12 @@ async function importDBAttachment (
mimeType: docMeta.mimeType, mimeType: docMeta.mimeType,
size: docMeta.size size: docMeta.size
} }
await importAttachment(ctx, client, storage, ws, data, attachment, space, dbPage) await importAttachment(client, uploadFile, data, attachment, space, dbPage)
} }
async function importAttachment ( async function importAttachment (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
ws: WorkspaceIdWithUrl,
data: Buffer, data: Buffer,
docMeta: DocumentMetadata, docMeta: DocumentMetadata,
space: Ref<Teamspace>, space: Ref<Teamspace>,
@ -433,7 +400,17 @@ async function importAttachment (
const size = docMeta.size ?? 0 const size = docMeta.size ?? 0
const type = docMeta.mimeType ?? DEFAULT_ATTACHMENT_MIME_TYPE const type = docMeta.mimeType ?? DEFAULT_ATTACHMENT_MIME_TYPE
await storage.put(ctx, ws, docMeta.id, data, type, size)
const form = new FormData()
const file = new File([new Blob([data])], docMeta.name)
form.append('file', file, docMeta.id)
form.append('type', type)
form.append('size', size.toString())
form.append('name', docMeta.name)
form.append('id', docMeta.id)
form.append('data', new Blob([data])) // ?
await uploadFile(docMeta.id, form)
const attachedData: AttachedData<Attachment> = { const attachedData: AttachedData<Attachment> = {
file: docMeta.id as Ref<Blob>, file: docMeta.id as Ref<Blob>,
@ -455,10 +432,8 @@ async function importAttachment (
} }
async function importPageDocument ( async function importPageDocument (
ctx: MeasureMetricsContext,
client: TxOperations, client: TxOperations,
storage: StorageAdapter, uploadFile: FileUploader,
ws: WorkspaceIdWithUrl,
data: Buffer, data: Buffer,
docMeta: DocumentMetadata, docMeta: DocumentMetadata,
space: Ref<Teamspace>, space: Ref<Teamspace>,
@ -474,7 +449,19 @@ async function importPageDocument (
const id = docMeta.id as Ref<Document> const id = docMeta.id as Ref<Document>
const collabId = makeCollaborativeDoc(id, 'content') const collabId = makeCollaborativeDoc(id, 'content')
const yDoc = jsonToYDocNoSchema(json, 'content') const yDoc = jsonToYDocNoSchema(json, 'content')
await saveCollaborativeDoc(storage, ws, collabId, yDoc, ctx) const { documentId } = collaborativeDocParse(collabId)
const buffer = yDocToBuffer(yDoc)
const form = new FormData()
const file = new File([new Blob([buffer])], docMeta.name)
form.append('file', file, documentId)
form.append('type', 'application/ydoc')
form.append('size', buffer.length.toString())
form.append('name', docMeta.name)
form.append('id', docMeta.id)
form.append('data', new Blob([buffer])) // ?
await uploadFile(docMeta.id, form)
const parentId = parentMeta?.id ?? document.ids.NoParent const parentId = parentMeta?.id ?? document.ids.NoParent

View File

@ -16,51 +16,46 @@
<script lang="ts"> <script lang="ts">
import { LoginInfo, Workspace } from '@hcengineering/login' import { LoginInfo, Workspace } from '@hcengineering/login'
import { OK, Severity, Status } from '@hcengineering/platform' import { OK, Severity, Status } from '@hcengineering/platform'
import presentation, { NavLink, isAdminUser } from '@hcengineering/presentation' import presentation, { NavLink, isAdminUser, reduceCalls } from '@hcengineering/presentation'
import { import {
Button, Button,
Label, Label,
Scroller, Scroller,
SearchEdit, SearchEdit,
deviceOptionsStore as deviceInfo, deviceOptionsStore as deviceInfo,
setMetadataLocalStorage setMetadataLocalStorage,
ticker
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import login from '../plugin' import login from '../plugin'
import { getAccount, getHref, getWorkspaces, goTo, navigateToWorkspace, selectWorkspace } from '../utils' import { getAccount, getHref, getWorkspaces, goTo, navigateToWorkspace, selectWorkspace } from '../utils'
import StatusControl from './StatusControl.svelte' import StatusControl from './StatusControl.svelte'
const CHECK_INTERVAL = 1000
export let navigateUrl: string | undefined = undefined export let navigateUrl: string | undefined = undefined
let workspaces: Workspace[] = [] let workspaces: Workspace[] = []
let flagToUpdateWorkspaces = false
let status = OK let status = OK
let account: LoginInfo | undefined = undefined let account: LoginInfo | undefined = undefined
let flagToUpdateWorkspaces = false
async function loadAccount (): Promise<void> { async function loadAccount (): Promise<void> {
account = await getAccount() account = await getAccount()
} }
async function updateWorkspaces (): Promise<void> { const updateWorkspaces = reduceCalls(async function updateWorkspaces (time: number): Promise<void> {
try { try {
workspaces = await getWorkspaces() workspaces = await getWorkspaces()
} catch (e) { } catch (e) {
// we should be able to continue from this state // we should be able to continue from this state
} }
if (flagToUpdateWorkspaces) { })
setTimeout(updateWorkspaces, CHECK_INTERVAL)
} $: if (flagToUpdateWorkspaces) updateWorkspaces($ticker)
}
onMount(() => { onMount(() => {
void loadAccount() void loadAccount()
return () => {
flagToUpdateWorkspaces = false
}
}) })
async function select (workspace: string): Promise<void> { async function select (workspace: string): Promise<void> {
@ -81,8 +76,8 @@
} }
workspaces = res workspaces = res
await updateWorkspaces(0)
flagToUpdateWorkspaces = true flagToUpdateWorkspaces = true
await updateWorkspaces()
} catch (err: any) { } catch (err: any) {
setMetadataLocalStorage(login.metadata.LastToken, null) setMetadataLocalStorage(login.metadata.LastToken, null)
setMetadataLocalStorage(presentation.metadata.Token, null) setMetadataLocalStorage(presentation.metadata.Token, null)

View File

@ -862,7 +862,7 @@ export async function backup (
addedDocuments += descrJson.length addedDocuments += descrJson.length
addedDocuments += blob.size addedDocuments += blob.size
printDownloaded(blob._id, descrJson.length) printDownloaded('', descrJson.length)
try { try {
const buffers: Buffer[] = [] const buffers: Buffer[] = []
await blobClient.writeTo(ctx, blob._id, blob.size, { await blobClient.writeTo(ctx, blob._id, blob.size, {
@ -907,7 +907,7 @@ export async function backup (
}) })
} }
printDownloaded(blob._id, blob.size) printDownloaded('', blob.size)
} catch (err: any) { } catch (err: any) {
if (err.message?.startsWith('No file for') === true) { if (err.message?.startsWith('No file for') === true) {
ctx.error('failed to download blob', { message: err.message }) ctx.error('failed to download blob', { message: err.message })
@ -925,7 +925,7 @@ export async function backup (
if (err != null) throw err if (err != null) throw err
}) })
processChanges(d) processChanges(d)
printDownloaded(d._id, data.length) printDownloaded('', data.length)
} }
} }
} }
@ -1141,6 +1141,9 @@ export async function restore (
let uploaded = 0 let uploaded = 0
const printUploaded = (msg: string, size: number): void => { const printUploaded = (msg: string, size: number): void => {
if (size == null) {
return
}
uploaded += size uploaded += size
const newDownloadedMb = Math.round(uploaded / (1024 * 1024)) const newDownloadedMb = Math.round(uploaded / (1024 * 1024))
const newId = Math.round(newDownloadedMb / 10) const newId = Math.round(newDownloadedMb / 10)
@ -1275,6 +1278,10 @@ export async function restore (
blobs.delete(name) blobs.delete(name)
const doc = d?.doc as Blob const doc = d?.doc as Blob
;(doc as any)['%hash%'] = changeset.get(doc._id) ;(doc as any)['%hash%'] = changeset.get(doc._id)
let sz = doc.size
if (Number.isNaN(sz) || sz !== bf.length) {
sz = bf.length
}
void blobClient.upload(ctx, doc._id, doc.size, doc.contentType, bf).then(() => { void blobClient.upload(ctx, doc._id, doc.size, doc.contentType, bf).then(() => {
void sendChunk(doc, bf.length).finally(() => { void sendChunk(doc, bf.length).finally(() => {
requiredDocs.delete(doc._id) requiredDocs.delete(doc._id)

View File

@ -18,11 +18,19 @@ import { getMetadata, PlatformError, unknownError } from '@hcengineering/platfor
import plugin from './plugin' import plugin from './plugin'
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string
workspaceId: string
}
export interface LoginInfo {
token: string
endpoint: string
confirmed: boolean
email: string
}
export async function listAccountWorkspaces (token: string): Promise<BaseWorkspaceInfo[]> { export async function listAccountWorkspaces (token: string): Promise<BaseWorkspaceInfo[]> {
const accountsUrl = getMetadata(plugin.metadata.Endpoint) const accountsUrl = getAccoutsUrlOrFail()
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
const workspaces = await ( const workspaces = await (
await fetch(accountsUrl, { await fetch(accountsUrl, {
method: 'POST', method: 'POST',
@ -44,11 +52,7 @@ export async function getTransactorEndpoint (
kind: 'internal' | 'external' = 'internal', kind: 'internal' | 'external' = 'internal',
timeout: number = -1 timeout: number = -1
): Promise<string> { ): Promise<string> {
const accountsUrl = getMetadata(plugin.metadata.Endpoint) const accountsUrl = getAccoutsUrlOrFail()
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
const st = Date.now() const st = Date.now()
while (true) { while (true) {
try { try {
@ -86,11 +90,7 @@ export async function getPendingWorkspace (
version: Data<Version>, version: Data<Version>,
operation: 'create' | 'upgrade' | 'all' operation: 'create' | 'upgrade' | 'all'
): Promise<BaseWorkspaceInfo | undefined> { ): Promise<BaseWorkspaceInfo | undefined> {
const accountsUrl = getMetadata(plugin.metadata.Endpoint) const accountsUrl = getAccoutsUrlOrFail()
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
const workspaces = await ( const workspaces = await (
await fetch(accountsUrl, { await fetch(accountsUrl, {
method: 'POST', method: 'POST',
@ -115,10 +115,7 @@ export async function updateWorkspaceInfo (
progress: number, progress: number,
message?: string message?: string
): Promise<void> { ): Promise<void> {
const accountsUrl = getMetadata(plugin.metadata.Endpoint) const accountsUrl = getAccoutsUrlOrFail()
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
await ( await (
await fetch(accountsUrl, { await fetch(accountsUrl, {
method: 'POST', method: 'POST',
@ -139,11 +136,7 @@ export async function workerHandshake (
version: Data<Version>, version: Data<Version>,
operation: 'create' | 'upgrade' | 'all' operation: 'create' | 'upgrade' | 'all'
): Promise<void> { ): Promise<void> {
const accountsUrl = getMetadata(plugin.metadata.Endpoint) const accountsUrl = getAccoutsUrlOrFail()
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
await fetch(accountsUrl, { await fetch(accountsUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -157,10 +150,7 @@ export async function workerHandshake (
} }
export async function getWorkspaceInfo (token: string): Promise<BaseWorkspaceInfo | undefined> { export async function getWorkspaceInfo (token: string): Promise<BaseWorkspaceInfo | undefined> {
const accountsUrl = getMetadata(plugin.metadata.Endpoint) const accountsUrl = getAccoutsUrlOrFail()
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
const workspaceInfo = await ( const workspaceInfo = await (
await fetch(accountsUrl, { await fetch(accountsUrl, {
method: 'POST', method: 'POST',
@ -177,3 +167,63 @@ export async function getWorkspaceInfo (token: string): Promise<BaseWorkspaceInf
return workspaceInfo.result as BaseWorkspaceInfo | undefined return workspaceInfo.result as BaseWorkspaceInfo | undefined
} }
export async function login (user: string, password: string, workspace: string): Promise<string> {
const accountsUrl = getAccoutsUrlOrFail()
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'login',
params: [user, password, workspace]
})
})
const result = await response.json()
const { token } = result.result
return token
}
export async function getUserWorkspaces (token: string): Promise<BaseWorkspaceInfo[]> {
const accountsUrl = getAccoutsUrlOrFail()
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'getUserWorkspaces',
params: []
})
})
const result = await response.json()
return (result.result as BaseWorkspaceInfo[]) ?? []
}
export async function selectWorkspace (token: string, workspace: string): Promise<WorkspaceLoginInfo> {
const accountsUrl = getAccoutsUrlOrFail()
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token
},
body: JSON.stringify({
method: 'selectWorkspace',
params: [workspace, 'external']
})
})
const result = await response.json()
return result.result as WorkspaceLoginInfo
}
function getAccoutsUrlOrFail (): string {
const accountsUrl = getMetadata(plugin.metadata.Endpoint)
if (accountsUrl == null) {
throw new PlatformError(unknownError('No account endpoint specified'))
}
return accountsUrl
}

View File

@ -14,7 +14,6 @@
// //
import core, { import core, {
DOMAIN_TRANSIENT,
TxProcessor, TxProcessor,
type Doc, type Doc,
type Domain, type Domain,
@ -49,13 +48,7 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
for (const tx of txes) { for (const tx of txes) {
if (TxProcessor.isExtendsCUD(tx._class)) { if (TxProcessor.isExtendsCUD(tx._class)) {
const objectClass = (tx as TxCUD<Doc>).objectClass txToStore.push(tx)
if (
objectClass !== core.class.BenchmarkDoc &&
this.context.hierarchy.findDomain(objectClass) !== DOMAIN_TRANSIENT
) {
txToStore.push(tx)
}
} }
} }
let result: TxMiddlewareResult = {} let result: TxMiddlewareResult = {}

View File

@ -158,7 +158,7 @@ export function startHttpServer (
void sessions.profiling.stop().then((profile) => { void sessions.profiling.stop().then((profile) => {
ctx.warn( ctx.warn(
'---------------------------------------------PROFILING SESSION STOPPED---------------------------------------------', '---------------------------------------------PROFILING SESSION STOPPED---------------------------------------------',
{ profile } {}
) )
res.writeHead(200, { 'Content-Type': 'application/json' }) res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(profile ?? '{ error: "no profiling" }') res.end(profile ?? '{ error: "no profiling" }')
@ -205,7 +205,12 @@ export function startHttpServer (
const name = req.query.name as string const name = req.query.name as string
const contentType = req.query.contentType as string const contentType = req.query.contentType as string
const size = parseInt((req.query.size as string) ?? '-1') const size = parseInt((req.query.size as string) ?? '-1')
if (Number.isNaN(size)) {
ctx.error('/api/v1/blob put error', { message: 'invalid NaN file size' })
res.writeHead(404, {})
res.end()
return
}
ctx ctx
.with( .with(
'storage upload', 'storage upload',