2020-08-02 13:28:50 +02:00
import LockHandler , { LockType } from './LockHandler' ;
import { Dirnames } from './utils/types' ;
2021-01-22 19:41:11 +02:00
import BaseService from '../BaseService' ;
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
const migrations = [
null ,
require ( './migrations/1.js' ) . default ,
require ( './migrations/2.js' ) . default ,
] ;
2021-01-22 19:41:11 +02:00
import Setting from '../../models/Setting' ;
2020-08-02 13:28:50 +02:00
const { sprintf } = require ( 'sprintf-js' ) ;
2020-11-05 18:58:23 +02:00
const JoplinError = require ( '../../JoplinError' ) ;
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 {
2020-11-12 21:13:28 +02:00
private api_ : any = null ;
private lockHandler_ : LockHandler = null ;
private clientType_ : string ;
private clientId_ : string ;
2020-08-02 13:28:50 +02:00
2020-11-12 21:13:28 +02:00
constructor ( api : any , lockHandler : LockHandler , clientType : string , clientId : string ) {
2020-08-02 13:28:50 +02:00
super ( ) ;
this . api_ = api ;
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 ) ;
}
2020-11-12 21:13:28 +02:00
async checkCanSync ( ) : Promise < SyncTargetInfo > {
2020-08-02 13:28:50 +02:00
const supportedSyncTargetVersion = Setting . value ( 'syncVersion' ) ;
const syncTargetInfo = await this . fetchSyncTargetInfo ( ) ;
if ( syncTargetInfo . version ) {
if ( syncTargetInfo . version > supportedSyncTargetVersion ) {
throw new JoplinError ( sprintf ( 'Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.' , syncTargetInfo . version , supportedSyncTargetVersion ) , 'outdatedClient' ) ;
} else if ( syncTargetInfo . version < supportedSyncTargetVersion ) {
throw new JoplinError ( sprintf ( 'Sync version of the target (%d) is lower than the version supported by the client (%d). Please upgrade the sync target.' , syncTargetInfo . version , supportedSyncTargetVersion ) , 'outdatedSyncTarget' ) ;
}
}
return syncTargetInfo ;
}
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 ) {
throw new JoplinError ( sprintf ( 'Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.' , syncTargetInfo . version , supportedSyncTargetVersion ) , 'outdatedClient' ) ;
}
// 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' ) ;
const exclusiveLock = await this . lockHandler_ . acquireLock ( LockType . Exclusive , this . clientType_ , this . clientId_ , 1000 * 30 ) ;
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 ;
await migration ( this . api_ ) ;
if ( autoLockError ) throw autoLockError ;
await this . api_ . put ( 'info.json' , this . serializeSyncTargetInfo ( {
. . . syncTargetInfo ,
version : newVersion ,
} ) ) ;
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_ ) ;
}
}
}