1'use strict' 2 3const path = require('path') 4const fs = require('graceful-fs') 5const chain = require('slide').chain 6const mkdir = require('./mkdir.js') 7const rm = require('./rm.js') 8const inferOwner = require('infer-owner') 9const chown = require('./chown.js') 10 11exports = module.exports = { 12 link: link, 13 linkIfExists: linkIfExists 14} 15 16function linkIfExists (from, to, opts, cb) { 17 opts.currentIsLink = false 18 opts.currentExists = false 19 fs.stat(from, function (er) { 20 if (er) return cb() 21 fs.readlink(to, function (er, fromOnDisk) { 22 if (!er || er.code !== 'ENOENT') { 23 opts.currentExists = true 24 } 25 // if the link already exists and matches what we would do, 26 // we don't need to do anything 27 if (!er) { 28 opts.currentIsLink = true 29 var toDir = path.dirname(to) 30 var absoluteFrom = path.resolve(toDir, from) 31 var absoluteFromOnDisk = path.resolve(toDir, fromOnDisk) 32 opts.currentTarget = absoluteFromOnDisk 33 if (absoluteFrom === absoluteFromOnDisk) return cb() 34 } 35 link(from, to, opts, cb) 36 }) 37 }) 38} 39 40function resolveIfSymlink (maybeSymlinkPath, cb) { 41 fs.lstat(maybeSymlinkPath, function (err, stat) { 42 if (err) return cb.apply(this, arguments) 43 if (!stat.isSymbolicLink()) return cb(null, maybeSymlinkPath) 44 fs.readlink(maybeSymlinkPath, cb) 45 }) 46} 47 48function ensureFromIsNotSource (from, to, cb) { 49 resolveIfSymlink(from, function (err, fromDestination) { 50 if (err) return cb.apply(this, arguments) 51 if (path.resolve(path.dirname(from), fromDestination) === path.resolve(to)) { 52 return cb(new Error('Link target resolves to the same directory as link source: ' + to)) 53 } 54 cb.apply(this, arguments) 55 }) 56} 57 58function link (from, to, opts, cb) { 59 to = path.resolve(to) 60 opts.base = path.dirname(to) 61 var absTarget = path.resolve(opts.base, from) 62 var relativeTarget = path.relative(opts.base, absTarget) 63 var target = opts.absolute ? absTarget : relativeTarget 64 65 const tasks = [ 66 [ensureFromIsNotSource, absTarget, to], 67 [fs, 'stat', absTarget], 68 [clobberLinkGently, from, to, opts], 69 [mkdir, path.dirname(to)], 70 [fs, 'symlink', target, to, 'junction'] 71 ] 72 73 if (chown.selfOwner.uid !== 0) { 74 chain(tasks, cb) 75 } else { 76 inferOwner(to).then(owner => { 77 tasks.push([chown, to, owner.uid, owner.gid]) 78 chain(tasks, cb) 79 }) 80 } 81} 82 83exports._clobberLinkGently = clobberLinkGently 84function clobberLinkGently (from, to, opts, cb) { 85 if (opts.currentExists === false) { 86 // nothing to clobber! 87 opts.log.silly('gently link', 'link does not already exist', { 88 link: to, 89 target: from 90 }) 91 return cb() 92 } 93 94 if (!opts.clobberLinkGently || 95 opts.force === true || 96 !opts.gently || 97 typeof opts.gently !== 'string') { 98 opts.log.silly('gently link', 'deleting existing link forcefully', { 99 link: to, 100 target: from, 101 force: opts.force, 102 gently: opts.gently, 103 clobberLinkGently: opts.clobberLinkGently 104 }) 105 return rm(to, opts, cb) 106 } 107 108 if (!opts.currentIsLink) { 109 opts.log.verbose('gently link', 'cannot remove, not a link', to) 110 // don't delete. it'll fail with EEXIST when it tries to symlink. 111 return cb() 112 } 113 114 if (opts.currentTarget.indexOf(opts.gently) === 0) { 115 opts.log.silly('gently link', 'delete existing link', to) 116 return rm(to, opts, cb) 117 } else { 118 opts.log.verbose('gently link', 'refusing to delete existing link', { 119 link: to, 120 currentTarget: opts.currentTarget, 121 newTarget: from, 122 gently: opts.gently 123 }) 124 return cb() 125 } 126} 127