Browse Source

Reduce risk of file-system corruption / inconsistencies

The migration nonce is now stored in `app.json` file instead of
separate `migration.json`. It will ensure consistency between nonce
and app data, because the file is written in atomic way.

Migration handle legacy migration file and remove it properly.
master
meriadec 7 years ago
committed by Gaëtan Renaudeau
parent
commit
6cec4d7c77
  1. 8
      src/migrations/index.js
  2. 11
      src/migrations/migrations.js
  3. 52
      src/migrations/migrations.spec.js
  4. BIN
      src/migrations/mocks/userdata_v1.1.1_mock-01.zip

8
src/migrations/index.js

@ -8,14 +8,16 @@ import migrations from './migrations'
// Logic to run all the migrations based on what was not yet run:
export const runMigrations = async (): Promise<void> => {
const current = await db.getNamespace('migrations')
// Legacy: the migration nonce was previously stored in separate file
// it can happen, so we have to check it from here also.
const current = (await db.getKey('app', 'migrations')) || (await db.getNamespace('migrations'))
let { nonce } = current || { nonce: migrations.length }
const outdated = migrations.length - nonce
if (!outdated) {
if (!current) {
await db.setNamespace('migrations', { nonce })
await db.setKey('app', 'migrations.nonce', nonce)
}
return
}
@ -31,6 +33,6 @@ export const runMigrations = async (): Promise<void> => {
}
logger.log(`${outdated} migration(s) performed.`)
} finally {
await db.setNamespace('migrations', { nonce })
await db.setKey('app', 'migrations.nonce', nonce)
}
}

11
src/migrations/migrations.js

@ -37,6 +37,17 @@ const migrations: Migration[] = [
)
},
},
{
doc: 'merging migrations into app.json',
run: async () => {
const migrations = await db.getNamespace('migrations')
await db.setKey('app', 'migrations', migrations)
const dbPath = db.getDBPath()
try {
await fsUnlink(path.resolve(dbPath, 'migrations.json'))
} catch (err) {} // eslint-disable-line
},
},
]
async function getFileData(dbPath, fileName) {

52
src/migrations/migrations.spec.js

@ -12,6 +12,7 @@ import db from 'helpers/db'
const rimraf = promisify(rimrafModule)
const fsReaddir = promisify(fs.readdir)
const fsReadFile = promisify(fs.readFile)
const tmpDir = os.tmpdir()
@ -20,14 +21,12 @@ const accountsTransform = {
set: encodeAccountsModel,
}
describe('migration 1', () => {
describe('from nonce 0', () => {
describe('without encryption', () => {
test('merging db files', async () => {
const dir = await extractMock('userdata_v1.0.5_mock-01')
let files
db.init(dir)
files = await fsReaddir(dir)
expect(files).toEqual([
await expectFiles(dir, [
'accounts.json',
'countervalues.json',
'migrations.json',
@ -35,8 +34,7 @@ describe('migration 1', () => {
'user.json',
])
await runMigrations()
files = await fsReaddir(dir)
expect(files).toEqual(['app.json', 'migrations.json', 'windowParams.json'])
await expectFiles(dir, ['app.json', 'windowParams.json'])
db.init(dir)
db.registerTransform('app', 'accounts', accountsTransform)
const accounts = await db.getKey('app', 'accounts')
@ -53,9 +51,12 @@ describe('migration 1', () => {
test('handle missing file without crash', async () => {
const dir = await extractMock('userdata_v1.0.5_mock-03-missing-file')
let files
files = await fsReaddir(dir)
expect(files).toEqual(['countervalues.json', 'migrations.json', 'settings.json', 'user.json'])
await expectFiles(dir, [
'countervalues.json',
'migrations.json',
'settings.json',
'user.json',
])
db.init(dir)
let err
try {
@ -64,15 +65,12 @@ describe('migration 1', () => {
err = e
}
expect(err).toBeUndefined()
files = await fsReaddir(dir)
expect(files).toEqual(['app.json', 'migrations.json', 'windowParams.json'])
await expectFiles(dir, ['app.json', 'windowParams.json'])
})
test('handle where app.json is already present', async () => {
const dir = await extractMock('userdata_v1.0.5_mock-04-app-json-present')
let files
files = await fsReaddir(dir)
expect(files).toEqual([
await expectFiles(dir, [
'accounts.json',
'app.json',
'countervalues.json',
@ -82,8 +80,7 @@ describe('migration 1', () => {
])
db.init(dir)
await runMigrations()
files = await fsReaddir(dir)
expect(files).toEqual(['app.json', 'migrations.json', 'windowParams.json'])
await expectFiles(dir, ['app.json', 'windowParams.json'])
})
})
@ -94,8 +91,7 @@ describe('migration 1', () => {
db.registerTransform('app', 'accounts', accountsTransform)
await runMigrations()
await db.setEncryptionKey('app', 'accounts', 'passw0rd')
const files = await fsReaddir(dir)
expect(files).toEqual(['app.json', 'migrations.json', 'windowParams.json'])
await expectFiles(dir, ['app.json', 'windowParams.json'])
const accounts = await db.getKey('app', 'accounts')
expect(accounts.length).toBe(6)
expect(accounts[0].balance).toBeInstanceOf(BigNumber)
@ -114,6 +110,21 @@ describe('migration 1', () => {
})
})
describe('from nonce 1', () => {
test('merging migration file into app file', async () => {
const dir = await extractMock('userdata_v1.1.1_mock-01')
await expectFiles(dir, ['app.json', 'migrations.json', 'windowParams.json'])
const migrationsBefore = await fsReadFile(path.resolve(dir, 'migrations.json'), 'utf-8')
expect(migrationsBefore).toBe('{"data":{"nonce":1}}')
db.init(dir)
db.registerTransform('app', 'accounts', accountsTransform)
await runMigrations()
await expectFiles(dir, ['app.json', 'windowParams.json'])
const migrations = await db.getKey('app', 'migrations')
expect(migrations).toEqual({ nonce: 2 })
})
})
async function extractMock(mockName) {
const destDirectory = path.resolve(tmpDir, mockName)
const zipFilePath = path.resolve(__dirname, 'mocks', `${mockName}.zip`)
@ -129,3 +140,8 @@ function extractZip(zipFilePath, destDirectory) {
childProcess.on('error', reject)
})
}
async function expectFiles(dir, expectedFiles) {
const files = await fsReaddir(dir)
expect(files).toEqual(expectedFiles)
}

BIN
src/migrations/mocks/userdata_v1.1.1_mock-01.zip

Binary file not shown.
Loading…
Cancel
Save