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