Skip to content

Commit a0b6f95

Browse files
AuHaujacobheun
authored andcommitted
feat: automatic repo migrations (#202)
* 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]> * Throw error when repo's version is higher then expected License: MIT Signed-off-by: Adam Uhlir <[email protected]> * Tweaks License: MIT Signed-off-by: Adam Uhlir <[email protected]> * fix: readme changes * fix: tests setup * style: lint * fix: tweaks * feat: automatic reversion of repo * fix: pr feedback * chore: update dev-dependencies * chore: add released js-ipfs-repo-migrations
1 parent 9b25bb4 commit a0b6f95

10 files changed

+253
-43
lines changed

README.md

+11
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

@@ -136,6 +137,7 @@ Arguments:
136137

137138
* `path` (string, mandatory): the path for this repo
138139
* `options` (object, optional): may contain the following values
140+
* `autoMigrate` (bool, defaults to `true`): controls automatic migrations of repository.
139141
* `lock` ([Lock](#lock) or string *Deprecated*): what type of lock to use. Lock has to be acquired when opening. string can be `"fs"` or `"memory"`.
140142
* `storageBackends` (object, optional): may contain the following values, which should each be a class implementing the [datastore interface](https://github.com/ipfs/interface-datastore#readme):
141143
* `root` (defaults to [`datastore-fs`](https://github.com/ipfs/js-datastore-fs#readme) in Node.js and [`datastore-level`](https://github.com/ipfs/js-datastore-level#readme) in the browser). Defines the back-end type used for gets and puts of values at the root (`repo.set()`, `repo.get()`)
@@ -318,6 +320,15 @@ Returned promise resolves to a `boolean` indicating the existence of the lock.
318320

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

323+
### Migrations
324+
325+
When there is a new repo migration and the version of repo is increased, don't
326+
forget to propagate the changes into the test repo (`test/test-repo`).
327+
328+
**For tools that run mainly in the browser environment, be aware that disabling automatic
329+
migrations leaves the user with no way to run the migrations because there is no CLI in the browser. In such
330+
a case, you should provide a way to trigger migrations manually.**
331+
321332
## Contribute
322333

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

package.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,16 @@
4343
"npm": ">=3.0.0"
4444
},
4545
"devDependencies": {
46-
"aegir": "^19.0.3",
46+
"aegir": "^20.4.1",
4747
"chai": "^4.2.0",
4848
"dirty-chai": "^2.0.1",
4949
"lodash": "^4.17.11",
5050
"memdown": "^4.0.0",
51-
"multihashes": "~0.4.14",
52-
"multihashing-async": "~0.7.0",
51+
"multihashes": "~0.4.15",
52+
"multihashing-async": "~0.8.0",
5353
"ncp": "^2.0.0",
54-
"rimraf": "^2.6.3"
54+
"rimraf": "^3.0.0",
55+
"sinon": "^7.5.0"
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": "~0.1.0",
6769
"just-safe-get": "^1.3.0",
6870
"just-safe-set": "^2.1.0",
6971
"lodash.has": "^4.5.2",

src/errors/index.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ class LockExistsError extends Error {
77
constructor (message) {
88
super(message)
99
this.name = 'LockExistsError'
10-
this.code = 'ERR_LOCK_EXISTS'
11-
this.message = message
10+
this.code = LockExistsError.code
1211
}
1312
}
1413

@@ -22,14 +21,27 @@ class NotFoundError extends Error {
2221
constructor (message) {
2322
super(message)
2423
this.name = 'NotFoundError'
25-
this.code = 'ERR_NOT_FOUND'
26-
this.message = message
24+
this.code = NotFoundError.code
2725
}
2826
}
2927

3028
NotFoundError.code = 'ERR_NOT_FOUND'
3129
exports.NotFoundError = NotFoundError
3230

31+
/**
32+
* Error raised when version of the stored repo is not compatible with version of this package.
33+
*/
34+
class InvalidRepoVersionError extends Error {
35+
constructor (message) {
36+
super(message)
37+
this.name = 'InvalidRepoVersionError'
38+
this.code = InvalidRepoVersionError.code
39+
}
40+
}
41+
42+
InvalidRepoVersionError.code = 'ERR_INVALID_REPO_VERSION'
43+
exports.InvalidRepoVersionError = InvalidRepoVersionError
44+
3345
exports.ERR_REPO_NOT_INITIALIZED = 'ERR_REPO_NOT_INITIALIZED'
3446
exports.ERR_REPO_ALREADY_OPEN = 'ERR_REPO_ALREADY_OPEN'
3547
exports.ERR_REPO_ALREADY_CLOSED = 'ERR_REPO_ALREADY_CLOSED'

src/index.js

+49-8
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')
@@ -20,14 +22,13 @@ const ERRORS = require('./errors')
2022
const log = debug('repo')
2123

2224
const noLimit = Number.MAX_SAFE_INTEGER
25+
const AUTO_MIGRATE_CONFIG_KEY = 'repoAutoMigrate'
2326

2427
const lockers = {
2528
memory: require('./lock-memory'),
2629
fs: require('./lock')
2730
}
2831

29-
const repoVersion = require('./constants').repoVersion
30-
3132
/**
3233
* IpfsRepo implements all required functionality to read and write to an ipfs repo.
3334
*
@@ -64,7 +65,7 @@ class IpfsRepo {
6465
await this._openRoot()
6566
await this.config.set(buildConfig(config))
6667
await this.spec.set(buildDatastoreSpec(config))
67-
await this.version.set(repoVersion)
68+
await this.version.set(constants.repoVersion)
6869
}
6970

7071
/**
@@ -92,6 +93,16 @@ class IpfsRepo {
9293
this.blocks = await blockstore(blocksBaseStore, this.options.storageBackendOptions.blocks)
9394
log('creating keystore')
9495
this.keys = backends.create('keys', path.join(this.path, 'keys'), this.options)
96+
97+
const isCompatible = await this.version.check(constants.repoVersion)
98+
if (!isCompatible) {
99+
if (await this._isAutoMigrationEnabled()) {
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') {
@@ -240,8 +251,7 @@ class IpfsRepo {
240251
*/
241252
async stat (options) {
242253
options = Object.assign({}, { human: false }, options)
243-
let storageMax, blocks, version, datastore, keys
244-
[storageMax, blocks, version, datastore, keys] = await Promise.all([
254+
const [storageMax, blocks, version, datastore, keys] = await Promise.all([
245255
this._storageMaxStat(),
246256
this._blockStat(),
247257
this.version.get(),
@@ -264,6 +274,37 @@ class IpfsRepo {
264274
}
265275
}
266276

277+
async _isAutoMigrationEnabled () {
278+
if (this.options.autoMigrate !== undefined) {
279+
return this.options.autoMigrate
280+
}
281+
282+
let autoMigrateConfig
283+
try {
284+
autoMigrateConfig = await this.config.get(AUTO_MIGRATE_CONFIG_KEY)
285+
} catch (e) {
286+
if (e.code === ERRORS.NotFoundError.code) {
287+
autoMigrateConfig = true // Config's default value is True
288+
} else {
289+
throw e
290+
}
291+
}
292+
293+
return autoMigrateConfig
294+
}
295+
296+
async _migrate (toVersion) {
297+
const currentRepoVersion = await this.version.get()
298+
299+
if (currentRepoVersion > toVersion) {
300+
log('reverting to version ' + toVersion)
301+
return migrator.revert(this.path, toVersion, { ignoreLock: true, repoOptions: this.options })
302+
} else {
303+
log('migrating to version ' + toVersion)
304+
return migrator.migrate(this.path, toVersion, { ignoreLock: true, repoOptions: this.options })
305+
}
306+
}
307+
267308
async _storageMaxStat () {
268309
try {
269310
const max = await this.config.get('Datastore.StorageMax')
@@ -289,7 +330,7 @@ class IpfsRepo {
289330
}
290331

291332
async function getSize (queryFn) {
292-
let sum = new Big(0)
333+
const sum = new Big(0)
293334
for await (const block of queryFn.query({})) {
294335
sum.plus(block.value.byteLength)
295336
.plus(block.key._buf.byteLength)
@@ -299,7 +340,7 @@ async function getSize (queryFn) {
299340

300341
module.exports = IpfsRepo
301342
module.exports.utils = { blockstore: require('./blockstore-utils') }
302-
module.exports.repoVersion = repoVersion
343+
module.exports.repoVersion = constants.repoVersion
303344
module.exports.errors = ERRORS
304345

305346
function buildOptions (_options) {

src/version.js

+3-6
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/blockstore-test.js

+3
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ module.exports = (repo) => {
8585
close () {
8686

8787
}
88+
8889
has () {
8990
return true
9091
}
92+
9193
batch () {
9294
return {
9395
put () {
@@ -217,6 +219,7 @@ module.exports = (repo) => {
217219
close () {
218220

219221
}
222+
220223
get (c) {
221224
if (c.toString() === key.toString()) {
222225
throw err

test/browser.js

+13
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@
44

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

7+
async function createTempRepo (options = {}) {
8+
const date = Date.now().toString()
9+
const repoPath = 'test-repo-for-' + date
10+
11+
const repo = new IPFSRepo(repoPath, options)
12+
await repo.init({})
13+
await repo.open()
14+
15+
return repo
16+
}
17+
718
describe('IPFS Repo Tests on the Browser', () => {
819
require('./options-test')
20+
require('./migrations-test')(createTempRepo)
21+
922
const repo = new IPFSRepo('myrepo')
1023

1124
before(async () => {

0 commit comments

Comments
 (0)