write-entry.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. 'use strict'
  2. const MiniPass = require('minipass')
  3. const Pax = require('./pax.js')
  4. const Header = require('./header.js')
  5. const fs = require('fs')
  6. const path = require('path')
  7. const maxReadSize = 16 * 1024 * 1024
  8. const PROCESS = Symbol('process')
  9. const FILE = Symbol('file')
  10. const DIRECTORY = Symbol('directory')
  11. const SYMLINK = Symbol('symlink')
  12. const HARDLINK = Symbol('hardlink')
  13. const HEADER = Symbol('header')
  14. const READ = Symbol('read')
  15. const LSTAT = Symbol('lstat')
  16. const ONLSTAT = Symbol('onlstat')
  17. const ONREAD = Symbol('onread')
  18. const ONREADLINK = Symbol('onreadlink')
  19. const OPENFILE = Symbol('openfile')
  20. const ONOPENFILE = Symbol('onopenfile')
  21. const CLOSE = Symbol('close')
  22. const MODE = Symbol('mode')
  23. const warner = require('./warn-mixin.js')
  24. const winchars = require('./winchars.js')
  25. const modeFix = require('./mode-fix.js')
  26. const WriteEntry = warner(class WriteEntry extends MiniPass {
  27. constructor (p, opt) {
  28. opt = opt || {}
  29. super(opt)
  30. if (typeof p !== 'string')
  31. throw new TypeError('path is required')
  32. this.path = p
  33. // suppress atime, ctime, uid, gid, uname, gname
  34. this.portable = !!opt.portable
  35. // until node has builtin pwnam functions, this'll have to do
  36. this.myuid = process.getuid && process.getuid()
  37. this.myuser = process.env.USER || ''
  38. this.maxReadSize = opt.maxReadSize || maxReadSize
  39. this.linkCache = opt.linkCache || new Map()
  40. this.statCache = opt.statCache || new Map()
  41. this.preservePaths = !!opt.preservePaths
  42. this.cwd = opt.cwd || process.cwd()
  43. this.strict = !!opt.strict
  44. this.noPax = !!opt.noPax
  45. this.noMtime = !!opt.noMtime
  46. this.mtime = opt.mtime || null
  47. if (typeof opt.onwarn === 'function')
  48. this.on('warn', opt.onwarn)
  49. let pathWarn = false
  50. if (!this.preservePaths && path.win32.isAbsolute(p)) {
  51. // absolutes on posix are also absolutes on win32
  52. // so we only need to test this one to get both
  53. const parsed = path.win32.parse(p)
  54. this.path = p.substr(parsed.root.length)
  55. pathWarn = parsed.root
  56. }
  57. this.win32 = !!opt.win32 || process.platform === 'win32'
  58. if (this.win32) {
  59. this.path = winchars.decode(this.path.replace(/\\/g, '/'))
  60. p = p.replace(/\\/g, '/')
  61. }
  62. this.absolute = opt.absolute || path.resolve(this.cwd, p)
  63. if (this.path === '')
  64. this.path = './'
  65. if (pathWarn) {
  66. this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
  67. entry: this,
  68. path: pathWarn + this.path,
  69. })
  70. }
  71. if (this.statCache.has(this.absolute))
  72. this[ONLSTAT](this.statCache.get(this.absolute))
  73. else
  74. this[LSTAT]()
  75. }
  76. [LSTAT] () {
  77. fs.lstat(this.absolute, (er, stat) => {
  78. if (er)
  79. return this.emit('error', er)
  80. this[ONLSTAT](stat)
  81. })
  82. }
  83. [ONLSTAT] (stat) {
  84. this.statCache.set(this.absolute, stat)
  85. this.stat = stat
  86. if (!stat.isFile())
  87. stat.size = 0
  88. this.type = getType(stat)
  89. this.emit('stat', stat)
  90. this[PROCESS]()
  91. }
  92. [PROCESS] () {
  93. switch (this.type) {
  94. case 'File': return this[FILE]()
  95. case 'Directory': return this[DIRECTORY]()
  96. case 'SymbolicLink': return this[SYMLINK]()
  97. // unsupported types are ignored.
  98. default: return this.end()
  99. }
  100. }
  101. [MODE] (mode) {
  102. return modeFix(mode, this.type === 'Directory', this.portable)
  103. }
  104. [HEADER] () {
  105. if (this.type === 'Directory' && this.portable)
  106. this.noMtime = true
  107. this.header = new Header({
  108. path: this.path,
  109. linkpath: this.linkpath,
  110. // only the permissions and setuid/setgid/sticky bitflags
  111. // not the higher-order bits that specify file type
  112. mode: this[MODE](this.stat.mode),
  113. uid: this.portable ? null : this.stat.uid,
  114. gid: this.portable ? null : this.stat.gid,
  115. size: this.stat.size,
  116. mtime: this.noMtime ? null : this.mtime || this.stat.mtime,
  117. type: this.type,
  118. uname: this.portable ? null :
  119. this.stat.uid === this.myuid ? this.myuser : '',
  120. atime: this.portable ? null : this.stat.atime,
  121. ctime: this.portable ? null : this.stat.ctime,
  122. })
  123. if (this.header.encode() && !this.noPax) {
  124. this.write(new Pax({
  125. atime: this.portable ? null : this.header.atime,
  126. ctime: this.portable ? null : this.header.ctime,
  127. gid: this.portable ? null : this.header.gid,
  128. mtime: this.noMtime ? null : this.mtime || this.header.mtime,
  129. path: this.path,
  130. linkpath: this.linkpath,
  131. size: this.header.size,
  132. uid: this.portable ? null : this.header.uid,
  133. uname: this.portable ? null : this.header.uname,
  134. dev: this.portable ? null : this.stat.dev,
  135. ino: this.portable ? null : this.stat.ino,
  136. nlink: this.portable ? null : this.stat.nlink,
  137. }).encode())
  138. }
  139. this.write(this.header.block)
  140. }
  141. [DIRECTORY] () {
  142. if (this.path.substr(-1) !== '/')
  143. this.path += '/'
  144. this.stat.size = 0
  145. this[HEADER]()
  146. this.end()
  147. }
  148. [SYMLINK] () {
  149. fs.readlink(this.absolute, (er, linkpath) => {
  150. if (er)
  151. return this.emit('error', er)
  152. this[ONREADLINK](linkpath)
  153. })
  154. }
  155. [ONREADLINK] (linkpath) {
  156. this.linkpath = linkpath.replace(/\\/g, '/')
  157. this[HEADER]()
  158. this.end()
  159. }
  160. [HARDLINK] (linkpath) {
  161. this.type = 'Link'
  162. this.linkpath = path.relative(this.cwd, linkpath).replace(/\\/g, '/')
  163. this.stat.size = 0
  164. this[HEADER]()
  165. this.end()
  166. }
  167. [FILE] () {
  168. if (this.stat.nlink > 1) {
  169. const linkKey = this.stat.dev + ':' + this.stat.ino
  170. if (this.linkCache.has(linkKey)) {
  171. const linkpath = this.linkCache.get(linkKey)
  172. if (linkpath.indexOf(this.cwd) === 0)
  173. return this[HARDLINK](linkpath)
  174. }
  175. this.linkCache.set(linkKey, this.absolute)
  176. }
  177. this[HEADER]()
  178. if (this.stat.size === 0)
  179. return this.end()
  180. this[OPENFILE]()
  181. }
  182. [OPENFILE] () {
  183. fs.open(this.absolute, 'r', (er, fd) => {
  184. if (er)
  185. return this.emit('error', er)
  186. this[ONOPENFILE](fd)
  187. })
  188. }
  189. [ONOPENFILE] (fd) {
  190. const blockLen = 512 * Math.ceil(this.stat.size / 512)
  191. const bufLen = Math.min(blockLen, this.maxReadSize)
  192. const buf = Buffer.allocUnsafe(bufLen)
  193. this[READ](fd, buf, 0, buf.length, 0, this.stat.size, blockLen)
  194. }
  195. [READ] (fd, buf, offset, length, pos, remain, blockRemain) {
  196. fs.read(fd, buf, offset, length, pos, (er, bytesRead) => {
  197. if (er) {
  198. // ignoring the error from close(2) is a bad practice, but at
  199. // this point we already have an error, don't need another one
  200. return this[CLOSE](fd, () => this.emit('error', er))
  201. }
  202. this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead)
  203. })
  204. }
  205. [CLOSE] (fd, cb) {
  206. fs.close(fd, cb)
  207. }
  208. [ONREAD] (fd, buf, offset, length, pos, remain, blockRemain, bytesRead) {
  209. if (bytesRead <= 0 && remain > 0) {
  210. const er = new Error('encountered unexpected EOF')
  211. er.path = this.absolute
  212. er.syscall = 'read'
  213. er.code = 'EOF'
  214. return this[CLOSE](fd, () => this.emit('error', er))
  215. }
  216. if (bytesRead > remain) {
  217. const er = new Error('did not encounter expected EOF')
  218. er.path = this.absolute
  219. er.syscall = 'read'
  220. er.code = 'EOF'
  221. return this[CLOSE](fd, () => this.emit('error', er))
  222. }
  223. // null out the rest of the buffer, if we could fit the block padding
  224. if (bytesRead === remain) {
  225. for (let i = bytesRead; i < length && bytesRead < blockRemain; i++) {
  226. buf[i + offset] = 0
  227. bytesRead++
  228. remain++
  229. }
  230. }
  231. const writeBuf = offset === 0 && bytesRead === buf.length ?
  232. buf : buf.slice(offset, offset + bytesRead)
  233. remain -= bytesRead
  234. blockRemain -= bytesRead
  235. pos += bytesRead
  236. offset += bytesRead
  237. this.write(writeBuf)
  238. if (!remain) {
  239. if (blockRemain)
  240. this.write(Buffer.alloc(blockRemain))
  241. return this[CLOSE](fd, er => er ? this.emit('error', er) : this.end())
  242. }
  243. if (offset >= length) {
  244. buf = Buffer.allocUnsafe(length)
  245. offset = 0
  246. }
  247. length = buf.length - offset
  248. this[READ](fd, buf, offset, length, pos, remain, blockRemain)
  249. }
  250. })
  251. class WriteEntrySync extends WriteEntry {
  252. [LSTAT] () {
  253. this[ONLSTAT](fs.lstatSync(this.absolute))
  254. }
  255. [SYMLINK] () {
  256. this[ONREADLINK](fs.readlinkSync(this.absolute))
  257. }
  258. [OPENFILE] () {
  259. this[ONOPENFILE](fs.openSync(this.absolute, 'r'))
  260. }
  261. [READ] (fd, buf, offset, length, pos, remain, blockRemain) {
  262. let threw = true
  263. try {
  264. const bytesRead = fs.readSync(fd, buf, offset, length, pos)
  265. this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead)
  266. threw = false
  267. } finally {
  268. // ignoring the error from close(2) is a bad practice, but at
  269. // this point we already have an error, don't need another one
  270. if (threw) {
  271. try {
  272. this[CLOSE](fd, () => {})
  273. } catch (er) {}
  274. }
  275. }
  276. }
  277. [CLOSE] (fd, cb) {
  278. fs.closeSync(fd)
  279. cb()
  280. }
  281. }
  282. const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
  283. constructor (readEntry, opt) {
  284. opt = opt || {}
  285. super(opt)
  286. this.preservePaths = !!opt.preservePaths
  287. this.portable = !!opt.portable
  288. this.strict = !!opt.strict
  289. this.noPax = !!opt.noPax
  290. this.noMtime = !!opt.noMtime
  291. this.readEntry = readEntry
  292. this.type = readEntry.type
  293. if (this.type === 'Directory' && this.portable)
  294. this.noMtime = true
  295. this.path = readEntry.path
  296. this.mode = this[MODE](readEntry.mode)
  297. this.uid = this.portable ? null : readEntry.uid
  298. this.gid = this.portable ? null : readEntry.gid
  299. this.uname = this.portable ? null : readEntry.uname
  300. this.gname = this.portable ? null : readEntry.gname
  301. this.size = readEntry.size
  302. this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime
  303. this.atime = this.portable ? null : readEntry.atime
  304. this.ctime = this.portable ? null : readEntry.ctime
  305. this.linkpath = readEntry.linkpath
  306. if (typeof opt.onwarn === 'function')
  307. this.on('warn', opt.onwarn)
  308. let pathWarn = false
  309. if (path.isAbsolute(this.path) && !this.preservePaths) {
  310. const parsed = path.parse(this.path)
  311. pathWarn = parsed.root
  312. this.path = this.path.substr(parsed.root.length)
  313. }
  314. this.remain = readEntry.size
  315. this.blockRemain = readEntry.startBlockSize
  316. this.header = new Header({
  317. path: this.path,
  318. linkpath: this.linkpath,
  319. // only the permissions and setuid/setgid/sticky bitflags
  320. // not the higher-order bits that specify file type
  321. mode: this.mode,
  322. uid: this.portable ? null : this.uid,
  323. gid: this.portable ? null : this.gid,
  324. size: this.size,
  325. mtime: this.noMtime ? null : this.mtime,
  326. type: this.type,
  327. uname: this.portable ? null : this.uname,
  328. atime: this.portable ? null : this.atime,
  329. ctime: this.portable ? null : this.ctime,
  330. })
  331. if (pathWarn) {
  332. this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
  333. entry: this,
  334. path: pathWarn + this.path,
  335. })
  336. }
  337. if (this.header.encode() && !this.noPax) {
  338. super.write(new Pax({
  339. atime: this.portable ? null : this.atime,
  340. ctime: this.portable ? null : this.ctime,
  341. gid: this.portable ? null : this.gid,
  342. mtime: this.noMtime ? null : this.mtime,
  343. path: this.path,
  344. linkpath: this.linkpath,
  345. size: this.size,
  346. uid: this.portable ? null : this.uid,
  347. uname: this.portable ? null : this.uname,
  348. dev: this.portable ? null : this.readEntry.dev,
  349. ino: this.portable ? null : this.readEntry.ino,
  350. nlink: this.portable ? null : this.readEntry.nlink,
  351. }).encode())
  352. }
  353. super.write(this.header.block)
  354. readEntry.pipe(this)
  355. }
  356. [MODE] (mode) {
  357. return modeFix(mode, this.type === 'Directory', this.portable)
  358. }
  359. write (data) {
  360. const writeLen = data.length
  361. if (writeLen > this.blockRemain)
  362. throw new Error('writing more to entry than is appropriate')
  363. this.blockRemain -= writeLen
  364. return super.write(data)
  365. }
  366. end () {
  367. if (this.blockRemain)
  368. this.write(Buffer.alloc(this.blockRemain))
  369. return super.end()
  370. }
  371. })
  372. WriteEntry.Sync = WriteEntrySync
  373. WriteEntry.Tar = WriteEntryTar
  374. const getType = stat =>
  375. stat.isFile() ? 'File'
  376. : stat.isDirectory() ? 'Directory'
  377. : stat.isSymbolicLink() ? 'SymbolicLink'
  378. : 'Unsupported'
  379. module.exports = WriteEntry