replace.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. 'use strict'
  2. // tar -r
  3. const hlo = require('./high-level-opt.js')
  4. const Pack = require('./pack.js')
  5. const Parse = require('./parse.js')
  6. const fs = require('fs')
  7. const fsm = require('fs-minipass')
  8. const t = require('./list.js')
  9. const path = require('path')
  10. // starting at the head of the file, read a Header
  11. // If the checksum is invalid, that's our position to start writing
  12. // If it is, jump forward by the specified size (round up to 512)
  13. // and try again.
  14. // Write the new Pack stream starting there.
  15. const Header = require('./header.js')
  16. const r = module.exports = (opt_, files, cb) => {
  17. const opt = hlo(opt_)
  18. if (!opt.file)
  19. throw new TypeError('file is required')
  20. if (opt.gzip)
  21. throw new TypeError('cannot append to compressed archives')
  22. if (!files || !Array.isArray(files) || !files.length)
  23. throw new TypeError('no files or directories specified')
  24. files = Array.from(files)
  25. return opt.sync ? replaceSync(opt, files)
  26. : replace(opt, files, cb)
  27. }
  28. const replaceSync = (opt, files) => {
  29. const p = new Pack.Sync(opt)
  30. let threw = true
  31. let fd
  32. let position
  33. try {
  34. try {
  35. fd = fs.openSync(opt.file, 'r+')
  36. } catch (er) {
  37. if (er.code === 'ENOENT')
  38. fd = fs.openSync(opt.file, 'w+')
  39. else
  40. throw er
  41. }
  42. const st = fs.fstatSync(fd)
  43. const headBuf = Buffer.alloc(512)
  44. POSITION: for (position = 0; position < st.size; position += 512) {
  45. for (let bufPos = 0, bytes = 0; bufPos < 512; bufPos += bytes) {
  46. bytes = fs.readSync(
  47. fd, headBuf, bufPos, headBuf.length - bufPos, position + bufPos
  48. )
  49. if (position === 0 && headBuf[0] === 0x1f && headBuf[1] === 0x8b)
  50. throw new Error('cannot append to compressed archives')
  51. if (!bytes)
  52. break POSITION
  53. }
  54. let h = new Header(headBuf)
  55. if (!h.cksumValid)
  56. break
  57. let entryBlockSize = 512 * Math.ceil(h.size / 512)
  58. if (position + entryBlockSize + 512 > st.size)
  59. break
  60. // the 512 for the header we just parsed will be added as well
  61. // also jump ahead all the blocks for the body
  62. position += entryBlockSize
  63. if (opt.mtimeCache)
  64. opt.mtimeCache.set(h.path, h.mtime)
  65. }
  66. threw = false
  67. streamSync(opt, p, position, fd, files)
  68. } finally {
  69. if (threw)
  70. try { fs.closeSync(fd) } catch (er) {}
  71. }
  72. }
  73. const streamSync = (opt, p, position, fd, files) => {
  74. const stream = new fsm.WriteStreamSync(opt.file, {
  75. fd: fd,
  76. start: position
  77. })
  78. p.pipe(stream)
  79. addFilesSync(p, files)
  80. }
  81. const replace = (opt, files, cb) => {
  82. files = Array.from(files)
  83. const p = new Pack(opt)
  84. const getPos = (fd, size, cb_) => {
  85. const cb = (er, pos) => {
  86. if (er)
  87. fs.close(fd, _ => cb_(er))
  88. else
  89. cb_(null, pos)
  90. }
  91. let position = 0
  92. if (size === 0)
  93. return cb(null, 0)
  94. let bufPos = 0
  95. const headBuf = Buffer.alloc(512)
  96. const onread = (er, bytes) => {
  97. if (er)
  98. return cb(er)
  99. bufPos += bytes
  100. if (bufPos < 512 && bytes)
  101. return fs.read(
  102. fd, headBuf, bufPos, headBuf.length - bufPos,
  103. position + bufPos, onread
  104. )
  105. if (position === 0 && headBuf[0] === 0x1f && headBuf[1] === 0x8b)
  106. return cb(new Error('cannot append to compressed archives'))
  107. // truncated header
  108. if (bufPos < 512)
  109. return cb(null, position)
  110. const h = new Header(headBuf)
  111. if (!h.cksumValid)
  112. return cb(null, position)
  113. const entryBlockSize = 512 * Math.ceil(h.size / 512)
  114. if (position + entryBlockSize + 512 > size)
  115. return cb(null, position)
  116. position += entryBlockSize + 512
  117. if (position >= size)
  118. return cb(null, position)
  119. if (opt.mtimeCache)
  120. opt.mtimeCache.set(h.path, h.mtime)
  121. bufPos = 0
  122. fs.read(fd, headBuf, 0, 512, position, onread)
  123. }
  124. fs.read(fd, headBuf, 0, 512, position, onread)
  125. }
  126. const promise = new Promise((resolve, reject) => {
  127. p.on('error', reject)
  128. let flag = 'r+'
  129. const onopen = (er, fd) => {
  130. if (er && er.code === 'ENOENT' && flag === 'r+') {
  131. flag = 'w+'
  132. return fs.open(opt.file, flag, onopen)
  133. }
  134. if (er)
  135. return reject(er)
  136. fs.fstat(fd, (er, st) => {
  137. if (er)
  138. return reject(er)
  139. getPos(fd, st.size, (er, position) => {
  140. if (er)
  141. return reject(er)
  142. const stream = new fsm.WriteStream(opt.file, {
  143. fd: fd,
  144. start: position
  145. })
  146. p.pipe(stream)
  147. stream.on('error', reject)
  148. stream.on('close', resolve)
  149. addFilesAsync(p, files)
  150. })
  151. })
  152. }
  153. fs.open(opt.file, flag, onopen)
  154. })
  155. return cb ? promise.then(cb, cb) : promise
  156. }
  157. const addFilesSync = (p, files) => {
  158. files.forEach(file => {
  159. if (file.charAt(0) === '@')
  160. t({
  161. file: path.resolve(p.cwd, file.substr(1)),
  162. sync: true,
  163. noResume: true,
  164. onentry: entry => p.add(entry)
  165. })
  166. else
  167. p.add(file)
  168. })
  169. p.end()
  170. }
  171. const addFilesAsync = (p, files) => {
  172. while (files.length) {
  173. const file = files.shift()
  174. if (file.charAt(0) === '@')
  175. return t({
  176. file: path.resolve(p.cwd, file.substr(1)),
  177. noResume: true,
  178. onentry: entry => p.add(entry)
  179. }).then(_ => addFilesAsync(p, files))
  180. else
  181. p.add(file)
  182. }
  183. p.end()
  184. }