This repository has been archived by the owner on Oct 1, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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. `[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.
- Loading branch information
1 parent
d0866b1
commit cb7167c
Showing
8 changed files
with
336 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,11 @@ This package is inspired by the [go-ipfs repo migration tool](https://github.com | |
- [Tests](#tests) | ||
- [Empty migrations](#empty-migrations) | ||
- [Migrations matrix](#migrations-matrix) | ||
- [Migrations](#migrations) | ||
- [7](#7) | ||
- [8](#8) | ||
- [9](#9) | ||
- [10](#10) | ||
- [Developer](#developer) | ||
- [Module versioning notes](#module-versioning-notes) | ||
- [Contribute](#contribute) | ||
|
@@ -268,6 +273,24 @@ This will create an empty migration with the next version. | |
| 8 | v0.48.0 | | ||
| 9 | v0.49.0 | | ||
|
||
### Migrations | ||
|
||
#### 7 | ||
|
||
This is the initial version of the datastore, inherited from go-IPFS in an attempt to maintain cross-compatibility between the two implementations. | ||
|
||
#### 8 | ||
|
||
Blockstore keys are transformed into base32 representations of the multihash from the CID of the block. | ||
|
||
#### 9 | ||
|
||
Pins were migrated from a DAG to a Datastore - see [ipfs/js-ipfs#2771](https://github.com/ipfs/js-ipfs/pull/2771) | ||
|
||
#### 10 | ||
|
||
`[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) | ||
|
||
## Developer | ||
|
||
### Module versioning notes | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
'use strict' | ||
|
||
const { createStore } = require('../../src/utils') | ||
const { Key } = require('interface-datastore') | ||
const fromString = require('uint8arrays/from-string') | ||
const toString = require('uint8arrays/to-string') | ||
|
||
const findUpgradableDb = (store) => { | ||
let db = store | ||
|
||
while (db.db || db.child) { | ||
db = db.db || db.child | ||
|
||
// Will stop at Level in the browser, LevelDOWN in node | ||
if (db.constructor.name === 'Level') { | ||
return db | ||
} | ||
} | ||
} | ||
|
||
async function keysToBinary (name, store, onProgress = () => {}) { | ||
let db = findUpgradableDb(store) | ||
|
||
// only interested in Level | ||
if (!db) { | ||
onProgress(`${name} did not need an upgrade`) | ||
|
||
return | ||
} | ||
|
||
onProgress(`Upgrading ${name}`) | ||
|
||
await withEach(db, (key, value) => { | ||
return [ | ||
{ type: 'del', key: key }, | ||
{ type: 'put', key: fromString(key), value: value } | ||
] | ||
}) | ||
} | ||
|
||
async function keysToStrings (name, store, onProgress = () => {}) { | ||
let db = findUpgradableDb(store) | ||
|
||
// only interested in Level | ||
if (!db) { | ||
onProgress(`${name} did not need a downgrade`) | ||
|
||
return | ||
} | ||
|
||
onProgress(`Downgrading ${name}`) | ||
|
||
await withEach(db, (key, value) => { | ||
return [ | ||
{ type: 'del', key: key }, | ||
{ type: 'put', key: toString(key), value: value } | ||
] | ||
}) | ||
} | ||
|
||
async function process (repoPath, repoOptions, onProgress, fn) { | ||
const datastores = Object.keys(repoOptions.storageBackends) | ||
.filter(key => repoOptions.storageBackends[key].name === 'LevelDatastore') | ||
.map(name => ({ | ||
name, | ||
store: createStore(repoPath, name, repoOptions) | ||
})) | ||
|
||
onProgress(0, `Migrating ${datastores.length} dbs`) | ||
let migrated = 0 | ||
|
||
for (const { name, store } of datastores) { | ||
await store.open() | ||
|
||
try { | ||
await fn(name, store, (message) => { | ||
onProgress(parseInt((migrated / datastores.length) * 100), message) | ||
}) | ||
} finally { | ||
migrated++ | ||
store.close() | ||
} | ||
} | ||
|
||
onProgress(100, `Migrated ${datastores.length} dbs`) | ||
} | ||
|
||
module.exports = { | ||
version: 10, | ||
description: 'Migrates datastore-level keys to binary', | ||
migrate: (repoPath, repoOptions, onProgress) => { | ||
return process(repoPath, repoOptions, onProgress, keysToBinary) | ||
}, | ||
revert: (repoPath, repoOptions, onProgress) => { | ||
return process(repoPath, repoOptions, onProgress, keysToStrings) | ||
} | ||
} | ||
|
||
/** | ||
* @typedef {Error | undefined} Err | ||
* @typedef {Uint8Array|string} Key | ||
* @typedef {Uint8Array} Value | ||
* @typedef {{ type: 'del', key: Key } | { type: 'put', key: Key, value: Value }} Operation | ||
* | ||
* Uses the upgrade strategy from [email protected] - note we can't call the `.upgrade` command | ||
* directly because it will be removed in [email protected] and we can't guarantee users will | ||
* have migrated by then - e.g. they may jump from [email protected] straight to [email protected] | ||
* so we have to duplicate the code here. | ||
* | ||
* @param {import('interface-datastore').Datastore} db | ||
* @param {function (Err, Key, Value): Operation[]} fn | ||
*/ | ||
function withEach (db, fn) { | ||
function batch (operations, next) { | ||
const store = db.store('readwrite') | ||
const transaction = store.transaction | ||
let index = 0 | ||
let error | ||
|
||
transaction.onabort = () => next(error || transaction.error || new Error('aborted by user')) | ||
transaction.oncomplete = () => next() | ||
|
||
function loop () { | ||
var op = operations[index++] | ||
var key = op.key | ||
|
||
try { | ||
var req = op.type === 'del' ? store.delete(key) : store.put(op.value, key) | ||
} catch (err) { | ||
error = err | ||
transaction.abort() | ||
return | ||
} | ||
|
||
if (index < operations.length) { | ||
req.onsuccess = loop | ||
} | ||
} | ||
|
||
loop() | ||
} | ||
|
||
return new Promise((resolve, reject) => { | ||
const it = db.iterator() | ||
// raw keys and values only | ||
it._deserializeKey = it._deserializeValue = (data) => data | ||
next() | ||
|
||
function next () { | ||
it.next((err, key, value) => { | ||
if (err || key === undefined) { | ||
it.end((err2) => { | ||
if (err2) { | ||
reject(err2) | ||
return | ||
} | ||
|
||
resolve() | ||
}) | ||
|
||
return | ||
} | ||
|
||
batch(fn(key, value), next) | ||
}) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* eslint-env mocha */ | ||
/* eslint-disable max-nested-callbacks */ | ||
'use strict' | ||
|
||
const { expect } = require('aegir/utils/chai') | ||
|
||
const { createStore } = require('../../src/utils') | ||
const migration = require('../../migrations/migration-10') | ||
const Key = require('interface-datastore').Key | ||
const fromString = require('uint8arrays/from-string') | ||
const Level5 = require('level-5') | ||
const Level6 = require('level-6') | ||
|
||
const keys = { | ||
CIQCKN76QUQUGYCHIKGFE6V6P3GJ2W26YFFPQW6YXV7NFHH3QB2RI3I: 'hello', | ||
CIQKKLBWAIBQZOIS5X7E32LQAL6236OUKZTMHPQSFIXPWXNZHQOV7JQ: fromString('derp') | ||
} | ||
|
||
async function bootstrap (dir, backend, repoOptions) { | ||
const store = createStore(dir, backend, repoOptions) | ||
await store.open() | ||
|
||
for (const name of Object.keys(keys)) { | ||
await store.put(new Key(name), keys[name]) | ||
} | ||
|
||
await store.close() | ||
} | ||
|
||
async function validate (dir, backend, repoOptions) { | ||
const store = createStore(dir, backend, repoOptions) | ||
|
||
await store.open() | ||
|
||
for (const name of Object.keys(keys)) { | ||
const key = new Key(`/${name}`) | ||
|
||
expect(await store.has(key)).to.be.true(`Could not read key ${name}`) | ||
expect(store.get(key)).to.eventually.equal(keys[name], `Could not read value for key ${keys[name]}`) | ||
} | ||
|
||
await store.close() | ||
} | ||
|
||
function withLevel (repoOptions, levelImpl) { | ||
const stores = Object.keys(repoOptions.storageBackends) | ||
.filter(key => repoOptions.storageBackends[key].name === 'LevelDatastore') | ||
|
||
const output = { | ||
...repoOptions | ||
} | ||
|
||
stores.forEach(store => { | ||
// override version of level passed to datastore options | ||
output.storageBackendOptions[store] = { | ||
...output.storageBackendOptions[store], | ||
db: levelImpl | ||
} | ||
}) | ||
|
||
return output | ||
} | ||
|
||
module.exports = (setup, cleanup, repoOptions) => { | ||
describe('migration 10', function () { | ||
this.timeout(240 * 1000) | ||
let dir | ||
|
||
beforeEach(async () => { | ||
dir = await setup() | ||
}) | ||
|
||
afterEach(async () => { | ||
await cleanup(dir) | ||
}) | ||
|
||
describe('forwards', () => { | ||
beforeEach(async () => { | ||
for (const backend of Object.keys(repoOptions.storageBackends)) { | ||
await bootstrap(dir, backend, withLevel(repoOptions, Level5)) | ||
} | ||
}) | ||
|
||
it('should migrate keys and values forward', async () => { | ||
await migration.migrate(dir, withLevel(repoOptions, Level6), () => {}) | ||
|
||
for (const backend of Object.keys(repoOptions.storageBackends)) { | ||
await validate(dir, backend, withLevel(repoOptions, Level6)) | ||
} | ||
}) | ||
}) | ||
|
||
describe('backwards using [email protected]', () => { | ||
beforeEach(async () => { | ||
for (const backend of Object.keys(repoOptions.storageBackends)) { | ||
await bootstrap(dir, backend, withLevel(repoOptions, Level6)) | ||
} | ||
}) | ||
|
||
it('should migrate keys and values backward', async () => { | ||
await migration.revert(dir, withLevel(repoOptions, Level6), () => {}) | ||
|
||
for (const backend of Object.keys(repoOptions.storageBackends)) { | ||
await validate(dir, backend, withLevel(repoOptions, Level5)) | ||
} | ||
}) | ||
}) | ||
|
||
describe('backwards using [email protected]', () => { | ||
beforeEach(async () => { | ||
for (const backend of Object.keys(repoOptions.storageBackends)) { | ||
await bootstrap(dir, backend, withLevel(repoOptions, Level6)) | ||
} | ||
}) | ||
|
||
it('should migrate keys and values backward', async () => { | ||
await migration.revert(dir, withLevel(repoOptions, Level5), () => {}) | ||
|
||
for (const backend of Object.keys(repoOptions.storageBackends)) { | ||
await validate(dir, backend, withLevel(repoOptions, Level5)) | ||
} | ||
}) | ||
}) | ||
}) | ||
} |
Oops, something went wrong.