Skip to content
This repository was archived by the owner on Oct 1, 2021. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ jobs:
firefox: latest
script: npx aegir test -t browser -- --browsers FirefoxHeadless

- stage: test
name: sharness
os:
- linux
- osx
script: cd ./test/sharness && make

- stage: test
name: electron-main
os: osx
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Migration tool for JS IPFS Repo
# Migration tool for JS IPFS Repo <!-- omit in toc -->

[![Travis CI](https://flat.badgen.net/travis/ipfs/js-ipfs-repo-migrations)](https://travis-ci.com/ipfs/js-ipfs-repo-migrations)
[![codecov](https://codecov.io/gh/ipfs/js-ipfs-repo-migrations/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipfs-repo-migrations)
Expand All @@ -15,36 +15,41 @@

This package is inspired by the [go-ipfs repo migration tool](https://github.com/ipfs/fs-repo-migrations/)

## Lead Maintainer
## Lead Maintainer <!-- omit in toc -->

[Adam Uhlíř](https://github.com/auhau/)

## Table of Contents
## Table of Contents <!-- omit in toc -->

- [Background](#background)
- [Install](#install)
- [npm](#npm)
- [Use in Node.js](#use-in-nodejs)
- [Use in a browser with browserify, webpack or any other bundler](#use-in-a-browser-with-browserify-webpack-or-any-other-bundler)
- [Use in a browser Using a script tag](#use-in-a-browser-using-a-script-tag)
- [Usage](#usage)
- [API](#api)
- [`.migrate(path, toVersion, {ignoreLock, repoOptions, onProgress, isDryRun}) -> Promise<void>`](#migratepath-toversion-ignorelock-repooptions-onprogress-isdryrun---promisevoid)
- [`onProgress(migration, counter, totalMigrations)`](#onprogressmigration-counter-totalmigrations)
- [`.revert(path, toVersion, {ignoreLock, repoOptions, onProgress, isDryRun}) -> Promise<void>`](#revertpath-toversion-ignorelock-repooptions-onprogress-isdryrun---promisevoid)
- [`getLatestMigrationVersion() -> int`](#getlatestmigrationversion---int)
- [CLI](#cli)
- [Creating a new migration](#creating-a-new-migration)
- [Architecture of a migration](#architecture-of-a-migration)
- [`.migrate(repoPath, repoOptions)`](#migraterepopath-repooptions)
- [`.revert(repoPath, repoOptions)`](#revertrepopath-repooptions)
- [Browser vs. NodeJS environments](#browser-vs-nodejs-environments)
- [Guidelines](#guidelines)
- [Integration with js-ipfs](#integration-with-js-ipfs)
- [Tests](#tests)
- [Empty migrations](#empty-migrations)
- [Migrations matrix](#migrations-matrix)
- [Developer](#developer)
- [Module versioning notes](#module-versioning-notes)
- [Module versioning notes](#module-versioning-notes)
- [Contribute](#contribute)
- [License](#license)

## Background


As js-ipfs evolves and new technologies, algorithms and data structures are incorporated it is necessary to
enable users to transition between versions. Different versions of js-ipfs may expect a different IPFS repo structure or content (see: [IPFS repo spec](https://github.com/ipfs/specs/tree/master/repo), [JS implementation](https://github.com/ipfs/js-ipfs-repo) ).
So the IPFS repo is versioned, and this package provides a framework to create migrations to transition
Expand Down Expand Up @@ -87,10 +92,15 @@ const migrations = require('ipfs-repo-migrations')
const repoPath = 'some/repo/path'
const currentRepoVersion = 7
const latestVersion = migrations.getLatestMigrationVersion()
const repoOptions = {
... // the same storage backend/storage options passed to `ipfs-repo`
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is repoOptions still optional and we just need to make sure to pass it through if provided?

Copy link
Member Author

@achingbrain achingbrain Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been passed through by ipfs-repo since migrations were added.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readme api methods mention repoOptions as always optional, this is not the case as this change will throw errors. This doesn't matter for ipfs-repo since it is providing passing repoOptions to migrate and revert, but we should remove the optional jsdocs/readme as this is required.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the optional notes in the readme. Which is a bit odd because it makes the options object mandatory, but I think that's ok for now as we are the only consumer of this module and we pass an options object. When I re-PR this against master I'll move the repo options out of the options object to make the options optional again.


if(currentRepoVersion < latestVersion){
// Old repo! Lets migrate to latest version!
await migrations.migrate(repoPath, latestVersion)
await migrations.migrate(repoPath, latestVersion, {
repoOptions
})
}
```

Expand All @@ -106,9 +116,9 @@ Executes a forward migration to a specific version, or to the latest version if

* `path` (string, mandatory) - path to the repo to be migrated
* `toVersion` (int, mandatory) - version to which the repo should be migrated.
* `options` (object, optional) - options for the migration
* `options` (object, mandatory) - options for the migration
* `options.ignoreLock` (bool, optional) - if true will not lock the repo when applying migrations. Use with caution.
* `options.repoOptions` (object, optional) - options that are passed to migrations, that use them to construct the datastore. (options are the same as for IPFSRepo).
* `options.repoOptions` (object, mandatory) - options that are passed to migrations, that use them to construct the datastore. (options are the same as for IPFSRepo).
* `options.onProgress` (function, optional) - callback that is called after finishing execution of each migration to report progress.
* `options.isDryRun` (bool, optional) - flag that indicates if it is a dry run that should give the same output as running a migration but without making any actual changes.

Expand All @@ -129,9 +139,9 @@ Executes backward migration to a specific version.

* `path` (string, mandatory) - path to the repo to be reverted
* `toVersion` (int, mandatory) - version to which the repo should be reverted to.
* `options` (object, optional) - options for the reversion
* `options` (object, mandatory) - options for the reversion
* `options.ignoreLock` (bool, optional) - if true will not lock the repo when applying migrations. Use with caution.
* `options.repoOptions` (object, optional) - options that are passed to migrations, that use them to construct the datastore. (options are the same as for IPFSRepo).
* `options.repoOptions` (object, mandatory) - options that are passed to migrations, that use them to construct the datastore. (options are the same as for IPFSRepo).
* `options.onProgress` (function, optional) - callback that is called after finishing execution of each migration to report progress.
* `options.isDryRun` (bool, optional) - flag that indicates if it is a dry run that should give the same output as running a migration but without making any actual changes.

Expand Down
8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@
"main": "src/index.js",
"browser": {
"./src/repo/lock.js": "./src/repo/lock-memory.js",
"./src/repo/default-root-options.js": "./src/repo/ldefault-root-options.browser.js",
"datastore-fs": "datastore-level"
},
"bin": {
"jsipfs-migrations": "./src/cli.js"
},
"repository": {
"type": "git",
"url": "https://github.com/ipfs/js-ipfs-repo-migrations.git"
Expand Down Expand Up @@ -53,9 +51,7 @@
"debug": "^4.1.0",
"interface-datastore": "^1.0.2",
"multibase": "^1.0.1",
"proper-lockfile": "^4.1.1",
"yargs": "^15.3.1",
"yargs-promise": "^1.1.0"
"proper-lockfile": "^4.1.1"
},
"devDependencies": {
"aegir": "^23.0.0",
Expand Down
62 changes: 0 additions & 62 deletions src/cli.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ function reportingClosure (action) {
process.stdout.write(`${chalk.green(`[${currentlyMigrated}/${totalToMigrate}]`)} Successfully ${action} ${chalk.bold(migration.version)}: ${migration.description}\n`)
}

async function migrate ({ repoPath, migrations, to, dry, revertOk }) {
async function migrate ({ repoPath, repoOptions, migrations, to, dry, revertOk }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file isn't used anymore since the cli.js is deleted, this can go away right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, have removed.

repoPath = repoPath || process.env.IPFS_PATH || path.join(os.homedir(), '.jsipfs')
migrations = migrations === undefined ? require('../migrations') : require(migrations)

if (!to) {
to = migrator.getLatestMigrationVersion(migrations)
}

const version = await repoVersion.getVersion(repoPath)
const version = await repoVersion.getVersion(repoPath, repoOptions)

let action
if (dry) {
Expand Down
15 changes: 15 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,18 @@ class InvalidValueError extends Error {

InvalidValueError.code = 'ERR_INVALID_VALUE'
exports.InvalidValueError = InvalidValueError

/**
* Exception raised when config is not passed.
*/
class MissingRepoOptionsError extends Error {
constructor (message) {
super(message)
this.name = 'MissingRepoOptionsError'
this.code = 'ERR_MISSING_REPO_OPTIONS'
this.message = message
}
}

MissingRepoOptionsError.code = 'ERR_MISSING_REPO_OPTIONS'
exports.MissingRepoOptionsError = MissingRepoOptionsError
10 changes: 5 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function migrate (path, toVersion, { ignoreLock = false, repoOptions, onPr
throw new errors.InvalidValueError('Version has to be positive integer!')
}

const currentVersion = await repoVersion.getVersion(path)
const currentVersion = await repoVersion.getVersion(path, repoOptions)

if (currentVersion === toVersion) {
log('Nothing to migrate.')
Expand Down Expand Up @@ -105,7 +105,7 @@ async function migrate (path, toVersion, { ignoreLock = false, repoOptions, onPr
log(`Migrating to version ${migration.version} finished`)
}

if (!isDryRun) await repoVersion.setVersion(path, toVersion || getLatestMigrationVersion(migrations))
if (!isDryRun) await repoVersion.setVersion(path, toVersion || getLatestMigrationVersion(migrations), repoOptions)
log('Repo successfully migrated ', toVersion !== undefined ? `to version ${toVersion}!` : 'to latest version!')
} finally {
if (!isDryRun && !ignoreLock) await lock.close()
Expand Down Expand Up @@ -146,7 +146,7 @@ async function revert (path, toVersion, { ignoreLock = false, repoOptions, onPro
throw new errors.InvalidValueError('Version has to be positive integer!')
}

const currentVersion = await repoVersion.getVersion(path)
const currentVersion = await repoVersion.getVersion(path, repoOptions)
if (currentVersion === toVersion) {
log('Nothing to revert.')
return
Expand Down Expand Up @@ -182,7 +182,7 @@ async function revert (path, toVersion, { ignoreLock = false, repoOptions, onPro
} catch (e) {
const lastSuccessfullyRevertedVersion = migration.version
log(`An exception was raised during execution of migration. Setting the repo's version to last successfully reverted version: ${lastSuccessfullyRevertedVersion}`)
await repoVersion.setVersion(path, lastSuccessfullyRevertedVersion)
await repoVersion.setVersion(path, lastSuccessfullyRevertedVersion, repoOptions)

e.message = `During reversion to version ${migration.version} exception was raised: ${e.message}`
throw e
Expand All @@ -192,7 +192,7 @@ async function revert (path, toVersion, { ignoreLock = false, repoOptions, onPro
log(`Reverting to version ${migration.version} finished`)
}

if (!isDryRun) await repoVersion.setVersion(path, toVersion)
if (!isDryRun) await repoVersion.setVersion(path, toVersion, repoOptions)
log(`All migrations successfully reverted to version ${toVersion}!`)
} finally {
if (!isDryRun && !ignoreLock) await lock.close()
Expand Down
27 changes: 17 additions & 10 deletions src/repo/init.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
'use strict'

const Datastore = require('datastore-fs')
const log = require('debug')('repo-migrations:repo:init')
const { CONFIG_KEY, VERSION_KEY, getDatastoreAndOptions } = require('../utils')
const { MissingRepoOptionsError } = require('../errors')

const Key = require('interface-datastore').Key

const versionKey = new Key('/version')
const configKey = new Key('/config')
exports.isRepoInitialized = async function isRepoInitialized (path, repoOptions) {
if (!repoOptions) {
throw new MissingRepoOptionsError('Please pass repo options when trying to open a repo')
}

exports.isRepoInitialized = async function isRepoInitialized (path) {
let root
try {
root = new Datastore(path, { extension: '', createIfMissing: false })
const {
StorageBackend,
storageOptions
} = getDatastoreAndOptions(repoOptions, 'root')

root = new StorageBackend(path, storageOptions)
await root.open()
const versionCheck = await root.has(versionKey)
const configCheck = await root.has(configKey)
const versionCheck = await root.has(VERSION_KEY)
const configCheck = await root.has(CONFIG_KEY)
if (!versionCheck || !configCheck) {
log(`Version entry present: ${versionCheck}`)
log(`Config entry present: ${configCheck}`)
Expand All @@ -26,6 +31,8 @@ exports.isRepoInitialized = async function isRepoInitialized (path) {
log('While checking if repo is initialized error was thrown: ' + e.message)
return false
} finally {
if (root !== undefined) await root.close()
if (root !== undefined) {
await root.close()
}
}
}
45 changes: 31 additions & 14 deletions src/repo/version.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
'use strict'

const { Buffer } = require('buffer')
const errors = require('../errors')
const repoInit = require('./init')
const Datastore = require('datastore-fs')

const Key = require('interface-datastore').Key

const versionKey = new Key('version')
const { MissingRepoOptionsError, NotInitializedRepoError } = require('../errors')
const { VERSION_KEY, getDatastoreAndOptions } = require('../utils')

exports.getVersion = getVersion

Expand All @@ -17,17 +13,27 @@ exports.getVersion = getVersion
* even in case of change of repo's versioning.
*
* @param {string} path
* @param {Object} repoOptions Options used to create a repo, the same as pased to ipfs-repo
* @returns {Promise<int>}
*/
async function getVersion (path) {
if (!(await repoInit.isRepoInitialized(path))) {
throw new errors.NotInitializedRepoError(`Repo in path ${path} is not initialized!`)
async function getVersion (path, repoOptions) {
if (!(await repoInit.isRepoInitialized(path, repoOptions))) {
throw new NotInitializedRepoError(`Repo in path ${path} is not initialized!`)
}

const store = new Datastore(path, { extension: '', createIfMissing: false })
if (!repoOptions) {
throw new MissingRepoOptionsError('Please pass repo options when trying to open a repo')
}

const {
StorageBackend,
storageOptions
} = getDatastoreAndOptions(repoOptions, 'root')

const store = new StorageBackend(path, storageOptions)
await store.open()

const version = parseInt(await store.get(versionKey))
const version = parseInt(await store.get(VERSION_KEY))
await store.close()

return version
Expand All @@ -38,12 +44,23 @@ async function getVersion (path) {
*
* @param {string} path
* @param {int} version
* @param {Object} repoOptions Options used to create a repo, the same as pased to ipfs-repo
* @returns {Promise<void>}
*/
async function setVersion (path, version) {
const store = new Datastore(path, { extension: '', createIfMissing: false })
async function setVersion (path, version, repoOptions) {
if (!repoOptions) {
throw new MissingRepoOptionsError('Please pass repo options when trying to open a repo')
}

const {
StorageBackend,
storageOptions
} = getDatastoreAndOptions(repoOptions, 'root')

const store = new StorageBackend(path, storageOptions)
await store.open()
await store.put(versionKey, Buffer.from(String(version)))

await store.put(VERSION_KEY, Buffer.from(String(version)))
await store.close()
}

Expand Down
Loading