write-entry.js 15 KB

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