Files
jellyseerr/server/lib/settings/migrator.ts
Gauthier 4e9c94c80f feat: Overseerr to Jellyseerr migration (#2019)
* feat: add Overseerr migration

* refactor: rename to Seerr

* refactor: more rename to Seerr

* feat: update the value of the MediaStatus.DELETED enum

* fix: add more details in migration logs

* fix: replace .update by .save for TypeORM hooks

* fix: add fake migration to skip the duplicated UpdateWebPush migration

* fix: rewrite the AddUserAvatarCacheFields migration for Overseerr merge

* fix: replace jellyseerr migrations with a dedicated one for overseerr

* fix: update overseerr migration

* fix: update overseerr migration

* fix: remove irrelevant changes

* fix: typos

* docs: update jsdoc comment

* docs: update seerr description

* docs: fix the contributing.md link

* fix: remove unwanterd change on postgres dev datasource

* docs: add latest tag to docker image

* fix: migrate old deleted status for 4k media

* fix: update Seerr version check
2025-10-30 19:57:50 +01:00

102 lines
3.2 KiB
TypeScript

import type { AllSettings } from '@server/lib/settings';
import logger from '@server/logger';
import fs from 'fs/promises';
import path from 'path';
const migrationsDir = path.join(__dirname, 'migrations');
export const runMigrations = async (
settings: AllSettings,
SETTINGS_PATH: string
): Promise<AllSettings> => {
let migrated = settings;
try {
// we read old backup and create a backup of currents settings
const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json');
let oldBackup: string | null = null;
try {
oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8');
} catch {
/* empty */
}
await fs.writeFile(BACKUP_PATH, JSON.stringify(settings, undefined, ' '));
const migrations = (await fs.readdir(migrationsDir)).filter(
(file) => file.endsWith('.js') || file.endsWith('.ts')
);
const settingsBefore = JSON.stringify(migrated);
for (const migration of migrations) {
try {
logger.debug(`Checking migration '${migration}'...`, {
label: 'Settings Migrator',
});
const { default: migrationFn } = await import(
path.join(migrationsDir, migration)
);
const newSettings = await migrationFn(structuredClone(migrated));
if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) {
logger.debug(`Migration '${migration}' has been applied.`, {
label: 'Settings Migrator',
});
}
migrated = newSettings;
} catch (e) {
// we stop Seerr if the migration failed
logger.error(
`Error while running migration '${migration}': ${e.message}\n${e.stack}`,
{
label: 'Settings Migrator',
}
);
logger.error(
'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.',
{
label: 'Settings Migrator',
}
);
process.exit();
}
}
const settingsAfter = JSON.stringify(migrated);
if (settingsBefore !== settingsAfter) {
// a migration occured
// we check that the new config will be saved
await fs.writeFile(
SETTINGS_PATH,
JSON.stringify(migrated, undefined, ' ')
);
const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
if (JSON.stringify(fileSaved) !== settingsAfter) {
// something went wrong while saving file
throw new Error('Unable to save settings after migration.');
}
} else if (oldBackup) {
// no migration occured
// we save the old backup (to avoid settings.json and settings.old.json being the same)
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
}
} catch (e) {
// we stop Seerr if the migration failed
logger.error(
`Something went wrong while running settings migrations: ${e.message}`,
{
label: 'Settings Migrator',
}
);
logger.error(
'A common cause for this issue is a permission error of your configuration folder.',
{
label: 'Settings Migrator',
}
);
process.exit();
}
return migrated;
};