2021-11-03 14:26:26 +02:00
import LockHandler , { LockClientType , LockType } from './LockHandler' ;
2020-08-02 13:28:50 +02:00
import { Dirnames } from './utils/types' ;
2021-01-22 19:41:11 +02:00
import BaseService from '../BaseService' ;
2021-08-12 17:54:10 +02:00
import migration1 from './migrations/1' ;
import migration2 from './migrations/2' ;
import migration3 from './migrations/3' ;
import Setting from '../../models/Setting' ;
import JoplinError from '../../JoplinError' ;
import { FileApi } from '../../file-api' ;
import JoplinDatabase from '../../JoplinDatabase' ;
import { fetchSyncInfo , SyncInfo } from './syncInfoUtils' ;
const { sprintf } = require ( 'sprintf-js' ) ;
export type MigrationFunction = ( api : FileApi , db : JoplinDatabase ) = > Promise < void > ;
2020-08-02 13:28:50 +02:00
// To add a new migration:
// - Add the migration logic in ./migrations/VERSION_NUM.js
// - Add the file to the array below.
// - Set Setting.syncVersion to VERSION_NUM in models/Setting.js
// - Add tests in synchronizer_migrationHandler
2021-08-12 17:54:10 +02:00
const migrations : MigrationFunction [ ] = [
2020-08-02 13:28:50 +02:00
null ,
2021-08-12 17:54:10 +02:00
migration1 ,
migration2 ,
migration3 ,
2020-08-02 13:28:50 +02:00
] ;
interface SyncTargetInfo {
2020-11-12 21:29:22 +02:00
version : number ;
2020-08-02 13:28:50 +02:00
}
export default class MigrationHandler extends BaseService {
2021-08-07 13:22:37 +02:00
private api_ : FileApi = null ;
2020-11-12 21:13:28 +02:00
private lockHandler_ : LockHandler = null ;
2021-11-03 14:26:26 +02:00
private clientType_ : LockClientType ;
2020-11-12 21:13:28 +02:00
private clientId_ : string ;
2021-08-12 17:54:10 +02:00
private db_ : JoplinDatabase ;
2020-08-02 13:28:50 +02:00
2021-11-03 14:26:26 +02:00
public constructor ( api : FileApi , db : JoplinDatabase , lockHandler : LockHandler , clientType : LockClientType , clientId : string ) {
2020-08-02 13:28:50 +02:00
super ( ) ;
this . api_ = api ;
2021-08-12 17:54:10 +02:00
this . db_ = db ;
2020-08-02 13:28:50 +02:00
this . lockHandler_ = lockHandler ;
this . clientType_ = clientType ;
this . clientId_ = clientId ;
}
2020-11-12 21:13:28 +02:00
public async fetchSyncTargetInfo ( ) : Promise < SyncTargetInfo > {
2020-08-02 13:28:50 +02:00
const syncTargetInfoText = await this . api_ . get ( 'info.json' ) ;
// Returns version 0 if the sync target is empty
2020-11-12 21:13:28 +02:00
let output : SyncTargetInfo = { version : 0 } ;
2020-08-02 13:28:50 +02:00
if ( syncTargetInfoText ) {
output = JSON . parse ( syncTargetInfoText ) ;
if ( ! output . version ) throw new Error ( 'Missing "version" field in info.json' ) ;
} else {
const oldVersion = await this . api_ . get ( '.sync/version.txt' ) ;
if ( oldVersion ) output = { version : 1 } ;
}
return output ;
}
2020-11-12 21:13:28 +02:00
private serializeSyncTargetInfo ( info : SyncTargetInfo ) {
2020-08-02 13:28:50 +02:00
return JSON . stringify ( info ) ;
}
2021-08-12 17:54:10 +02:00
public async checkCanSync ( remoteInfo : SyncInfo = null ) {
remoteInfo = remoteInfo || await fetchSyncInfo ( this . api_ ) ;
2020-08-02 13:28:50 +02:00
const supportedSyncTargetVersion = Setting . value ( 'syncVersion' ) ;
2021-08-12 17:54:10 +02:00
if ( remoteInfo . version ) {
if ( remoteInfo . version > supportedSyncTargetVersion ) {
2021-08-19 11:36:04 +02:00
throw new JoplinError ( sprintf ( 'Sync version of the target (%d) is greater than the version supported by the app (%d). Please upgrade your app.' , remoteInfo . version , supportedSyncTargetVersion ) , 'outdatedClient' ) ;
2021-08-12 17:54:10 +02:00
} else if ( remoteInfo . version < supportedSyncTargetVersion ) {
2021-08-19 11:36:04 +02:00
throw new JoplinError ( sprintf ( 'Sync version of the target (%d) is lower than the version supported by the app (%d). Please upgrade the sync target.' , remoteInfo . version , supportedSyncTargetVersion ) , 'outdatedSyncTarget' ) ;
2020-08-02 13:28:50 +02:00
}
}
}
2020-11-12 21:13:28 +02:00
async upgrade ( targetVersion : number = 0 ) {
2020-08-02 13:28:50 +02:00
const supportedSyncTargetVersion = Setting . value ( 'syncVersion' ) ;
const syncTargetInfo = await this . fetchSyncTargetInfo ( ) ;
if ( syncTargetInfo . version > supportedSyncTargetVersion ) {
2021-08-19 11:36:04 +02:00
throw new JoplinError ( sprintf ( 'Sync version of the target (%d) is greater than the version supported by the app (%d). Please upgrade your app.' , syncTargetInfo . version , supportedSyncTargetVersion ) , 'outdatedClient' ) ;
2020-08-02 13:28:50 +02:00
}
// if (supportedSyncTargetVersion !== migrations.length - 1) {
// // Sanity check - it means a migration has been added by syncVersion has not be incremented or vice-versa,
// // so abort as it can cause strange issues.
// throw new JoplinError('Application error: mismatch between max supported sync version and max migration number: ' + supportedSyncTargetVersion + ' / ' + (migrations.length - 1));
// }
// Special case for version 1 because it didn't have the lock folder and without
// it the lock handler will break. So we create the directory now.
// Also if the sync target version is 0, it means it's a new one so we need the
// lock folder first before doing anything else.
2020-09-12 00:33:34 +02:00
// Temp folder is needed too to get remoteDate() call to work.
2020-08-02 13:28:50 +02:00
if ( syncTargetInfo . version === 0 || syncTargetInfo . version === 1 ) {
2020-09-12 00:33:34 +02:00
this . logger ( ) . info ( 'MigrationHandler: Sync target version is 0 or 1 - creating "locks" and "temp" directory:' , syncTargetInfo ) ;
2020-08-02 13:28:50 +02:00
await this . api_ . mkdir ( Dirnames . Locks ) ;
2020-09-12 00:33:34 +02:00
await this . api_ . mkdir ( Dirnames . Temp ) ;
2020-08-02 13:28:50 +02:00
}
this . logger ( ) . info ( 'MigrationHandler: Acquiring exclusive lock' ) ;
2021-08-18 16:49:43 +02:00
const exclusiveLock = await this . lockHandler_ . acquireLock ( LockType . Exclusive , this . clientType_ , this . clientId_ , {
clearExistingSyncLocksFromTheSameClient : true ,
timeoutMs : 1000 * 30 ,
} ) ;
2020-08-02 13:28:50 +02:00
let autoLockError = null ;
2020-11-12 21:13:28 +02:00
this . lockHandler_ . startAutoLockRefresh ( exclusiveLock , ( error : any ) = > {
2020-08-02 13:28:50 +02:00
autoLockError = error ;
} ) ;
this . logger ( ) . info ( 'MigrationHandler: Acquired exclusive lock:' , exclusiveLock ) ;
try {
for ( let newVersion = syncTargetInfo . version + 1 ; newVersion < migrations . length ; newVersion ++ ) {
if ( targetVersion && newVersion > targetVersion ) break ;
const fromVersion = newVersion - 1 ;
this . logger ( ) . info ( ` MigrationHandler: Migrating from version ${ fromVersion } to version ${ newVersion } ` ) ;
const migration = migrations [ newVersion ] ;
if ( ! migration ) continue ;
try {
if ( autoLockError ) throw autoLockError ;
2021-08-12 17:54:10 +02:00
await migration ( this . api_ , this . db_ ) ;
2020-08-02 13:28:50 +02:00
if ( autoLockError ) throw autoLockError ;
2021-08-12 17:54:10 +02:00
// For legacy support. New migrations should set the sync
// target info directly as needed.
if ( [ 1 , 2 ] . includes ( newVersion ) ) {
await this . api_ . put ( 'info.json' , this . serializeSyncTargetInfo ( {
. . . syncTargetInfo ,
version : newVersion ,
} ) ) ;
}
2020-08-02 13:28:50 +02:00
this . logger ( ) . info ( ` MigrationHandler: Done migrating from version ${ fromVersion } to version ${ newVersion } ` ) ;
} catch ( error ) {
error . message = ` Could not upgrade from version ${ fromVersion } to version ${ newVersion } : ${ error . message } ` ;
throw error ;
}
}
} finally {
this . logger ( ) . info ( 'MigrationHandler: Releasing exclusive lock' ) ;
this . lockHandler_ . stopAutoLockRefresh ( exclusiveLock ) ;
await this . lockHandler_ . releaseLock ( LockType . Exclusive , this . clientType_ , this . clientId_ ) ;
}
}
}