1'use strict' 2const path = require('path') 3const fs = require('graceful-fs') 4const Bluebird = require('bluebird') 5const rimraf = Bluebird.promisify(require('rimraf')) 6const mkdirp = Bluebird.promisify(require('gentle-fs').mkdir) 7const lstat = Bluebird.promisify(fs.lstat) 8const readdir = Bluebird.promisify(fs.readdir) 9const symlink = Bluebird.promisify(fs.symlink) 10const gentlyRm = Bluebird.promisify(require('../../utils/gently-rm')) 11const moduleStagingPath = require('../module-staging-path.js') 12const move = require('move-concurrently') 13const moveOpts = {fs: fs, Promise: Bluebird, maxConcurrency: 4} 14const getRequested = require('../get-requested.js') 15const log = require('npmlog') 16const packageId = require('../../utils/package-id.js') 17 18module.exports = function (staging, pkg, log) { 19 log.silly('finalize', pkg.realpath) 20 21 const extractedTo = moduleStagingPath(staging, pkg) 22 23 const delpath = path.join(path.dirname(pkg.realpath), '.' + path.basename(pkg.realpath) + '.DELETE') 24 let movedDestAway = false 25 26 const requested = pkg.package._requested || getRequested(pkg) 27 if (requested.type === 'directory') { 28 const relative = path.relative(path.dirname(pkg.path), pkg.realpath) 29 return makeParentPath(pkg.path) 30 .then(() => symlink(relative, pkg.path, 'junction')) 31 .catch((ex) => { 32 return rimraf(pkg.path).then(() => symlink(relative, pkg.path, 'junction')) 33 }) 34 } else { 35 return makeParentPath(pkg.realpath) 36 .then(moveStagingToDestination) 37 .then(restoreOldNodeModules) 38 .catch((err) => { 39 if (movedDestAway) { 40 return rimraf(pkg.realpath).then(moveOldDestinationBack).then(() => { 41 throw err 42 }) 43 } else { 44 throw err 45 } 46 }) 47 .then(() => rimraf(delpath)) 48 } 49 50 function makeParentPath (dir) { 51 return mkdirp(path.dirname(dir)) 52 } 53 54 function moveStagingToDestination () { 55 return destinationIsClear() 56 .then(actuallyMoveStaging) 57 .catch(() => moveOldDestinationAway().then(actuallyMoveStaging)) 58 } 59 60 function destinationIsClear () { 61 return lstat(pkg.realpath).then(() => { 62 throw new Error('destination exists') 63 }, () => {}) 64 } 65 66 function actuallyMoveStaging () { 67 return move(extractedTo, pkg.realpath, moveOpts) 68 } 69 70 function moveOldDestinationAway () { 71 return rimraf(delpath).then(() => { 72 return move(pkg.realpath, delpath, moveOpts) 73 }).then(() => { movedDestAway = true }) 74 } 75 76 function moveOldDestinationBack () { 77 return move(delpath, pkg.realpath, moveOpts).then(() => { movedDestAway = false }) 78 } 79 80 function restoreOldNodeModules () { 81 if (!movedDestAway) return 82 return readdir(path.join(delpath, 'node_modules')).catch(() => []).then((modules) => { 83 if (!modules.length) return 84 return mkdirp(path.join(pkg.realpath, 'node_modules')).then(() => Bluebird.map(modules, (file) => { 85 const from = path.join(delpath, 'node_modules', file) 86 const to = path.join(pkg.realpath, 'node_modules', file) 87 return move(from, to, moveOpts) 88 })) 89 }) 90 } 91} 92 93module.exports.rollback = function (top, staging, pkg) { 94 return Bluebird.try(() => { 95 const requested = pkg.package._requested || getRequested(pkg) 96 if (requested && requested.type === 'directory') return Promise.resolve() 97 // strictly speaking rolling back a finalize should ONLY remove module that 98 // was being finalized, not any of the things under it. But currently 99 // those modules are guaranteed to be useless so we may as well remove them too. 100 // When/if we separate `commit` step and can rollback to previous versions 101 // of upgraded modules then we'll need to revisit this… 102 return gentlyRm(pkg.path, false, top).catch((err) => { 103 log.warn('rollback', `Rolling back ${packageId(pkg)} failed (this is probably harmless): ${err.message ? err.message : err}`) 104 }) 105 }) 106} 107