List unused workspaces (#6073)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-07-16 12:22:16 +07:00 committed by GitHub
parent 6d2f17b2cb
commit f11bc4c89d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 99 deletions

View File

@ -1,81 +0,0 @@
import { dropWorkspaceFull, setWorkspaceDisabled, type Workspace } from '@hcengineering/account'
import core, { AccountRole, MeasureMetricsContext, SortingOrder, type MeasureContext } from '@hcengineering/core'
import contact from '@hcengineering/model-contact'
import { type StorageAdapter } from '@hcengineering/server-core'
import { connect } from '@hcengineering/server-tool'
import { type Db, type MongoClient } from 'mongodb'
export async function checkOrphanWorkspaces (
ctx: MeasureContext,
workspaces: Workspace[],
transactorUrl: string,
productId: string,
cmd: { remove: boolean, disable: boolean },
db: Db,
client: MongoClient,
storageAdapter: StorageAdapter,
excludes: string[]
): Promise<void> {
for (const ws of workspaces) {
if (excludes.includes(ws.workspace) || (ws.workspaceUrl != null && excludes.includes(ws.workspaceUrl))) {
continue
}
if ((ws.accounts ?? []).length === 0) {
// Potential orhpan workspace
// Let's connect and check activity.
const connection = await connect(transactorUrl, { name: ws.workspace, productId }, undefined, { admin: 'true' })
const accounts = await connection.findAll(contact.class.PersonAccount, {})
const employees = await connection.findAll(contact.mixin.Employee, {})
let activeOwners = 0
for (const person of employees) {
const account = accounts.find((it) => it.person === person._id)
if (account !== undefined) {
if (account.role === AccountRole.Owner && person.active) {
activeOwners++
}
// console.log('-----------', person.name, person.active, account.email, account.role)
}
}
// Find last transaction index:
const wspace = { name: ws.workspace, productId }
const hasBucket = await storageAdapter.exists(ctx, wspace)
const [lastTx] = await connection.findAll(
core.class.Tx,
{
objectSpace: { $ne: core.space.Model },
createdBy: { $nin: [core.account.System, core.account.ConfigUser] },
modifiedBy: { $ne: core.account.System }
},
{ limit: 1, sort: { modifiedOn: SortingOrder.Descending } }
)
await connection.close()
const lastTxHours = Math.floor((Date.now() - (lastTx?.modifiedOn ?? 0)) / 1000 / 60 / 60)
if (((activeOwners === 0 || lastTx == null) && lastTxHours > 1000) || !hasBucket) {
const createdOn = (ws.createdOn ?? 0) !== 0 ? new Date(ws.createdOn).toDateString() : ''
console.log(
'Found orhpan workspace',
`'${ws.workspaceName}' id: '${ws.workspace}' url:${ws.workspaceUrl} by: ${ws.createdBy ?? ''} on: '${createdOn}'`,
lastTxHours + ' hours without modifications',
hasBucket
)
if (cmd.disable) {
await setWorkspaceDisabled(db, ws._id, true)
}
if (cmd.remove) {
await dropWorkspaceFull(
new MeasureMetricsContext('tool', {}),
db,
client,
productId,
null,
ws.workspace,
storageAdapter
)
}
}
}
}
}

View File

@ -33,7 +33,8 @@ import {
setAccountAdmin,
setRole,
UpgradeWorker,
upgradeWorkspace
upgradeWorkspace,
type Workspace
} from '@hcengineering/account'
import { setMetadata } from '@hcengineering/platform'
import {
@ -86,7 +87,6 @@ import {
restoreHrTaskTypesFromUpdates,
restoreRecruitingTaskTypes
} from './clean'
import { checkOrphanWorkspaces } from './cleanOrphan'
import { changeConfiguration } from './configuration'
import { fixJsonMarkup } from './markup'
import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
@ -394,31 +394,60 @@ export function devTool (
)
program
.command('remove-unused-workspaces')
.command('list-unused-workspaces')
.description(
'remove unused workspaces, please pass --remove to really delete them. Without it will only mark them disabled'
)
.option('-r|--remove [remove]', 'Force remove', false)
.option('-d|--disable [disable]', 'Force disable', false)
.option('-e|--exclude [exclude]', 'A comma separated list of workspaces to exclude', '')
.action(async (cmd: { remove: boolean, disable: boolean, exclude: string }) => {
.option('-t|--timeout [timeout]', 'Timeout in days', '7')
.action(async (cmd: { remove: boolean, disable: boolean, exclude: string, timeout: string }) => {
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db, client) => {
const workspaces = await listWorkspacesPure(db, productId)
const workspaces = new Map((await listWorkspacesPure(db, productId)).map((p) => [p._id.toString(), p]))
const accounts = await listAccounts(db)
const _timeout = parseInt(cmd.timeout) ?? 7
await withStorage(mongodbUri, async (adapter) => {
// We need to update workspaces with missing workspaceUrl
await checkOrphanWorkspaces(
toolCtx,
workspaces,
transactorUrl,
productId,
cmd,
db,
client,
adapter,
cmd.exclude.split(',')
)
for (const a of accounts) {
const authored = a.workspaces
.map((it) => workspaces.get(it.toString()))
.filter((it) => it !== undefined && it.createdBy?.trim() === a.email?.trim()) as Workspace[]
authored.sort((a, b) => b.lastVisit - a.lastVisit)
if (authored.length > 0) {
const lastLoginDays = Math.floor((Date.now() - a.lastVisit) / 1000 / 3600 / 24)
toolCtx.info(a.email, {
workspaces: a.workspaces.length,
firstName: a.first,
lastName: a.last,
lastLoginDays
})
for (const ws of authored) {
const lastVisitDays = Math.floor((Date.now() - ws.lastVisit) / 1000 / 3600 / 24)
if (lastVisitDays > _timeout) {
toolCtx.warn(' --- unused', {
url: ws.workspaceUrl,
id: ws.workspace,
lastVisitDays
})
if (cmd.remove) {
await dropWorkspaceFull(toolCtx, db, client, productId, null, ws.workspace, adapter)
}
} else {
toolCtx.warn(' +++ used', {
url: ws.workspaceUrl,
id: ws.workspace,
createdBy: ws.createdBy,
lastVisitDays
})
}
}
}
}
})
})
})