index.js 10.0 KB

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