• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2// wrapper around mkdirp for tar's needs.
3
4// TODO: This should probably be a class, not functionally
5// passing around state in a gazillion args.
6
7const mkdirp = require('mkdirp')
8const fs = require('fs')
9const path = require('path')
10const chownr = require('chownr')
11const normPath = require('./normalize-windows-path.js')
12
13class SymlinkError extends Error {
14  constructor (symlink, path) {
15    super('Cannot extract through symbolic link')
16    this.path = path
17    this.symlink = symlink
18  }
19
20  get name () {
21    return 'SylinkError'
22  }
23}
24
25class CwdError extends Error {
26  constructor (path, code) {
27    super(code + ': Cannot cd into \'' + path + '\'')
28    this.path = path
29    this.code = code
30  }
31
32  get name () {
33    return 'CwdError'
34  }
35}
36
37const cGet = (cache, key) => cache.get(normPath(key))
38const cSet = (cache, key, val) => cache.set(normPath(key), val)
39
40const checkCwd = (dir, cb) => {
41  fs.stat(dir, (er, st) => {
42    if (er || !st.isDirectory())
43      er = new CwdError(dir, er && er.code || 'ENOTDIR')
44    cb(er)
45  })
46}
47
48module.exports = (dir, opt, cb) => {
49  dir = normPath(dir)
50
51  // if there's any overlap between mask and mode,
52  // then we'll need an explicit chmod
53  const umask = opt.umask
54  const mode = opt.mode | 0o0700
55  const needChmod = (mode & umask) !== 0
56
57  const uid = opt.uid
58  const gid = opt.gid
59  const doChown = typeof uid === 'number' &&
60    typeof gid === 'number' &&
61    ( uid !== opt.processUid || gid !== opt.processGid )
62
63  const preserve = opt.preserve
64  const unlink = opt.unlink
65  const cache = opt.cache
66  const cwd = normPath(opt.cwd)
67
68  const done = (er, created) => {
69    if (er)
70      cb(er)
71    else {
72      cSet(cache, dir, true)
73      if (created && doChown)
74        chownr(created, uid, gid, er => done(er))
75      else if (needChmod)
76        fs.chmod(dir, mode, cb)
77      else
78        cb()
79    }
80  }
81
82  if (cache && cGet(cache, dir) === true)
83    return done()
84
85  if (dir === cwd)
86    return checkCwd(dir, done)
87
88  if (preserve)
89    return mkdirp(dir, mode, done)
90
91  const sub = normPath(path.relative(cwd, dir))
92  const parts = sub.split('/')
93  mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done)
94}
95
96const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => {
97  if (!parts.length)
98    return cb(null, created)
99  const p = parts.shift()
100  const part = normPath(path.resolve(base + '/' + p))
101  if (cGet(cache, part))
102    return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
103  fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb))
104}
105
106const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => {
107  if (er) {
108    fs.lstat(part, (statEr, st) => {
109      if (statEr) {
110        statEr.path = statEr.path && normPath(statEr.path)
111        cb(statEr)
112      } else if (st.isDirectory())
113        mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
114      else if (unlink)
115        fs.unlink(part, er => {
116          if (er)
117            return cb(er)
118          fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb))
119        })
120      else if (st.isSymbolicLink())
121        return cb(new SymlinkError(part, part + '/' + parts.join('/')))
122      else
123        cb(er)
124    })
125  } else {
126    created = created || part
127    mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
128  }
129}
130
131const checkCwdSync = dir => {
132  let ok = false
133  let code = 'ENOTDIR'
134  try {
135    ok = fs.statSync(dir).isDirectory()
136  } catch (er) {
137    code = er.code
138  } finally {
139    if (!ok)
140      throw new CwdError(dir, code)
141  }
142}
143
144module.exports.sync = (dir, opt) => {
145  dir = normPath(dir)
146  // if there's any overlap between mask and mode,
147  // then we'll need an explicit chmod
148  const umask = opt.umask
149  const mode = opt.mode | 0o0700
150  const needChmod = (mode & umask) !== 0
151
152  const uid = opt.uid
153  const gid = opt.gid
154  const doChown = typeof uid === 'number' &&
155    typeof gid === 'number' &&
156    ( uid !== opt.processUid || gid !== opt.processGid )
157
158  const preserve = opt.preserve
159  const unlink = opt.unlink
160  const cache = opt.cache
161  const cwd = normPath(opt.cwd)
162
163  const done = (created) => {
164    cSet(cache, dir, true)
165    if (created && doChown)
166      chownr.sync(created, uid, gid)
167    if (needChmod)
168      fs.chmodSync(dir, mode)
169  }
170
171  if (cache && cGet(cache, dir) === true)
172    return done()
173
174  if (dir === cwd) {
175    checkCwdSync(cwd)
176    return done()
177  }
178
179  if (preserve)
180    return done(mkdirp.sync(dir, mode))
181
182  const sub = normPath(path.relative(cwd, dir))
183  const parts = sub.split('/')
184  let created = null
185  for (let p = parts.shift(), part = cwd;
186    p && (part += '/' + p);
187    p = parts.shift()) {
188    part = normPath(path.resolve(part))
189    if (cGet(cache, part))
190      continue
191
192    try {
193      fs.mkdirSync(part, mode)
194      created = created || part
195      cSet(cache, part, true)
196    } catch (er) {
197      const st = fs.lstatSync(part)
198      if (st.isDirectory()) {
199        cSet(cache, part, true)
200        continue
201      } else if (unlink) {
202        fs.unlinkSync(part)
203        fs.mkdirSync(part, mode)
204        created = created || part
205        cSet(cache, part, true)
206        continue
207      } else if (st.isSymbolicLink())
208        return new SymlinkError(part, part + '/' + parts.join('/'))
209    }
210  }
211
212  return done(created)
213}
214