Skip to content
This repository was archived by the owner on Oct 1, 2021. It is now read-only.

Commit cb7167c

Browse files
committed
feat: migration 10 to allow upgrading level in the browser
We use the [level](https://www.npmjs.com/package/level) module to supply either [leveldown](http://npmjs.com/package/leveldown) or [level-js](https://www.npmjs.com/package/level-js) to [datastore-level](https://www.npmjs.com/package/datastore-level) depending on if we're running under node or in the browser. `level@6.x.x` upgrades the `level-js` dependency from `4.x.x` to `5.x.x` which includes the changes from [Level/level-js#179](Level/level-js#179) so `>5.x.x` requires all database keys/values to be Uint8Arrays and they can no longer be strings. We already store values as Uint8Arrays but our keys are strings, so here we add a migration to converts all datastore keys to Uint8Arrays. N.b. `leveldown` already does this conversion for us so this migration only needs to run in the browser.
1 parent d0866b1 commit cb7167c

File tree

8 files changed

+336
-13
lines changed

8 files changed

+336
-13
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ This package is inspired by the [go-ipfs repo migration tool](https://github.com
4242
- [Tests](#tests)
4343
- [Empty migrations](#empty-migrations)
4444
- [Migrations matrix](#migrations-matrix)
45+
- [Migrations](#migrations)
46+
- [7](#7)
47+
- [8](#8)
48+
- [9](#9)
49+
- [10](#10)
4550
- [Developer](#developer)
4651
- [Module versioning notes](#module-versioning-notes)
4752
- [Contribute](#contribute)
@@ -268,6 +273,24 @@ This will create an empty migration with the next version.
268273
| 8 | v0.48.0 |
269274
| 9 | v0.49.0 |
270275

276+
### Migrations
277+
278+
#### 7
279+
280+
This is the initial version of the datastore, inherited from go-IPFS in an attempt to maintain cross-compatibility between the two implementations.
281+
282+
#### 8
283+
284+
Blockstore keys are transformed into base32 representations of the multihash from the CID of the block.
285+
286+
#### 9
287+
288+
Pins were migrated from a DAG to a Datastore - see [ipfs/js-ipfs#2771](https://github.com/ipfs/js-ipfs/pull/2771)
289+
290+
#### 10
291+
292+
`level@6.x.x` upgrades the `level-js` dependency from `4.x.x` to `5.x.x`. This update requires a database migration to convert all string keys/values into buffers. Only runs in the browser, node is unaffected. See [Level/level-js#179](https://github.com/Level/level-js/pull/179)
293+
271294
## Developer
272295

273296
### Module versioning notes

migrations/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ module.exports = [
1616
Object.assign({version: 6}, emptyMigration),
1717
Object.assign({version: 7}, emptyMigration),
1818
require('./migration-8'),
19-
require('./migration-9')
19+
require('./migration-9'),
20+
require('./migration-10')
2021
]

migrations/migration-10/index.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
'use strict'
2+
3+
const { createStore } = require('../../src/utils')
4+
const { Key } = require('interface-datastore')
5+
const fromString = require('uint8arrays/from-string')
6+
const toString = require('uint8arrays/to-string')
7+
8+
const findUpgradableDb = (store) => {
9+
let db = store
10+
11+
while (db.db || db.child) {
12+
db = db.db || db.child
13+
14+
// Will stop at Level in the browser, LevelDOWN in node
15+
if (db.constructor.name === 'Level') {
16+
return db
17+
}
18+
}
19+
}
20+
21+
async function keysToBinary (name, store, onProgress = () => {}) {
22+
let db = findUpgradableDb(store)
23+
24+
// only interested in Level
25+
if (!db) {
26+
onProgress(`${name} did not need an upgrade`)
27+
28+
return
29+
}
30+
31+
onProgress(`Upgrading ${name}`)
32+
33+
await withEach(db, (key, value) => {
34+
return [
35+
{ type: 'del', key: key },
36+
{ type: 'put', key: fromString(key), value: value }
37+
]
38+
})
39+
}
40+
41+
async function keysToStrings (name, store, onProgress = () => {}) {
42+
let db = findUpgradableDb(store)
43+
44+
// only interested in Level
45+
if (!db) {
46+
onProgress(`${name} did not need a downgrade`)
47+
48+
return
49+
}
50+
51+
onProgress(`Downgrading ${name}`)
52+
53+
await withEach(db, (key, value) => {
54+
return [
55+
{ type: 'del', key: key },
56+
{ type: 'put', key: toString(key), value: value }
57+
]
58+
})
59+
}
60+
61+
async function process (repoPath, repoOptions, onProgress, fn) {
62+
const datastores = Object.keys(repoOptions.storageBackends)
63+
.filter(key => repoOptions.storageBackends[key].name === 'LevelDatastore')
64+
.map(name => ({
65+
name,
66+
store: createStore(repoPath, name, repoOptions)
67+
}))
68+
69+
onProgress(0, `Migrating ${datastores.length} dbs`)
70+
let migrated = 0
71+
72+
for (const { name, store } of datastores) {
73+
await store.open()
74+
75+
try {
76+
await fn(name, store, (message) => {
77+
onProgress(parseInt((migrated / datastores.length) * 100), message)
78+
})
79+
} finally {
80+
migrated++
81+
store.close()
82+
}
83+
}
84+
85+
onProgress(100, `Migrated ${datastores.length} dbs`)
86+
}
87+
88+
module.exports = {
89+
version: 10,
90+
description: 'Migrates datastore-level keys to binary',
91+
migrate: (repoPath, repoOptions, onProgress) => {
92+
return process(repoPath, repoOptions, onProgress, keysToBinary)
93+
},
94+
revert: (repoPath, repoOptions, onProgress) => {
95+
return process(repoPath, repoOptions, onProgress, keysToStrings)
96+
}
97+
}
98+
99+
/**
100+
* @typedef {Error | undefined} Err
101+
* @typedef {Uint8Array|string} Key
102+
* @typedef {Uint8Array} Value
103+
* @typedef {{ type: 'del', key: Key } | { type: 'put', key: Key, value: Value }} Operation
104+
*
105+
* Uses the upgrade strategy from level-js@5.x.x - note we can't call the `.upgrade` command
106+
* directly because it will be removed in level-js@6.x.x and we can't guarantee users will
107+
* have migrated by then - e.g. they may jump from level-js@4.x.x straight to level-js@6.x.x
108+
* so we have to duplicate the code here.
109+
*
110+
* @param {import('interface-datastore').Datastore} db
111+
* @param {function (Err, Key, Value): Operation[]} fn
112+
*/
113+
function withEach (db, fn) {
114+
function batch (operations, next) {
115+
const store = db.store('readwrite')
116+
const transaction = store.transaction
117+
let index = 0
118+
let error
119+
120+
transaction.onabort = () => next(error || transaction.error || new Error('aborted by user'))
121+
transaction.oncomplete = () => next()
122+
123+
function loop () {
124+
var op = operations[index++]
125+
var key = op.key
126+
127+
try {
128+
var req = op.type === 'del' ? store.delete(key) : store.put(op.value, key)
129+
} catch (err) {
130+
error = err
131+
transaction.abort()
132+
return
133+
}
134+
135+
if (index < operations.length) {
136+
req.onsuccess = loop
137+
}
138+
}
139+
140+
loop()
141+
}
142+
143+
return new Promise((resolve, reject) => {
144+
const it = db.iterator()
145+
// raw keys and values only
146+
it._deserializeKey = it._deserializeValue = (data) => data
147+
next()
148+
149+
function next () {
150+
it.next((err, key, value) => {
151+
if (err || key === undefined) {
152+
it.end((err2) => {
153+
if (err2) {
154+
reject(err2)
155+
return
156+
}
157+
158+
resolve()
159+
})
160+
161+
return
162+
}
163+
164+
batch(fn(key, value), next)
165+
})
166+
}
167+
})
168+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
"datastore-level": "^3.0.0",
6565
"it-all": "^1.0.2",
6666
"just-safe-set": "^2.1.0",
67+
"level-5": "npm:level@^5.0.0",
68+
"level-6": "npm:level@^6.0.0",
6769
"ncp": "^2.0.0",
6870
"rimraf": "^3.0.0",
6971
"sinon": "^9.0.2"

test/browser.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
/* eslint-env mocha */
22
'use strict'
33

4+
const DatastoreLevel = require('datastore-level')
45
const { createRepo, createAndLoadRepo } = require('./fixtures/repo')
56

67
const repoOptions = {
78
lock: 'memory',
89
storageBackends: {
9-
root: require('datastore-level'),
10-
blocks: require('datastore-level'),
11-
keys: require('datastore-level'),
12-
datastore: require('datastore-level'),
13-
pins: require('datastore-level')
10+
root: DatastoreLevel,
11+
blocks: DatastoreLevel,
12+
keys: DatastoreLevel,
13+
datastore: DatastoreLevel,
14+
pins: DatastoreLevel
1415
},
1516
storageBackendOptions: {
1617
root: {
@@ -51,7 +52,7 @@ describe('Browser specific tests', () => {
5152
})
5253

5354
describe('migrations tests', () => {
54-
require('./migrations')(() => createRepo(repoOptions), repoCleanup, repoOptions)
55+
require('./migrations')(() => createRepo(repoOptions), repoCleanup)
5556
})
5657

5758
describe('init tests', () => {

test/migrations/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ module.exports = (createRepo, repoCleanup) => {
7777
describe(name, () => {
7878
require('./migration-8-test')(createRepo, repoCleanup, options)
7979
require('./migration-9-test')(createRepo, repoCleanup, options)
80+
require('./migration-10-test')(createRepo, repoCleanup, options)
8081
})
8182
})
8283
}

test/migrations/migration-10-test.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* eslint-env mocha */
2+
/* eslint-disable max-nested-callbacks */
3+
'use strict'
4+
5+
const { expect } = require('aegir/utils/chai')
6+
7+
const { createStore } = require('../../src/utils')
8+
const migration = require('../../migrations/migration-10')
9+
const Key = require('interface-datastore').Key
10+
const fromString = require('uint8arrays/from-string')
11+
const Level5 = require('level-5')
12+
const Level6 = require('level-6')
13+
14+
const keys = {
15+
CIQCKN76QUQUGYCHIKGFE6V6P3GJ2W26YFFPQW6YXV7NFHH3QB2RI3I: 'hello',
16+
CIQKKLBWAIBQZOIS5X7E32LQAL6236OUKZTMHPQSFIXPWXNZHQOV7JQ: fromString('derp')
17+
}
18+
19+
async function bootstrap (dir, backend, repoOptions) {
20+
const store = createStore(dir, backend, repoOptions)
21+
await store.open()
22+
23+
for (const name of Object.keys(keys)) {
24+
await store.put(new Key(name), keys[name])
25+
}
26+
27+
await store.close()
28+
}
29+
30+
async function validate (dir, backend, repoOptions) {
31+
const store = createStore(dir, backend, repoOptions)
32+
33+
await store.open()
34+
35+
for (const name of Object.keys(keys)) {
36+
const key = new Key(`/${name}`)
37+
38+
expect(await store.has(key)).to.be.true(`Could not read key ${name}`)
39+
expect(store.get(key)).to.eventually.equal(keys[name], `Could not read value for key ${keys[name]}`)
40+
}
41+
42+
await store.close()
43+
}
44+
45+
function withLevel (repoOptions, levelImpl) {
46+
const stores = Object.keys(repoOptions.storageBackends)
47+
.filter(key => repoOptions.storageBackends[key].name === 'LevelDatastore')
48+
49+
const output = {
50+
...repoOptions
51+
}
52+
53+
stores.forEach(store => {
54+
// override version of level passed to datastore options
55+
output.storageBackendOptions[store] = {
56+
...output.storageBackendOptions[store],
57+
db: levelImpl
58+
}
59+
})
60+
61+
return output
62+
}
63+
64+
module.exports = (setup, cleanup, repoOptions) => {
65+
describe('migration 10', function () {
66+
this.timeout(240 * 1000)
67+
let dir
68+
69+
beforeEach(async () => {
70+
dir = await setup()
71+
})
72+
73+
afterEach(async () => {
74+
await cleanup(dir)
75+
})
76+
77+
describe('forwards', () => {
78+
beforeEach(async () => {
79+
for (const backend of Object.keys(repoOptions.storageBackends)) {
80+
await bootstrap(dir, backend, withLevel(repoOptions, Level5))
81+
}
82+
})
83+
84+
it('should migrate keys and values forward', async () => {
85+
await migration.migrate(dir, withLevel(repoOptions, Level6), () => {})
86+
87+
for (const backend of Object.keys(repoOptions.storageBackends)) {
88+
await validate(dir, backend, withLevel(repoOptions, Level6))
89+
}
90+
})
91+
})
92+
93+
describe('backwards using level@6.x.x', () => {
94+
beforeEach(async () => {
95+
for (const backend of Object.keys(repoOptions.storageBackends)) {
96+
await bootstrap(dir, backend, withLevel(repoOptions, Level6))
97+
}
98+
})
99+
100+
it('should migrate keys and values backward', async () => {
101+
await migration.revert(dir, withLevel(repoOptions, Level6), () => {})
102+
103+
for (const backend of Object.keys(repoOptions.storageBackends)) {
104+
await validate(dir, backend, withLevel(repoOptions, Level5))
105+
}
106+
})
107+
})
108+
109+
describe('backwards using level@5.x.x', () => {
110+
beforeEach(async () => {
111+
for (const backend of Object.keys(repoOptions.storageBackends)) {
112+
await bootstrap(dir, backend, withLevel(repoOptions, Level6))
113+
}
114+
})
115+
116+
it('should migrate keys and values backward', async () => {
117+
await migration.revert(dir, withLevel(repoOptions, Level5), () => {})
118+
119+
for (const backend of Object.keys(repoOptions.storageBackends)) {
120+
await validate(dir, backend, withLevel(repoOptions, Level5))
121+
}
122+
})
123+
})
124+
})
125+
}

0 commit comments

Comments
 (0)