index.js 10.0 KB

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