diff --git a/dev/tool/package.json b/dev/tool/package.json index 474d390fb2..47c913a296 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -75,6 +75,7 @@ "@hcengineering/model-all": "^0.6.0", "@hcengineering/model-attachment": "^0.6.0", "@hcengineering/model-contact": "^0.6.1", + "@hcengineering/model-document": "^0.6.0", "@hcengineering/model-recruit": "^0.6.0", "@hcengineering/model-telegram": "^0.6.0", "@hcengineering/model-tracker": "^0.6.0", diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index faa9a62a3d..cb53d67e7b 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -122,6 +122,7 @@ import { copyToDatalake, moveFiles, showLostFiles } from './storage' import { getModelVersion } from '@hcengineering/model-all' import { type DatalakeConfig, DatalakeService, createDatalakeClient } from '@hcengineering/datalake' import { S3Service, type S3Config } from '@hcengineering/s3' +import { restoreWikiContentMongo } from './markup' const colorConstants = { colorRed: '\u001b[31m', @@ -1240,6 +1241,58 @@ export function devTool ( } }) + program + .command('restore-wiki-content-mongo') + .description('restore wiki document contents') + .option('-w, --workspace ', 'Selected workspace only', '') + .option('-d, --dryrun', 'Dry run', false) + .action(async (cmd: { workspace: string, dryrun: boolean }) => { + const params = { + dryRun: cmd.dryrun + } + + const { dbUrl, version } = prepareTools() + + let workspaces: Workspace[] = [] + const accountUrl = getAccountDBUrl() + await withDatabase(accountUrl, async (db) => { + workspaces = await listWorkspacesPure(db) + workspaces = workspaces + .filter((p) => p.mode !== 'archived') + .filter((p) => cmd.workspace === '' || p.workspace === cmd.workspace) + .sort((a, b) => b.lastVisit - a.lastVisit) + }) + + await withStorage(async (storageAdapter) => { + await withDatabase(dbUrl, async (db) => { + const mongodbUri = getMongoDBUrl() + const client = getMongoClient(mongodbUri) + const _client = await client.getClient() + + try { + const count = workspaces.length + let index = 0 + for (const workspace of workspaces) { + index++ + + toolCtx.info('processing workspace', { workspace: workspace.workspace, index, count }) + if (workspace.version === undefined || !deepEqual(workspace.version, version)) { + console.log(`upgrade to ${versionToString(version)} is required`) + continue + } + + const workspaceId = getWorkspaceId(workspace.workspace) + const wsDb = getWorkspaceMongoDB(_client, { name: workspace.workspace }) + + await restoreWikiContentMongo(toolCtx, wsDb, workspaceId, storageAdapter, params) + } + } finally { + client.close() + } + }) + }) + }) + program .command('confirm-email ') .description('confirm user email') diff --git a/dev/tool/src/markup.ts b/dev/tool/src/markup.ts new file mode 100644 index 0000000000..4712f2ad31 --- /dev/null +++ b/dev/tool/src/markup.ts @@ -0,0 +1,90 @@ +// +// 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 { loadCollabYdoc, saveCollabYdoc, yDocCopyXmlField } from '@hcengineering/collaboration' +import { type MeasureContext, type WorkspaceId, makeCollabYdocId } from '@hcengineering/core' +import document, { type Document } from '@hcengineering/document' +import { DOMAIN_DOCUMENT } from '@hcengineering/model-document' +import { type StorageAdapter } from '@hcengineering/server-core' +import { type Db } from 'mongodb' + +export interface RestoreWikiContentParams { + dryRun: boolean +} + +export async function restoreWikiContentMongo ( + ctx: MeasureContext, + db: Db, + workspaceId: WorkspaceId, + storageAdapter: StorageAdapter, + params: RestoreWikiContentParams +): Promise { + const iterator = db.collection(DOMAIN_DOCUMENT).find({ _class: document.class.Document }) + + let processedCnt = 0 + let restoredCnt = 0 + + function printStats (): void { + console.log('...processed', processedCnt, 'restored', restoredCnt) + } + + try { + while (true) { + const doc = await iterator.next() + if (doc === null) return + + processedCnt++ + if (processedCnt % 100 === 0) { + printStats() + } + + const correctCollabId = { objectClass: doc._class, objectId: doc._id, objectAttr: 'content' } + const wrongCollabId = { objectClass: doc._class, objectId: doc._id, objectAttr: 'description' } + + const stat = storageAdapter.stat(ctx, workspaceId, makeCollabYdocId(wrongCollabId)) + if (stat === undefined) continue + + const ydoc1 = await loadCollabYdoc(ctx, storageAdapter, workspaceId, correctCollabId) + const ydoc2 = await loadCollabYdoc(ctx, storageAdapter, workspaceId, wrongCollabId) + + if (ydoc1 !== undefined && ydoc1.share.has('content')) { + // There already is content, we should skip the document + continue + } + + if (ydoc2 === undefined) { + // There are no content to restore + continue + } + + try { + console.log('restoring content for', doc._id) + if (!params.dryRun) { + if (ydoc2.share.has('description') && !ydoc2.share.has('content')) { + yDocCopyXmlField(ydoc2, 'description', 'content') + } + + await saveCollabYdoc(ctx, storageAdapter, workspaceId, correctCollabId, ydoc2) + } + restoredCnt++ + } catch (err: any) { + console.error('failed to restore content for', doc._id, err) + } + } + } finally { + printStats() + await iterator.close() + } +}