write-entry.js 12 KB

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