thelounge/server/command-line/storage.ts
Reto Brunner 21b1152f53 cleaner: expose cli task to do cleaning + vacuum
Make the cleaner available to users by exposing it as a subcommand
to thelounge storage.

This is recommended to be run whenever the storage policy significantly
changes in a way that makes many messages eligible for deletion.
The cleaner would cope, but it'll be inefficient and can take many hours.
Due to how storage works in sqlite, the space would not actually be
given back to the OS, just marked for future writes.
Hence this also runs a vacuum to compact the DB as much as it can.
2023-12-24 16:55:45 +01:00

124 lines
3.1 KiB
TypeScript

import log from "../log";
import {Command} from "commander";
import ClientManager from "../clientManager";
import Utils from "./utils";
import SqliteMessageStorage from "../plugins/messageStorage/sqlite";
import {StorageCleaner} from "../storageCleaner";
const program = new Command("storage").description(
"various utilities related to the message storage"
);
program
.command("migrate")
.argument("[username]", "migrate a specific user only, all if not provided")
.description("Migrate message storage where needed")
.on("--help", Utils.extraHelp)
.action(function (user) {
runMigrations(user).catch((err) => {
log.error(err.toString());
process.exit(1);
});
});
program
.command("clean")
.argument("[user]", "clean messages for a specific user only, all if not provided")
.description("Delete messages from the DB based on the storage policy")
.on("--help", Utils.extraHelp)
.action(function (user) {
runCleaning(user).catch((err) => {
log.error(err.toString());
process.exit(1);
});
});
async function runMigrations(user?: string) {
const manager = new ClientManager();
const users = manager.getUsers();
if (user) {
if (!users.includes(user)) {
throw new Error(`invalid user ${user}`);
}
return migrateUser(manager, user);
}
for (const name of users) {
await migrateUser(manager, name);
// if any migration fails we blow up,
// chances are the rest won't complete either
}
}
// runs sqlite migrations for a user, which must exist
async function migrateUser(manager: ClientManager, user: string) {
log.info("handling user", user);
if (!isUserLogEnabled(manager, user)) {
log.info("logging disabled for user", user, ". Skipping");
return;
}
const sqlite = new SqliteMessageStorage(user);
await sqlite.enable(); // enable runs migrations
await sqlite.close();
log.info("user", user, "migrated successfully");
}
function isUserLogEnabled(manager: ClientManager, user: string): boolean {
const conf = manager.readUserConfig(user);
if (!conf) {
log.error("Could not open user configuration of", user);
return false;
}
return conf.log;
}
async function runCleaning(user: string) {
const manager = new ClientManager();
const users = manager.getUsers();
if (user) {
if (!users.includes(user)) {
throw new Error(`invalid user ${user}`);
}
return cleanUser(manager, user);
}
for (const name of users) {
await cleanUser(manager, name);
// if any migration fails we blow up,
// chances are the rest won't complete either
}
}
async function cleanUser(manager: ClientManager, user: string) {
log.info("handling user", user);
if (!isUserLogEnabled(manager, user)) {
log.info("logging disabled for user", user, ". Skipping");
return;
}
const sqlite = new SqliteMessageStorage(user);
await sqlite.enable();
const cleaner = new StorageCleaner(sqlite);
const num_deleted = await cleaner.runDeletesNoLimit();
log.info(`deleted ${num_deleted} messages`);
log.info("running a vacuum now, this might take a while");
if (num_deleted > 0) {
await sqlite.vacuum();
}
await sqlite.close();
log.info(`cleaning messages for ${user} has been successful`);
}
export default program;