├── .npmignore ├── test ├── fixtures │ ├── fake-dump-dir │ │ └── databasename │ │ │ └── .gitignore │ └── invalid-dump-dir │ │ ├── databasename │ │ └── .gitignore │ │ └── secondfolder │ │ └── .gitignore ├── mocha.opts ├── getConnectionInfo.coffee ├── makeDumpCommand.coffee ├── makeFindCommand.coffee └── makeRestoreCommand.coffee ├── .travis.yml ├── .gitignore ├── .tm_properties ├── Makefile ├── package.json ├── LICENSE ├── README.md └── src └── utils.coffee /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/fake-dump-dir/databasename/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/invalid-dump-dir/databasename/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/invalid-dump-dir/secondfolder/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script 2 | --reporter spec -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib 3 | /mongo-utils.tmproj 4 | /npm-debug.log -------------------------------------------------------------------------------- /.tm_properties: -------------------------------------------------------------------------------- 1 | excludeDirectories = "{.git,node_modules}" 2 | excludeInFolderSearch = "{excludeDirectories,lib}" 3 | 4 | includeFiles = "{.gitignore,.npmignore,.travis.yml}" 5 | 6 | [ attr.untitled ] 7 | fileType = 'source.coffee' -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mkdir -p lib 3 | node_modules/.bin/coffee --compile -m --output lib/ src/ 4 | 5 | watch: 6 | node_modules/.bin/coffee --watch --compile --output lib/ src/ 7 | 8 | test: 9 | node_modules/.bin/mocha 10 | 11 | .PHONY: test -------------------------------------------------------------------------------- /test/getConnectionInfo.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | 3 | getConnectionInfo = require("../").getConnectionInfo 4 | 5 | describe "getConnectionInfo", -> 6 | it "makes port default to 27017", -> 7 | parsed = getConnectionInfo("somedb") 8 | assert.equal parsed.port, 27017 9 | it "makes protocol default to mongodb", -> 10 | parsed = getConnectionInfo("somedb") 11 | assert.equal parsed.protocol, "mongodb" 12 | it "makes hostname default to localhost", -> 13 | parsed = getConnectionInfo("somedb") 14 | assert.equal parsed.hostname, "localhost" 15 | it "gives host as [hostname]:[port]", -> 16 | parsed = getConnectionInfo("somedb") 17 | assert.equal parsed.host, "localhost:27017" 18 | -------------------------------------------------------------------------------- /test/makeDumpCommand.coffee: -------------------------------------------------------------------------------- 1 | assert = require 'assert' 2 | 3 | connString = 'mongodb://heroku:flk3ungh0x3anflx1bab@staff.mongohq.com:10092/app1321916260066' 4 | expectedCommand = "mongodump '--db' 'app1321916260066' '--host' 'staff.mongohq.com:10092' '--username' 'heroku' '--password' 'flk3ungh0x3anflx1bab' '--out' '/dumps/mongodb/some-backup'" 5 | dirName = '/dumps/mongodb/some-backup' 6 | 7 | utils = require '../' 8 | 9 | describe 'makeDumpCommand', -> 10 | it 'converts query string and dirname to a mongodump command', -> 11 | command = utils.makeDumpCommand connString, dirName 12 | assert.equal command, expectedCommand 13 | it 'throws an error if no dirName is given', -> 14 | try 15 | utils.makeDumpCommand connString 16 | catch error 17 | return assert.ok true 18 | assert.ok false, 'it did not throw an error.' 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-utils", 3 | "description": "Friendly interface to mongodump and mongorestore commands.", 4 | "keywords": [ 5 | "mongodb", 6 | "mongodump", 7 | "mongorestore", 8 | "heroku", 9 | "mongohq" 10 | ], 11 | "version": "0.5.0", 12 | "directories": { 13 | "lib": "./lib" 14 | }, 15 | "main": "lib/utils.js", 16 | "dependencies": { 17 | "heroku": "0.1.x", 18 | "mongoson": "0.2.x" 19 | }, 20 | "devDependencies": { 21 | "coffee-script": "1.6.x", 22 | "mocha": "~1.9.0" 23 | }, 24 | "engines": { 25 | "node": ">=0.8", 26 | "npm": ">=1.1" 27 | }, 28 | "optionalDependencies": {}, 29 | "author": "Meryn Stol ", 30 | "homepage": "https://github.com/meryn/mongo-utils", 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/meryn/mongo-utils.git" 34 | }, 35 | "scripts": { 36 | "prepublish": "npm test", 37 | "pretest": "make build", 38 | "test": "make test" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Meryn Stol 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/makeFindCommand.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | utils = require "../" 3 | 4 | query = ownerId: "6ac4f70f-fe05-4a7d-bba8-99d6fbe62261" 5 | 6 | describe "makeFindCommand", -> 7 | it "works with a sole query object", -> 8 | generated = utils.makeFindCommand "nodes", query 9 | expected = """ 10 | db.nodes.find({"ownerId":"6ac4f70f-fe05-4a7d-bba8-99d6fbe62261"}) 11 | """ 12 | assert.equal generated, expected 13 | it "works when options object has 'sort' field", -> 14 | generated = utils.makeFindCommand "nodes", query, sort: addedAt: 1 15 | expected = """ 16 | db.nodes.find({"ownerId":"6ac4f70f-fe05-4a7d-bba8-99d6fbe62261"}).sort({"addedAt":1}) 17 | """ 18 | assert.equal generated, expected 19 | it "works when options object has 'fields' field", -> 20 | generated = utils.makeFindCommand "nodes", query, 21 | fields: 22 | value: 1 23 | addedAt: 1 24 | changedAt: 1 25 | expected = """ 26 | db.nodes.find({"ownerId":"6ac4f70f-fe05-4a7d-bba8-99d6fbe62261"},{"value":1,"addedAt":1,"changedAt":1}) 27 | """ 28 | assert.equal generated, expected -------------------------------------------------------------------------------- /test/makeRestoreCommand.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | path = require "path" 3 | 4 | fixturesDir = path.resolve __dirname, "fixtures" 5 | 6 | connString = "mongodb://heroku:flk3ungh0x3anflx1bab@staff.mongohq.com:10092/app1321916260066" 7 | expectedCommand = "mongorestore '--db' 'app1321916260066' '--host' 'staff.mongohq.com:10092' '--username' 'heroku' '--password' 'flk3ungh0x3anflx1bab' '--drop' '#{fixturesDir}/fake-dump-dir/databasename'" 8 | 9 | utils = require "../" 10 | 11 | describe "makeRestoreCommand", -> 12 | it "converts query string and dirname to a mongorestore command", -> 13 | dirName = "#{fixturesDir}/fake-dump-dir" 14 | command = utils.makeRestoreCommand connString, dirName 15 | assert.equal command, expectedCommand 16 | it "throws an error if source directory does not exist", -> 17 | dirName = "#{fixturesDir}/not-existing" 18 | try 19 | utils.makeDumpCommand connString 20 | catch error 21 | return assert.ok true 22 | assert.ok false, "it did not throw an error." 23 | it "throws an error if source directory contains more than subdirectory", -> 24 | dirName = "#{fixturesDir}/invalid-dump-dir" 25 | try 26 | utils.makeDumpCommand connString 27 | catch error 28 | return assert.ok true 29 | assert.ok false, "it did not throw an error." 30 | it "throws an error if no dirName is given", -> 31 | try 32 | utils.makeDumpCommand connString 33 | catch error 34 | return assert.ok true 35 | assert.ok false, "it did not throw an error." 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongo-utils [![Build Status](https://travis-ci.org/meryn/mongo-utils.png?branch=master)](https://travis-ci.org/meryn/mongo-utils) 2 | 3 | mongo-utils provides a friendly interface to MongoDB's mongodump and mongorestore commands, as well as some utility functions. 4 | 5 | ## Synchronous functions 6 | 7 | ```coffee 8 | utils.parseConnectionString connectionString # mongo connection options object 9 | utils.makeRestoreCommand connectionString, sourceDir # mongorestore ... 10 | utils.makeDumpCommand connectionString, targetDir # mongodump ... 11 | ``` 12 | 13 | ## Asynchronous functions 14 | 15 | These functions simply wrap [`child_process.exec`](http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) in a convenient interface. There is absolutely no validation happening. Thus, the absence of an error (as `err` argument) does not mean the dump or restore succeeded. 16 | 17 | I advise to inspect `stdout` and `stderr` yourself if you use this module for any important dumps or restores, or verify the results otherwise. 18 | 19 | ```coffee 20 | utils.dumpDatabase connectionString, dirName, (err, stdout, stderr) -> 21 | utils.dumpHerokuMongoHQDatabase appName, dirName, (err, stdout, stderr) -> 22 | utils.restoreDatabase connectionString, dirName, (err, stdout, stderr) -> 23 | utils.dumpHerokuMongoHQDatabase appName, dirName, (err, stdout, stderr) -> 24 | ``` 25 | 26 | The heroku-mongohq functions look up the `MONGOHQ_URL` environment variable of your Heroku app, using the [heroku](https://github.com/toots/node-heroku) module. 27 | 28 | ## Configuration 29 | 30 | mongo-utils logs some messages to allow you to see what's going on behind the scenes, primarily when doing the using the dump or restore commands. To see what's being logged, you may assign a log function which takes a single `message` argument to `utils.log`. By default, `utils.log` is a noop. 31 | 32 | ```coffee 33 | utils = require "mongo-utils" 34 | utils.log = (msg) -> console.log msg 35 | ``` 36 | 37 | ## Prerequisites 38 | 39 | For the commands to work, you need to have `mongorestore` and `mongodump` in your path. 40 | The Heroku-specific commands require a `HEROKU_API_KEY` environment variable to be set. 41 | 42 | ## License 43 | 44 | mongo-utils is released under the [MIT License](http://opensource.org/licenses/MIT). 45 | Copyright (c) 2013 Meryn Stol -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | {exec} = require('child_process') 2 | parseURL = require('url').parse 3 | heroku = require 'heroku' 4 | fs = require 'fs' 5 | 6 | MSON = require 'mongoson' 7 | 8 | module.exports = utils = {} 9 | utils.log = -> 10 | 11 | utils.loggedExec = (command, next) -> 12 | utils.log "> #{command}" 13 | exec command, next 14 | 15 | utils.parseConnectionString = (connectionString) -> 16 | parsedURL = parseURL connectionString 17 | info = {} 18 | info.hostname = parsedURL.hostname 19 | info.port = parsedURL.port 20 | info.host = if info.port then "#{info.hostname}:#{info.port}" else info.hostname 21 | info.database = info.db = parsedURL.pathname && parsedURL.pathname.replace(/\//g, '') 22 | [info.username,info.password] = parsedURL.auth.split(':') if parsedURL.auth 23 | info 24 | 25 | utils.getConnectionInfo = (connectionString) -> 26 | info = utils.parseConnectionString connectionString 27 | info = {} 28 | info.protocol = info.protocol or "mongodb" 29 | info.hostname = info.hostname or "localhost" 30 | info.port = info.port or 27017 31 | info.host = if info.port then "#{info.hostname}:#{info.port}" else info.hostname 32 | info 33 | 34 | utils.dumpDatabase = (connectionString, dirName, next) -> 35 | dumpCommand = utils.makeDumpCommand connectionString, dirName 36 | utils.loggedExec dumpCommand, (err, stdOut, stdErr) -> 37 | return next err if err 38 | return next null, stdOut, stdErr 39 | 40 | utils.restoreDatabase = (connectionString, dirName, next) -> 41 | restoreCommand = utils.makeRestoreCommand connectionString, dirName 42 | utils.loggedExec restoreCommand, (err, stdOut, stdErr) -> 43 | return next err if err 44 | return next null, stdOut, stdErr 45 | 46 | utils.makeDumpCommand = (connectionString, dirName) -> 47 | throw "No target directory given." unless dirName 48 | throw "Target directory must be a string" unless typeof dirName is "string" 49 | connectionParameters = utils.parseConnectionString connectionString 50 | commandOptions = makeCommandOptions connectionParameters 51 | commandOptions.out = dirName 52 | commandArguments = makeCommandArguments commandOptions 53 | argumentString = makeArgumentString commandArguments 54 | "mongodump#{argumentString}" 55 | 56 | utils.makeRestoreCommand = (connectionString, dirName) -> 57 | throw "No source directory given." unless dirName 58 | throw "Source directory must be a string" unless typeof dirName is "string" 59 | actualDirName = utils.findDumpDirName dirName 60 | utils.log "Using #{actualDirName}" 61 | connectionParameters = utils.parseConnectionString connectionString 62 | commandOptions = makeCommandOptions connectionParameters 63 | commandOptions.drop = true 64 | commandArguments = makeCommandArguments commandOptions, actualDirName 65 | argumentString = makeArgumentString commandArguments 66 | "mongorestore#{argumentString}" 67 | 68 | utils.findDumpDirName = (dirName) -> 69 | dirCount = 0 70 | for entryName in fs.readdirSync dirName 71 | if fs.statSync("#{dirName}/#{entryName}").isDirectory() 72 | dirCount += 1 73 | lastDirName = entryName 74 | switch dirCount 75 | when 0 then return dirName # a proper dump dir 76 | when 1 then return dirName + "/" + lastDirName # assume this one is proper 77 | else throw new Error "#{dirName} contains multiple directories." 78 | 79 | utils.dumpHerokuMongoHQDatabase = (appName, dirName, next) -> 80 | utils.findHerokuMongoHQURL appName, (err, url) -> 81 | utils.log "Using #{url}" 82 | return next err if err 83 | return utils.dumpDatabase url, dirName, next 84 | 85 | utils.restoreHerokuMongoHQDatabase = (appName, dirName, next) -> 86 | utils.findHerokuMongoHQURL appName, (err, url) -> 87 | utils.log "Using #{url}" 88 | return next err if err 89 | return utils.restoreDatabase url, dirName, next 90 | 91 | utils.findHerokuMongoHQURL = (appName, next) -> 92 | return next new Error "Cannot find environment variable HEROKU_API_KEY" unless process.env['HEROKU_API_KEY'] 93 | herokuClient = new heroku.Heroku key: process.env['HEROKU_API_KEY'] 94 | herokuClient.get_config_vars appName, (err, herokuConfig) -> 95 | return next err if err 96 | return next new Error "Cannot find MONGOHQ_URL in config of #{appName}." unless herokuConfig.MONGOHQ_URL 97 | return next null, herokuConfig.MONGOHQ_URL 98 | 99 | utils.makeFindCommand = (collectionName, query, options = {}) -> 100 | command = "db.#{collectionName}.find(#{MSON.stringify query}" 101 | command += ",#{JSON.stringify options.fields}" if options.fields 102 | command += ")" 103 | command += ".sort(#{JSON.stringify options.sort})" if options.sort 104 | command 105 | 106 | makeCommandOptions = (connParams) -> 107 | options = {} 108 | options.db = connParams.db 109 | options.host = connParams.host unless connParams.host is "localhost" 110 | options.username = connParams.username if connParams.username 111 | options.password = connParams.password if connParams.password 112 | options 113 | 114 | makeCommandArguments = (options, object) -> 115 | args = [] 116 | for name, value of options 117 | args.push "--#{name}" unless value is false 118 | args.push "#{value}" unless value is true or value is false 119 | args.push object if object 120 | args 121 | 122 | makeArgumentString = (args) -> 123 | str = "" 124 | str += " '#{arg}'" for arg in args 125 | str --------------------------------------------------------------------------------