• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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