Skip to content

Commit 8a19b3b

Browse files
committed
feat: integration of js-ipfs-repo-migrations
Integration of js-ipfs-repo-migrations brings automatic repo migrations to ipfs-repo (both in-browser and fs). It is possible to control the automatic migration using either config's setting 'repoDisableAutoMigration' or IPFSRepo's option 'disableAutoMigration'. License: MIT Signed-off-by: Adam Uhlir <[email protected]>
1 parent cfcd0b0 commit 8a19b3b

12 files changed

+291
-33
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ This is the implementation of the [IPFS repo spec](https://github.com/ipfs/specs
2828
- [Use in a browser Using a script tag](#use-in-a-browser-using-a-script-tag)
2929
- [Usage](#usage)
3030
- [API](#api)
31+
- [Notes](#notes)
3132
- [Contribute](#contribute)
3233
- [License](#license)
3334

@@ -318,6 +319,11 @@ Returned promise resolves to a `boolean` indicating the existence of the lock.
318319

319320
- [Explanation of how repo is structured](https://github.com/ipfs/js-ipfs-repo/pull/111#issuecomment-279948247)
320321

322+
### Migrations
323+
324+
When there is a new repo migration and the version of repo is increased, don't
325+
forget to propagate the changes into the test repo (`test/test-repo`).
326+
321327
## Contribute
322328

323329
There are some ways you can make this module better:

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"multihashes": "~0.4.14",
5252
"multihashing-async": "~0.7.0",
5353
"ncp": "^2.0.0",
54-
"rimraf": "^2.6.3"
54+
"rimraf": "^2.6.3",
55+
"sinon": "^7.3.1"
5556
},
5657
"dependencies": {
5758
"base32.js": "~0.1.0",
@@ -64,6 +65,7 @@
6465
"err-code": "^1.1.2",
6566
"interface-datastore": "~0.7.0",
6667
"ipfs-block": "~0.8.1",
68+
"ipfs-repo-migrations": "AuHau/js-ipfs-repo-migrations#dev",
6769
"just-safe-get": "^1.3.0",
6870
"just-safe-set": "^2.1.0",
6971
"lodash.has": "^4.5.2",
@@ -73,6 +75,7 @@
7375
},
7476
"license": "MIT",
7577
"contributors": [
78+
"Adam Uhlir<[email protected]>",
7679
"Alan Shaw <[email protected]>",
7780
"Alex Potsides <[email protected]>",
7881
"Brian Hoffman <[email protected]>",

src/default-options-browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
// Default configuration for a repo in the browser
44
module.exports = {
5+
disableAutoMigration: false,
56
lock: 'memory',
67
storageBackends: {
78
root: require('datastore-level'),

src/default-options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
// Default configuration for a repo in node.js
44
module.exports = {
5+
disableAutoMigration: false,
56
lock: 'fs',
67
storageBackends: {
78
root: require('datastore-fs'),

src/errors/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ class NotFoundError extends Error {
1515
NotFoundError.code = 'ERR_NOT_FOUND'
1616
exports.NotFoundError = NotFoundError
1717

18+
/**
19+
* Error raised when version of the stored repo is not compatible with version of this package.
20+
*/
21+
class InvalidRepoVersionError extends Error {
22+
constructor (message) {
23+
super(message)
24+
this.name = 'InvalidRepoVersionError'
25+
this.code = 'ERR_INVALID_REPO_VERSION'
26+
this.message = message
27+
}
28+
}
29+
30+
InvalidRepoVersionError.code = 'ERR_INVALID_REPO_VERSION'
31+
exports.InvalidRepoVersionError = InvalidRepoVersionError
32+
1833
exports.ERR_REPO_NOT_INITIALIZED = 'ERR_REPO_NOT_INITIALIZED'
1934
exports.ERR_REPO_ALREADY_OPEN = 'ERR_REPO_ALREADY_OPEN'
2035
exports.ERR_REPO_ALREADY_CLOSED = 'ERR_REPO_ALREADY_CLOSED'

src/index.js

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ const path = require('path')
66
const debug = require('debug')
77
const Big = require('bignumber.js')
88
const errcode = require('err-code')
9+
const migrator = require('ipfs-repo-migrations')
910

11+
const constants = require('./constants')
1012
const backends = require('./backends')
1113
const version = require('./version')
1214
const config = require('./config')
@@ -26,8 +28,6 @@ const lockers = {
2628
fs: require('./lock')
2729
}
2830

29-
const repoVersion = require('./constants').repoVersion
30-
3131
/**
3232
* IpfsRepo implements all required functionality to read and write to an ipfs repo.
3333
*
@@ -64,7 +64,7 @@ class IpfsRepo {
6464
await this._openRoot()
6565
await this.config.set(buildConfig(config))
6666
await this.spec.set(buildDatastoreSpec(config))
67-
await this.version.set(repoVersion)
67+
await this.version.set(constants.repoVersion)
6868
}
6969

7070
/**
@@ -92,6 +92,17 @@ class IpfsRepo {
9292
this.blocks = await blockstore(blocksBaseStore, this.options.storageBackendOptions.blocks)
9393
log('creating keystore')
9494
this.keys = backends.create('keys', path.join(this.path, 'keys'), this.options)
95+
96+
if (!await this.version.check(constants.repoVersion)) {
97+
log('Something is fishy')
98+
if (!this.options.disableAutoMigration) {
99+
log('Let see what')
100+
await this._migrate(constants.repoVersion)
101+
} else {
102+
throw new ERRORS.InvalidRepoVersionError('Incompatible repo versions. Automatic migrations disabled. Please migrate the repo manually.')
103+
}
104+
}
105+
95106
this.closed = false
96107
log('all opened')
97108
} catch (err) {
@@ -176,7 +187,7 @@ class IpfsRepo {
176187
[config] = await Promise.all([
177188
this.config.exists(),
178189
this.spec.exists(),
179-
this.version.check(repoVersion)
190+
this.version.exists()
180191
])
181192
} catch (err) {
182193
if (err.code === 'ERR_NOT_FOUND') {
@@ -264,6 +275,40 @@ class IpfsRepo {
264275
}
265276
}
266277

278+
async _migrate (toVersion) {
279+
let disableMigrationsConfig
280+
try {
281+
disableMigrationsConfig = await this.config.get('repoDisableAutoMigration')
282+
} catch (e) {
283+
if (e.code === ERRORS.NotFoundError.code) {
284+
disableMigrationsConfig = false
285+
} else {
286+
throw e
287+
}
288+
}
289+
290+
if (disableMigrationsConfig) {
291+
throw new ERRORS.InvalidRepoVersionError('Incompatible repo versions. Automatic migrations disabled. Please migrate the repo manually.')
292+
}
293+
294+
const currentRepoVersion = await this.version.get()
295+
log(currentRepoVersion)
296+
if (currentRepoVersion >= toVersion) {
297+
if (currentRepoVersion > toVersion) {
298+
log('Your repo\'s version is higher then this version of js-ipfs-repo require! You should revert it.')
299+
}
300+
301+
log('Nothing to migrate')
302+
return
303+
}
304+
305+
if (toVersion > migrator.getLatestMigrationVersion()) {
306+
throw new Error('The ipfs-repo-migrations package does not have migration for version: ' + toVersion)
307+
}
308+
309+
return migrator.migrate(this.path, { toVersion: toVersion, ignoreLock: true, repoOptions: this.options })
310+
}
311+
267312
async _storageMaxStat () {
268313
try {
269314
const max = await this.config.get('Datastore.StorageMax')
@@ -298,7 +343,7 @@ async function getSize (queryFn) {
298343
}
299344

300345
module.exports = IpfsRepo
301-
module.exports.repoVersion = repoVersion
346+
module.exports.repoVersion = constants.repoVersion
302347
module.exports.errors = ERRORS
303348

304349
function buildOptions (_options) {

src/version.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const Key = require('interface-datastore').Key
44
const debug = require('debug')
55
const log = debug('repo:version')
6-
const errcode = require('err-code')
76

87
const versionKey = new Key('version')
98

@@ -36,9 +35,9 @@ module.exports = (store) => {
3635
return store.put(versionKey, Buffer.from(String(version)))
3736
},
3837
/**
39-
* Check the current version, and return an error on missmatch
38+
* Check the current version, and returns true if versions matches
4039
* @param {number} expected
41-
* @returns {void}
40+
* @returns {boolean}
4241
*/
4342
async check (expected) {
4443
const version = await this.get()
@@ -47,9 +46,7 @@ module.exports = (store) => {
4746
// TODO: Clean up the compatibility logic. Repo feature detection would be ideal, or a better version schema
4847
const compatibleVersion = (version === 6 && expected === 7) || (expected === 6 && version === 7)
4948

50-
if (version !== expected && !compatibleVersion) {
51-
throw errcode(new Error(`ipfs repo needs migration: expected version v${expected}, found version v${version}`), 'ERR_INVALID_REPO_VERSION')
52-
}
49+
return version === expected || compatibleVersion
5350
}
5451
}
5552
}

test/browser.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,29 @@
44

55
const IPFSRepo = require('../src')
66

7+
async function createTempRepo ({ dontOpen, opts }) {
8+
const date = Date.now().toString()
9+
const repoPath = 'test-repo-for-' + date
10+
11+
const repo = new IPFSRepo(repoPath, opts)
12+
await repo.init({})
13+
14+
if (!dontOpen) {
15+
await repo.open()
16+
}
17+
18+
return {
19+
path: repoPath,
20+
instance: repo,
21+
teardown: async () => {
22+
}
23+
}
24+
}
25+
726
describe('IPFS Repo Tests on the Browser', () => {
827
require('./options-test')
28+
require('./migrations-test')(createTempRepo)
29+
930
const repo = new IPFSRepo('myrepo')
1031

1132
before(async () => {

test/migrations-test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/* eslint max-nested-callbacks: ["error", 8] */
2+
/* eslint-env mocha */
3+
'use strict'
4+
5+
const chai = require('chai')
6+
const dirtyChai = require('dirty-chai')
7+
const expect = chai.expect
8+
chai.use(dirtyChai)
9+
const sinon = require('sinon')
10+
11+
const migrator = require('ipfs-repo-migrations')
12+
const constants = require('../src/constants')
13+
const errors = require('../src/errors')
14+
const IPFSRepo = require('../src')
15+
16+
module.exports = (createTempRepo) => {
17+
describe('Migrations tests', () => {
18+
let teardown
19+
let repo
20+
let migrateStub
21+
let repoVersionStub
22+
let getLatestMigrationVersionStub
23+
24+
before(() => {
25+
repoVersionStub = sinon.stub(constants, 'repoVersion')
26+
migrateStub = sinon.stub(migrator, 'migrate')
27+
getLatestMigrationVersionStub = sinon.stub(migrator, 'getLatestMigrationVersion')
28+
})
29+
30+
after(() => {
31+
repoVersionStub.restore()
32+
migrateStub.restore()
33+
getLatestMigrationVersionStub.restore()
34+
})
35+
36+
beforeEach(async () => {
37+
({ instance: repo, teardown } = await createTempRepo({}))
38+
sinon.reset()
39+
})
40+
41+
afterEach(async () => {
42+
await teardown()
43+
})
44+
45+
it('should migrate by default', async () => {
46+
migrateStub.resolves()
47+
repoVersionStub.value(8)
48+
getLatestMigrationVersionStub.returns(9)
49+
50+
await repo.version.set(7)
51+
await repo.close()
52+
53+
expect(migrateStub.called).to.be.false()
54+
55+
await repo.open()
56+
57+
expect(migrateStub.called).to.be.true()
58+
})
59+
60+
it('should not migrate when option disableAutoMigration is true', async () => {
61+
migrateStub.resolves()
62+
repoVersionStub.resolves(8)
63+
getLatestMigrationVersionStub.returns(9)
64+
65+
await repo.version.set(7)
66+
await repo.close()
67+
68+
const newOpts = Object.assign({}, repo.options)
69+
newOpts.disableAutoMigration = true
70+
const newRepo = new IPFSRepo(repo.path, newOpts)
71+
72+
expect(migrateStub.called).to.be.false()
73+
try {
74+
await newRepo.open()
75+
throw Error('Should throw error')
76+
} catch (e) {
77+
expect(e.code).to.equal(errors.InvalidRepoVersionError.code)
78+
}
79+
80+
expect(migrateStub.called).to.be.false()
81+
})
82+
83+
it('should not migrate when config option repoDisableAutoMigration is true', async () => {
84+
migrateStub.resolves()
85+
repoVersionStub.resolves(8)
86+
getLatestMigrationVersionStub.returns(9)
87+
88+
await repo.config.set('repoDisableAutoMigration', true)
89+
await repo.version.set(7)
90+
await repo.close()
91+
92+
expect(migrateStub.called).to.be.false()
93+
try {
94+
await repo.open()
95+
throw Error('Should throw error')
96+
} catch (e) {
97+
expect(migrateStub.called).to.be.false()
98+
expect(e.code).to.equal(errors.InvalidRepoVersionError.code)
99+
}
100+
})
101+
102+
it('should not migrate when versions matches', async () => {
103+
migrateStub.resolves()
104+
repoVersionStub.value(8)
105+
106+
await repo.version.set(8)
107+
await repo.close()
108+
109+
expect(migrateStub.called).to.be.false()
110+
111+
await repo.open()
112+
113+
expect(migrateStub.called).to.be.false()
114+
})
115+
116+
it('should not migrate when current repo versions is higher then expected', async () => {
117+
migrateStub.resolves()
118+
repoVersionStub.value(8)
119+
120+
await repo.version.set(9)
121+
await repo.close()
122+
123+
expect(migrateStub.called).to.be.false()
124+
125+
await repo.open()
126+
127+
expect(migrateStub.called).to.be.false()
128+
})
129+
130+
it('should throw error if ipfs-repo-migrations does not contain expected migration', async () => {
131+
migrateStub.resolves()
132+
repoVersionStub.value(8)
133+
getLatestMigrationVersionStub.returns(7)
134+
135+
await repo.version.set(7)
136+
await repo.close()
137+
138+
try {
139+
await repo.open()
140+
throw Error('Should throw')
141+
} catch (e) {
142+
expect(e.message).to.include('package does not have migration')
143+
}
144+
})
145+
})
146+
}

0 commit comments

Comments
 (0)