index.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. var chownr = require('chownr')
  2. var tar = require('tar-stream')
  3. var pump = require('pump')
  4. var mkdirp = require('mkdirp-classic')
  5. var fs = require('fs')
  6. var path = require('path')
  7. var os = require('os')
  8. var win32 = os.platform() === 'win32'
  9. var noop = function () {}
  10. var echo = function (name) {
  11. return name
  12. }
  13. var normalize = !win32 ? echo : function (name) {
  14. return name.replace(/\\/g, '/').replace(/[:?<>|]/g, '_')
  15. }
  16. var statAll = function (fs, stat, cwd, ignore, entries, sort) {
  17. var queue = entries || ['.']
  18. return function loop (callback) {
  19. if (!queue.length) return callback()
  20. var next = queue.shift()
  21. var nextAbs = path.join(cwd, next)
  22. stat.call(fs, nextAbs, function (err, stat) {
  23. if (err) return callback(err)
  24. if (!stat.isDirectory()) return callback(null, next, stat)
  25. fs.readdir(nextAbs, function (err, files) {
  26. if (err) return callback(err)
  27. if (sort) files.sort()
  28. for (var i = 0; i < files.length; i++) {
  29. if (!ignore(path.join(cwd, next, files[i]))) queue.push(path.join(next, files[i]))
  30. }
  31. callback(null, next, stat)
  32. })
  33. })
  34. }
  35. }
  36. var strip = function (map, level) {
  37. return function (header) {
  38. header.name = header.name.split('/').slice(level).join('/')
  39. var linkname = header.linkname
  40. if (linkname && (header.type === 'link' || path.isAbsolute(linkname))) {
  41. header.linkname = linkname.split('/').slice(level).join('/')
  42. }
  43. return map(header)
  44. }
  45. }
  46. exports.pack = function (cwd, opts) {
  47. if (!cwd) cwd = '.'
  48. if (!opts) opts = {}
  49. var xfs = opts.fs || fs
  50. var ignore = opts.ignore || opts.filter || noop
  51. var map = opts.map || noop
  52. var mapStream = opts.mapStream || echo
  53. var statNext = statAll(xfs, opts.dereference ? xfs.stat : xfs.lstat, cwd, ignore, opts.entries, opts.sort)
  54. var strict = opts.strict !== false
  55. var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask()
  56. var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0
  57. var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0
  58. var pack = opts.pack || tar.pack()
  59. var finish = opts.finish || noop
  60. if (opts.strip) map = strip(map, opts.strip)
  61. if (opts.readable) {
  62. dmode |= parseInt(555, 8)
  63. fmode |= parseInt(444, 8)
  64. }
  65. if (opts.writable) {
  66. dmode |= parseInt(333, 8)
  67. fmode |= parseInt(222, 8)
  68. }
  69. var onsymlink = function (filename, header) {
  70. xfs.readlink(path.join(cwd, filename), function (err, linkname) {
  71. if (err) return pack.destroy(err)
  72. header.linkname = normalize(linkname)
  73. pack.entry(header, onnextentry)
  74. })
  75. }
  76. var onstat = function (err, filename, stat) {
  77. if (err) return pack.destroy(err)
  78. if (!filename) {
  79. if (opts.finalize !== false) pack.finalize()
  80. return finish(pack)
  81. }
  82. if (stat.isSocket()) return onnextentry() // tar does not support sockets...
  83. var header = {
  84. name: normalize(filename),
  85. mode: (stat.mode | (stat.isDirectory() ? dmode : fmode)) & umask,
  86. mtime: stat.mtime,
  87. size: stat.size,
  88. type: 'file',
  89. uid: stat.uid,
  90. gid: stat.gid
  91. }
  92. if (stat.isDirectory()) {
  93. header.size = 0
  94. header.type = 'directory'
  95. header = map(header) || header
  96. return pack.entry(header, onnextentry)
  97. }
  98. if (stat.isSymbolicLink()) {
  99. header.size = 0
  100. header.type = 'symlink'
  101. header = map(header) || header
  102. return onsymlink(filename, header)
  103. }
  104. // TODO: add fifo etc...
  105. header = map(header) || header
  106. if (!stat.isFile()) {
  107. if (strict) return pack.destroy(new Error('unsupported type for ' + filename))
  108. return onnextentry()
  109. }
  110. var entry = pack.entry(header, onnextentry)
  111. if (!entry) return
  112. var rs = mapStream(xfs.createReadStream(path.join(cwd, filename), { start: 0, end: header.size > 0 ? header.size - 1 : header.size }), header)
  113. rs.on('error', function (err) { // always forward errors on destroy
  114. entry.destroy(err)
  115. })
  116. pump(rs, entry)
  117. }
  118. var onnextentry = function (err) {
  119. if (err) return pack.destroy(err)
  120. statNext(onstat)
  121. }
  122. onnextentry()
  123. return pack
  124. }
  125. var head = function (list) {
  126. return list.length ? list[list.length - 1] : null
  127. }
  128. var processGetuid = function () {
  129. return process.getuid ? process.getuid() : -1
  130. }
  131. var processUmask = function () {
  132. return process.umask ? process.umask() : 0
  133. }
  134. exports.extract = function (cwd, opts) {
  135. if (!cwd) cwd = '.'
  136. if (!opts) opts = {}
  137. var xfs = opts.fs || fs
  138. var ignore = opts.ignore || opts.filter || noop
  139. var map = opts.map || noop
  140. var mapStream = opts.mapStream || echo
  141. var own = opts.chown !== false && !win32 && processGetuid() === 0
  142. var extract = opts.extract || tar.extract()
  143. var stack = []
  144. var now = new Date()
  145. var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask()
  146. var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0
  147. var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0
  148. var strict = opts.strict !== false
  149. if (opts.strip) map = strip(map, opts.strip)
  150. if (opts.readable) {
  151. dmode |= parseInt(555, 8)
  152. fmode |= parseInt(444, 8)
  153. }
  154. if (opts.writable) {
  155. dmode |= parseInt(333, 8)
  156. fmode |= parseInt(222, 8)
  157. }
  158. var utimesParent = function (name, cb) { // we just set the mtime on the parent dir again everytime we write an entry
  159. var top
  160. while ((top = head(stack)) && name.slice(0, top[0].length) !== top[0]) stack.pop()
  161. if (!top) return cb()
  162. xfs.utimes(top[0], now, top[1], cb)
  163. }
  164. var utimes = function (name, header, cb) {
  165. if (opts.utimes === false) return cb()
  166. if (header.type === 'directory') return xfs.utimes(name, now, header.mtime, cb)
  167. if (header.type === 'symlink') return utimesParent(name, cb) // TODO: how to set mtime on link?
  168. xfs.utimes(name, now, header.mtime, function (err) {
  169. if (err) return cb(err)
  170. utimesParent(name, cb)
  171. })
  172. }
  173. var chperm = function (name, header, cb) {
  174. var link = header.type === 'symlink'
  175. /* eslint-disable node/no-deprecated-api */
  176. var chmod = link ? xfs.lchmod : xfs.chmod
  177. var chown = link ? xfs.lchown : xfs.chown
  178. /* eslint-enable node/no-deprecated-api */
  179. if (!chmod) return cb()
  180. var mode = (header.mode | (header.type === 'directory' ? dmode : fmode)) & umask
  181. if (chown && own) chown.call(xfs, name, header.uid, header.gid, onchown)
  182. else onchown(null)
  183. function onchown (err) {
  184. if (err) return cb(err)
  185. if (!chmod) return cb()
  186. chmod.call(xfs, name, mode, cb)
  187. }
  188. }
  189. extract.on('entry', function (header, stream, next) {
  190. header = map(header) || header
  191. header.name = normalize(header.name)
  192. var name = path.join(cwd, path.join('/', header.name))
  193. if (ignore(name, header)) {
  194. stream.resume()
  195. return next()
  196. }
  197. var stat = function (err) {
  198. if (err) return next(err)
  199. utimes(name, header, function (err) {
  200. if (err) return next(err)
  201. if (win32) return next()
  202. chperm(name, header, next)
  203. })
  204. }
  205. var onsymlink = function () {
  206. if (win32) return next() // skip symlinks on win for now before it can be tested
  207. xfs.unlink(name, function () {
  208. xfs.symlink(header.linkname, name, stat)
  209. })
  210. }
  211. var onlink = function () {
  212. if (win32) return next() // skip links on win for now before it can be tested
  213. xfs.unlink(name, function () {
  214. var srcpath = path.join(cwd, path.join('/', header.linkname))
  215. xfs.link(srcpath, name, function (err) {
  216. if (err && err.code === 'EPERM' && opts.hardlinkAsFilesFallback) {
  217. stream = xfs.createReadStream(srcpath)
  218. return onfile()
  219. }
  220. stat(err)
  221. })
  222. })
  223. }
  224. var onfile = function () {
  225. var ws = xfs.createWriteStream(name)
  226. var rs = mapStream(stream, header)
  227. ws.on('error', function (err) { // always forward errors on destroy
  228. rs.destroy(err)
  229. })
  230. pump(rs, ws, function (err) {
  231. if (err) return next(err)
  232. ws.on('close', stat)
  233. })
  234. }
  235. if (header.type === 'directory') {
  236. stack.push([name, header.mtime])
  237. return mkdirfix(name, {
  238. fs: xfs, own: own, uid: header.uid, gid: header.gid
  239. }, stat)
  240. }
  241. var dir = path.dirname(name)
  242. validate(xfs, dir, path.join(cwd, '.'), function (err, valid) {
  243. if (err) return next(err)
  244. if (!valid) return next(new Error(dir + ' is not a valid path'))
  245. mkdirfix(dir, {
  246. fs: xfs, own: own, uid: header.uid, gid: header.gid
  247. }, function (err) {
  248. if (err) return next(err)
  249. switch (header.type) {
  250. case 'file': return onfile()
  251. case 'link': return onlink()
  252. case 'symlink': return onsymlink()
  253. }
  254. if (strict) return next(new Error('unsupported type for ' + name + ' (' + header.type + ')'))
  255. stream.resume()
  256. next()
  257. })
  258. })
  259. })
  260. if (opts.finish) extract.on('finish', opts.finish)
  261. return extract
  262. }
  263. function validate (fs, name, root, cb) {
  264. if (name === root) return cb(null, true)
  265. fs.lstat(name, function (err, st) {
  266. if (err && err.code !== 'ENOENT') return cb(err)
  267. if (err || st.isDirectory()) return validate(fs, path.join(name, '..'), root, cb)
  268. cb(null, false)
  269. })
  270. }
  271. function mkdirfix (name, opts, cb) {
  272. mkdirp(name, { fs: opts.fs }, function (err, made) {
  273. if (!err && made && opts.own) {
  274. chownr(made, opts.uid, opts.gid, cb)
  275. } else {
  276. cb(err)
  277. }
  278. })
  279. }