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

Commit 7dc562b

Browse files
authored
feat: migration 10 to allow upgrading level in the browser (#59)
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. `[email protected]` 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 7dc562b

File tree

13 files changed

+451
-29
lines changed

13 files changed

+451
-29
lines changed

README.md

+23
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+
`[email protected]` 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

+2-1
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

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

migrations/migration-8/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ async function process (repoPath, repoOptions, onProgress, keyFunction) {
7575
module.exports = {
7676
version: 8,
7777
description: 'Transforms key names into base32 encoding and converts Block store to use bare multihashes encoded as base32',
78-
migrate: (repoPath, repoOptions, onProgress) => {
78+
migrate: (repoPath, repoOptions, onProgress = () => {}) => {
7979
return process(repoPath, repoOptions, onProgress, keyToMultihash)
8080
},
81-
revert: (repoPath, repoOptions, onProgress) => {
81+
revert: (repoPath, repoOptions, onProgress = () => {}) => {
8282
return process(repoPath, repoOptions, onProgress, keyToCid)
8383
}
8484
}

migrations/migration-9/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,10 @@ async function process (repoPath, repoOptions, onProgress, fn) {
135135
module.exports = {
136136
version: 9,
137137
description: 'Migrates pins to datastore',
138-
migrate: (repoPath, repoOptions, onProgress) => {
138+
migrate: (repoPath, repoOptions, onProgress = () => {}) => {
139139
return process(repoPath, repoOptions, onProgress, pinsToDatastore)
140140
},
141-
revert: (repoPath, repoOptions, onProgress) => {
141+
revert: (repoPath, repoOptions, onProgress = () => {}) => {
142142
return process(repoPath, repoOptions, onProgress, pinsToDAG)
143143
}
144144
}

package.json

+2
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"

src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async function migrate (path, repoOptions, toVersion, { ignoreLock = false, onPr
121121
await repoVersion.setVersion(path, toVersion || getLatestMigrationVersion(migrations), repoOptions)
122122
}
123123

124-
log('Repo successfully migrated ', toVersion !== undefined ? `to version ${toVersion}!` : 'to latest version!')
124+
log('Repo successfully migrated', toVersion !== undefined ? `to version ${toVersion}!` : 'to latest version!')
125125
} finally {
126126
if (!isDryRun && !ignoreLock) {
127127
await lock.close()

src/repo/version.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const repoInit = require('./init')
44
const { MissingRepoOptionsError, NotInitializedRepoError } = require('../errors')
55
const { VERSION_KEY, createStore } = require('../utils')
66
const uint8ArrayFromString = require('uint8arrays/from-string')
7+
const uint8ArrayToString = require('uint8arrays/to-string')
78

89
exports.getVersion = getVersion
910

@@ -28,7 +29,14 @@ async function getVersion (path, repoOptions) {
2829
const store = createStore(path, 'root', repoOptions)
2930
await store.open()
3031

31-
const version = parseInt(await store.get(VERSION_KEY))
32+
let version = await store.get(VERSION_KEY)
33+
34+
if (version instanceof Uint8Array) {
35+
version = uint8ArrayToString(version)
36+
}
37+
38+
version = parseInt(version)
39+
3240
await store.close()
3341

3442
return version

0 commit comments

Comments
 (0)