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 } 45 cb(er) 46 }) 47} 48 49module.exports = (dir, opt, cb) => { 50 dir = normPath(dir) 51 52 // if there's any overlap between mask and mode, 53 // then we'll need an explicit chmod 54 const umask = opt.umask 55 const mode = opt.mode | 0o0700 56 const needChmod = (mode & umask) !== 0 57 58 const uid = opt.uid 59 const gid = opt.gid 60 const doChown = typeof uid === 'number' && 61 typeof gid === 'number' && 62 (uid !== opt.processUid || gid !== opt.processGid) 63 64 const preserve = opt.preserve 65 const unlink = opt.unlink 66 const cache = opt.cache 67 const cwd = normPath(opt.cwd) 68 69 const done = (er, created) => { 70 if (er) { 71 cb(er) 72 } else { 73 cSet(cache, dir, true) 74 if (created && doChown) { 75 chownr(created, uid, gid, er => done(er)) 76 } else if (needChmod) { 77 fs.chmod(dir, mode, cb) 78 } else { 79 cb() 80 } 81 } 82 } 83 84 if (cache && cGet(cache, dir) === true) { 85 return done() 86 } 87 88 if (dir === cwd) { 89 return checkCwd(dir, done) 90 } 91 92 if (preserve) { 93 return mkdirp(dir, { mode }).then(made => done(null, made), done) 94 } 95 96 const sub = normPath(path.relative(cwd, dir)) 97 const parts = sub.split('/') 98 mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done) 99} 100 101const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => { 102 if (!parts.length) { 103 return cb(null, created) 104 } 105 const p = parts.shift() 106 const part = normPath(path.resolve(base + '/' + p)) 107 if (cGet(cache, part)) { 108 return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) 109 } 110 fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb)) 111} 112 113const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => { 114 if (er) { 115 fs.lstat(part, (statEr, st) => { 116 if (statEr) { 117 statEr.path = statEr.path && normPath(statEr.path) 118 cb(statEr) 119 } else if (st.isDirectory()) { 120 mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) 121 } else if (unlink) { 122 fs.unlink(part, er => { 123 if (er) { 124 return cb(er) 125 } 126 fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb)) 127 }) 128 } else if (st.isSymbolicLink()) { 129 return cb(new SymlinkError(part, part + '/' + parts.join('/'))) 130 } else { 131 cb(er) 132 } 133 }) 134 } else { 135 created = created || part 136 mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) 137 } 138} 139 140const checkCwdSync = dir => { 141 let ok = false 142 let code = 'ENOTDIR' 143 try { 144 ok = fs.statSync(dir).isDirectory() 145 } catch (er) { 146 code = er.code 147 } finally { 148 if (!ok) { 149 throw new CwdError(dir, code) 150 } 151 } 152} 153 154module.exports.sync = (dir, opt) => { 155 dir = normPath(dir) 156 // if there's any overlap between mask and mode, 157 // then we'll need an explicit chmod 158 const umask = opt.umask 159 const mode = opt.mode | 0o0700 160 const needChmod = (mode & umask) !== 0 161 162 const uid = opt.uid 163 const gid = opt.gid 164 const doChown = typeof uid === 'number' && 165 typeof gid === 'number' && 166 (uid !== opt.processUid || gid !== opt.processGid) 167 168 const preserve = opt.preserve 169 const unlink = opt.unlink 170 const cache = opt.cache 171 const cwd = normPath(opt.cwd) 172 173 const done = (created) => { 174 cSet(cache, dir, true) 175 if (created && doChown) { 176 chownr.sync(created, uid, gid) 177 } 178 if (needChmod) { 179 fs.chmodSync(dir, mode) 180 } 181 } 182 183 if (cache && cGet(cache, dir) === true) { 184 return done() 185 } 186 187 if (dir === cwd) { 188 checkCwdSync(cwd) 189 return done() 190 } 191 192 if (preserve) { 193 return done(mkdirp.sync(dir, mode)) 194 } 195 196 const sub = normPath(path.relative(cwd, dir)) 197 const parts = sub.split('/') 198 let created = null 199 for (let p = parts.shift(), part = cwd; 200 p && (part += '/' + p); 201 p = parts.shift()) { 202 part = normPath(path.resolve(part)) 203 if (cGet(cache, part)) { 204 continue 205 } 206 207 try { 208 fs.mkdirSync(part, mode) 209 created = created || part 210 cSet(cache, part, true) 211 } catch (er) { 212 const st = fs.lstatSync(part) 213 if (st.isDirectory()) { 214 cSet(cache, part, true) 215 continue 216 } else if (unlink) { 217 fs.unlinkSync(part) 218 fs.mkdirSync(part, mode) 219 created = created || part 220 cSet(cache, part, true) 221 continue 222 } else if (st.isSymbolicLink()) { 223 return new SymlinkError(part, part + '/' + parts.join('/')) 224 } 225 } 226 } 227 228 return done(created) 229} 230