You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							547 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
	
	
							547 lines
						
					
					
						
							15 KiB
						
					
					
				| 'use strict'
 | |
| const { Minipass } = require('minipass')
 | |
| const Pax = require('./pax.js')
 | |
| const Header = require('./header.js')
 | |
| const fs = require('fs')
 | |
| const path = require('path')
 | |
| const normPath = require('./normalize-windows-path.js')
 | |
| const stripSlash = require('./strip-trailing-slashes.js')
 | |
| 
 | |
| const prefixPath = (path, prefix) => {
 | |
|   if (!prefix) {
 | |
|     return normPath(path)
 | |
|   }
 | |
|   path = normPath(path).replace(/^\.(\/|$)/, '')
 | |
|   return stripSlash(prefix) + '/' + path
 | |
| }
 | |
| 
 | |
| const maxReadSize = 16 * 1024 * 1024
 | |
| const PROCESS = Symbol('process')
 | |
| const FILE = Symbol('file')
 | |
| const DIRECTORY = Symbol('directory')
 | |
| const SYMLINK = Symbol('symlink')
 | |
| const HARDLINK = Symbol('hardlink')
 | |
| const HEADER = Symbol('header')
 | |
| const READ = Symbol('read')
 | |
| const LSTAT = Symbol('lstat')
 | |
| const ONLSTAT = Symbol('onlstat')
 | |
| const ONREAD = Symbol('onread')
 | |
| const ONREADLINK = Symbol('onreadlink')
 | |
| const OPENFILE = Symbol('openfile')
 | |
| const ONOPENFILE = Symbol('onopenfile')
 | |
| const CLOSE = Symbol('close')
 | |
| const MODE = Symbol('mode')
 | |
| const AWAITDRAIN = Symbol('awaitDrain')
 | |
| const ONDRAIN = Symbol('ondrain')
 | |
| const PREFIX = Symbol('prefix')
 | |
| const HAD_ERROR = Symbol('hadError')
 | |
| const warner = require('./warn-mixin.js')
 | |
| const winchars = require('./winchars.js')
 | |
| const stripAbsolutePath = require('./strip-absolute-path.js')
 | |
| 
 | |
| const modeFix = require('./mode-fix.js')
 | |
| 
 | |
| const WriteEntry = warner(class WriteEntry extends Minipass {
 | |
|   constructor (p, opt) {
 | |
|     opt = opt || {}
 | |
|     super(opt)
 | |
|     if (typeof p !== 'string') {
 | |
|       throw new TypeError('path is required')
 | |
|     }
 | |
|     this.path = normPath(p)
 | |
|     // suppress atime, ctime, uid, gid, uname, gname
 | |
|     this.portable = !!opt.portable
 | |
|     // until node has builtin pwnam functions, this'll have to do
 | |
|     this.myuid = process.getuid && process.getuid() || 0
 | |
|     this.myuser = process.env.USER || ''
 | |
|     this.maxReadSize = opt.maxReadSize || maxReadSize
 | |
|     this.linkCache = opt.linkCache || new Map()
 | |
|     this.statCache = opt.statCache || new Map()
 | |
|     this.preservePaths = !!opt.preservePaths
 | |
|     this.cwd = normPath(opt.cwd || process.cwd())
 | |
|     this.strict = !!opt.strict
 | |
|     this.noPax = !!opt.noPax
 | |
|     this.noMtime = !!opt.noMtime
 | |
|     this.mtime = opt.mtime || null
 | |
|     this.prefix = opt.prefix ? normPath(opt.prefix) : null
 | |
| 
 | |
|     this.fd = null
 | |
|     this.blockLen = null
 | |
|     this.blockRemain = null
 | |
|     this.buf = null
 | |
|     this.offset = null
 | |
|     this.length = null
 | |
|     this.pos = null
 | |
|     this.remain = null
 | |
| 
 | |
|     if (typeof opt.onwarn === 'function') {
 | |
|       this.on('warn', opt.onwarn)
 | |
|     }
 | |
| 
 | |
|     let pathWarn = false
 | |
|     if (!this.preservePaths) {
 | |
|       const [root, stripped] = stripAbsolutePath(this.path)
 | |
|       if (root) {
 | |
|         this.path = stripped
 | |
|         pathWarn = root
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.win32 = !!opt.win32 || process.platform === 'win32'
 | |
|     if (this.win32) {
 | |
|       // force the \ to / normalization, since we might not *actually*
 | |
|       // be on windows, but want \ to be considered a path separator.
 | |
|       this.path = winchars.decode(this.path.replace(/\\/g, '/'))
 | |
|       p = p.replace(/\\/g, '/')
 | |
|     }
 | |
| 
 | |
|     this.absolute = normPath(opt.absolute || path.resolve(this.cwd, p))
 | |
| 
 | |
|     if (this.path === '') {
 | |
|       this.path = './'
 | |
|     }
 | |
| 
 | |
|     if (pathWarn) {
 | |
|       this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
 | |
|         entry: this,
 | |
|         path: pathWarn + this.path,
 | |
|       })
 | |
|     }
 | |
| 
 | |
|     if (this.statCache.has(this.absolute)) {
 | |
|       this[ONLSTAT](this.statCache.get(this.absolute))
 | |
|     } else {
 | |
|       this[LSTAT]()
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   emit (ev, ...data) {
 | |
|     if (ev === 'error') {
 | |
|       this[HAD_ERROR] = true
 | |
|     }
 | |
|     return super.emit(ev, ...data)
 | |
|   }
 | |
| 
 | |
|   [LSTAT] () {
 | |
|     fs.lstat(this.absolute, (er, stat) => {
 | |
|       if (er) {
 | |
|         return this.emit('error', er)
 | |
|       }
 | |
|       this[ONLSTAT](stat)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   [ONLSTAT] (stat) {
 | |
|     this.statCache.set(this.absolute, stat)
 | |
|     this.stat = stat
 | |
|     if (!stat.isFile()) {
 | |
|       stat.size = 0
 | |
|     }
 | |
|     this.type = getType(stat)
 | |
|     this.emit('stat', stat)
 | |
|     this[PROCESS]()
 | |
|   }
 | |
| 
 | |
|   [PROCESS] () {
 | |
|     switch (this.type) {
 | |
|       case 'File': return this[FILE]()
 | |
|       case 'Directory': return this[DIRECTORY]()
 | |
|       case 'SymbolicLink': return this[SYMLINK]()
 | |
|       // unsupported types are ignored.
 | |
|       default: return this.end()
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   [MODE] (mode) {
 | |
|     return modeFix(mode, this.type === 'Directory', this.portable)
 | |
|   }
 | |
| 
 | |
|   [PREFIX] (path) {
 | |
|     return prefixPath(path, this.prefix)
 | |
|   }
 | |
| 
 | |
|   [HEADER] () {
 | |
|     if (this.type === 'Directory' && this.portable) {
 | |
|       this.noMtime = true
 | |
|     }
 | |
| 
 | |
|     this.header = new Header({
 | |
|       path: this[PREFIX](this.path),
 | |
|       // only apply the prefix to hard links.
 | |
|       linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
 | |
|       : this.linkpath,
 | |
|       // only the permissions and setuid/setgid/sticky bitflags
 | |
|       // not the higher-order bits that specify file type
 | |
|       mode: this[MODE](this.stat.mode),
 | |
|       uid: this.portable ? null : this.stat.uid,
 | |
|       gid: this.portable ? null : this.stat.gid,
 | |
|       size: this.stat.size,
 | |
|       mtime: this.noMtime ? null : this.mtime || this.stat.mtime,
 | |
|       type: this.type,
 | |
|       uname: this.portable ? null :
 | |
|       this.stat.uid === this.myuid ? this.myuser : '',
 | |
|       atime: this.portable ? null : this.stat.atime,
 | |
|       ctime: this.portable ? null : this.stat.ctime,
 | |
|     })
 | |
| 
 | |
|     if (this.header.encode() && !this.noPax) {
 | |
|       super.write(new Pax({
 | |
|         atime: this.portable ? null : this.header.atime,
 | |
|         ctime: this.portable ? null : this.header.ctime,
 | |
|         gid: this.portable ? null : this.header.gid,
 | |
|         mtime: this.noMtime ? null : this.mtime || this.header.mtime,
 | |
|         path: this[PREFIX](this.path),
 | |
|         linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
 | |
|         : this.linkpath,
 | |
|         size: this.header.size,
 | |
|         uid: this.portable ? null : this.header.uid,
 | |
|         uname: this.portable ? null : this.header.uname,
 | |
|         dev: this.portable ? null : this.stat.dev,
 | |
|         ino: this.portable ? null : this.stat.ino,
 | |
|         nlink: this.portable ? null : this.stat.nlink,
 | |
|       }).encode())
 | |
|     }
 | |
|     super.write(this.header.block)
 | |
|   }
 | |
| 
 | |
|   [DIRECTORY] () {
 | |
|     if (this.path.slice(-1) !== '/') {
 | |
|       this.path += '/'
 | |
|     }
 | |
|     this.stat.size = 0
 | |
|     this[HEADER]()
 | |
|     this.end()
 | |
|   }
 | |
| 
 | |
|   [SYMLINK] () {
 | |
|     fs.readlink(this.absolute, (er, linkpath) => {
 | |
|       if (er) {
 | |
|         return this.emit('error', er)
 | |
|       }
 | |
|       this[ONREADLINK](linkpath)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   [ONREADLINK] (linkpath) {
 | |
|     this.linkpath = normPath(linkpath)
 | |
|     this[HEADER]()
 | |
|     this.end()
 | |
|   }
 | |
| 
 | |
|   [HARDLINK] (linkpath) {
 | |
|     this.type = 'Link'
 | |
|     this.linkpath = normPath(path.relative(this.cwd, linkpath))
 | |
|     this.stat.size = 0
 | |
|     this[HEADER]()
 | |
|     this.end()
 | |
|   }
 | |
| 
 | |
|   [FILE] () {
 | |
|     if (this.stat.nlink > 1) {
 | |
|       const linkKey = this.stat.dev + ':' + this.stat.ino
 | |
|       if (this.linkCache.has(linkKey)) {
 | |
|         const linkpath = this.linkCache.get(linkKey)
 | |
|         if (linkpath.indexOf(this.cwd) === 0) {
 | |
|           return this[HARDLINK](linkpath)
 | |
|         }
 | |
|       }
 | |
|       this.linkCache.set(linkKey, this.absolute)
 | |
|     }
 | |
| 
 | |
|     this[HEADER]()
 | |
|     if (this.stat.size === 0) {
 | |
|       return this.end()
 | |
|     }
 | |
| 
 | |
|     this[OPENFILE]()
 | |
|   }
 | |
| 
 | |
|   [OPENFILE] () {
 | |
|     fs.open(this.absolute, 'r', (er, fd) => {
 | |
|       if (er) {
 | |
|         return this.emit('error', er)
 | |
|       }
 | |
|       this[ONOPENFILE](fd)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   [ONOPENFILE] (fd) {
 | |
|     this.fd = fd
 | |
|     if (this[HAD_ERROR]) {
 | |
|       return this[CLOSE]()
 | |
|     }
 | |
| 
 | |
|     this.blockLen = 512 * Math.ceil(this.stat.size / 512)
 | |
|     this.blockRemain = this.blockLen
 | |
|     const bufLen = Math.min(this.blockLen, this.maxReadSize)
 | |
|     this.buf = Buffer.allocUnsafe(bufLen)
 | |
|     this.offset = 0
 | |
|     this.pos = 0
 | |
|     this.remain = this.stat.size
 | |
|     this.length = this.buf.length
 | |
|     this[READ]()
 | |
|   }
 | |
| 
 | |
|   [READ] () {
 | |
|     const { fd, buf, offset, length, pos } = this
 | |
|     fs.read(fd, buf, offset, length, pos, (er, bytesRead) => {
 | |
|       if (er) {
 | |
|         // ignoring the error from close(2) is a bad practice, but at
 | |
|         // this point we already have an error, don't need another one
 | |
|         return this[CLOSE](() => this.emit('error', er))
 | |
|       }
 | |
|       this[ONREAD](bytesRead)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   [CLOSE] (cb) {
 | |
|     fs.close(this.fd, cb)
 | |
|   }
 | |
| 
 | |
|   [ONREAD] (bytesRead) {
 | |
|     if (bytesRead <= 0 && this.remain > 0) {
 | |
|       const er = new Error('encountered unexpected EOF')
 | |
|       er.path = this.absolute
 | |
|       er.syscall = 'read'
 | |
|       er.code = 'EOF'
 | |
|       return this[CLOSE](() => this.emit('error', er))
 | |
|     }
 | |
| 
 | |
|     if (bytesRead > this.remain) {
 | |
|       const er = new Error('did not encounter expected EOF')
 | |
|       er.path = this.absolute
 | |
|       er.syscall = 'read'
 | |
|       er.code = 'EOF'
 | |
|       return this[CLOSE](() => this.emit('error', er))
 | |
|     }
 | |
| 
 | |
|     // null out the rest of the buffer, if we could fit the block padding
 | |
|     // at the end of this loop, we've incremented bytesRead and this.remain
 | |
|     // to be incremented up to the blockRemain level, as if we had expected
 | |
|     // to get a null-padded file, and read it until the end.  then we will
 | |
|     // decrement both remain and blockRemain by bytesRead, and know that we
 | |
|     // reached the expected EOF, without any null buffer to append.
 | |
|     if (bytesRead === this.remain) {
 | |
|       for (let i = bytesRead; i < this.length && bytesRead < this.blockRemain; i++) {
 | |
|         this.buf[i + this.offset] = 0
 | |
|         bytesRead++
 | |
|         this.remain++
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const writeBuf = this.offset === 0 && bytesRead === this.buf.length ?
 | |
|       this.buf : this.buf.slice(this.offset, this.offset + bytesRead)
 | |
| 
 | |
|     const flushed = this.write(writeBuf)
 | |
|     if (!flushed) {
 | |
|       this[AWAITDRAIN](() => this[ONDRAIN]())
 | |
|     } else {
 | |
|       this[ONDRAIN]()
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   [AWAITDRAIN] (cb) {
 | |
|     this.once('drain', cb)
 | |
|   }
 | |
| 
 | |
|   write (writeBuf) {
 | |
|     if (this.blockRemain < writeBuf.length) {
 | |
|       const er = new Error('writing more data than expected')
 | |
|       er.path = this.absolute
 | |
|       return this.emit('error', er)
 | |
|     }
 | |
|     this.remain -= writeBuf.length
 | |
|     this.blockRemain -= writeBuf.length
 | |
|     this.pos += writeBuf.length
 | |
|     this.offset += writeBuf.length
 | |
|     return super.write(writeBuf)
 | |
|   }
 | |
| 
 | |
|   [ONDRAIN] () {
 | |
|     if (!this.remain) {
 | |
|       if (this.blockRemain) {
 | |
|         super.write(Buffer.alloc(this.blockRemain))
 | |
|       }
 | |
|       return this[CLOSE](er => er ? this.emit('error', er) : this.end())
 | |
|     }
 | |
| 
 | |
|     if (this.offset >= this.length) {
 | |
|       // if we only have a smaller bit left to read, alloc a smaller buffer
 | |
|       // otherwise, keep it the same length it was before.
 | |
|       this.buf = Buffer.allocUnsafe(Math.min(this.blockRemain, this.buf.length))
 | |
|       this.offset = 0
 | |
|     }
 | |
|     this.length = this.buf.length - this.offset
 | |
|     this[READ]()
 | |
|   }
 | |
| })
 | |
| 
 | |
| class WriteEntrySync extends WriteEntry {
 | |
|   [LSTAT] () {
 | |
|     this[ONLSTAT](fs.lstatSync(this.absolute))
 | |
|   }
 | |
| 
 | |
|   [SYMLINK] () {
 | |
|     this[ONREADLINK](fs.readlinkSync(this.absolute))
 | |
|   }
 | |
| 
 | |
|   [OPENFILE] () {
 | |
|     this[ONOPENFILE](fs.openSync(this.absolute, 'r'))
 | |
|   }
 | |
| 
 | |
|   [READ] () {
 | |
|     let threw = true
 | |
|     try {
 | |
|       const { fd, buf, offset, length, pos } = this
 | |
|       const bytesRead = fs.readSync(fd, buf, offset, length, pos)
 | |
|       this[ONREAD](bytesRead)
 | |
|       threw = false
 | |
|     } finally {
 | |
|       // ignoring the error from close(2) is a bad practice, but at
 | |
|       // this point we already have an error, don't need another one
 | |
|       if (threw) {
 | |
|         try {
 | |
|           this[CLOSE](() => {})
 | |
|         } catch (er) {}
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   [AWAITDRAIN] (cb) {
 | |
|     cb()
 | |
|   }
 | |
| 
 | |
|   [CLOSE] (cb) {
 | |
|     fs.closeSync(this.fd)
 | |
|     cb()
 | |
|   }
 | |
| }
 | |
| 
 | |
| const WriteEntryTar = warner(class WriteEntryTar extends Minipass {
 | |
|   constructor (readEntry, opt) {
 | |
|     opt = opt || {}
 | |
|     super(opt)
 | |
|     this.preservePaths = !!opt.preservePaths
 | |
|     this.portable = !!opt.portable
 | |
|     this.strict = !!opt.strict
 | |
|     this.noPax = !!opt.noPax
 | |
|     this.noMtime = !!opt.noMtime
 | |
| 
 | |
|     this.readEntry = readEntry
 | |
|     this.type = readEntry.type
 | |
|     if (this.type === 'Directory' && this.portable) {
 | |
|       this.noMtime = true
 | |
|     }
 | |
| 
 | |
|     this.prefix = opt.prefix || null
 | |
| 
 | |
|     this.path = normPath(readEntry.path)
 | |
|     this.mode = this[MODE](readEntry.mode)
 | |
|     this.uid = this.portable ? null : readEntry.uid
 | |
|     this.gid = this.portable ? null : readEntry.gid
 | |
|     this.uname = this.portable ? null : readEntry.uname
 | |
|     this.gname = this.portable ? null : readEntry.gname
 | |
|     this.size = readEntry.size
 | |
|     this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime
 | |
|     this.atime = this.portable ? null : readEntry.atime
 | |
|     this.ctime = this.portable ? null : readEntry.ctime
 | |
|     this.linkpath = normPath(readEntry.linkpath)
 | |
| 
 | |
|     if (typeof opt.onwarn === 'function') {
 | |
|       this.on('warn', opt.onwarn)
 | |
|     }
 | |
| 
 | |
|     let pathWarn = false
 | |
|     if (!this.preservePaths) {
 | |
|       const [root, stripped] = stripAbsolutePath(this.path)
 | |
|       if (root) {
 | |
|         this.path = stripped
 | |
|         pathWarn = root
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.remain = readEntry.size
 | |
|     this.blockRemain = readEntry.startBlockSize
 | |
| 
 | |
|     this.header = new Header({
 | |
|       path: this[PREFIX](this.path),
 | |
|       linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
 | |
|       : this.linkpath,
 | |
|       // only the permissions and setuid/setgid/sticky bitflags
 | |
|       // not the higher-order bits that specify file type
 | |
|       mode: this.mode,
 | |
|       uid: this.portable ? null : this.uid,
 | |
|       gid: this.portable ? null : this.gid,
 | |
|       size: this.size,
 | |
|       mtime: this.noMtime ? null : this.mtime,
 | |
|       type: this.type,
 | |
|       uname: this.portable ? null : this.uname,
 | |
|       atime: this.portable ? null : this.atime,
 | |
|       ctime: this.portable ? null : this.ctime,
 | |
|     })
 | |
| 
 | |
|     if (pathWarn) {
 | |
|       this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
 | |
|         entry: this,
 | |
|         path: pathWarn + this.path,
 | |
|       })
 | |
|     }
 | |
| 
 | |
|     if (this.header.encode() && !this.noPax) {
 | |
|       super.write(new Pax({
 | |
|         atime: this.portable ? null : this.atime,
 | |
|         ctime: this.portable ? null : this.ctime,
 | |
|         gid: this.portable ? null : this.gid,
 | |
|         mtime: this.noMtime ? null : this.mtime,
 | |
|         path: this[PREFIX](this.path),
 | |
|         linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
 | |
|         : this.linkpath,
 | |
|         size: this.size,
 | |
|         uid: this.portable ? null : this.uid,
 | |
|         uname: this.portable ? null : this.uname,
 | |
|         dev: this.portable ? null : this.readEntry.dev,
 | |
|         ino: this.portable ? null : this.readEntry.ino,
 | |
|         nlink: this.portable ? null : this.readEntry.nlink,
 | |
|       }).encode())
 | |
|     }
 | |
| 
 | |
|     super.write(this.header.block)
 | |
|     readEntry.pipe(this)
 | |
|   }
 | |
| 
 | |
|   [PREFIX] (path) {
 | |
|     return prefixPath(path, this.prefix)
 | |
|   }
 | |
| 
 | |
|   [MODE] (mode) {
 | |
|     return modeFix(mode, this.type === 'Directory', this.portable)
 | |
|   }
 | |
| 
 | |
|   write (data) {
 | |
|     const writeLen = data.length
 | |
|     if (writeLen > this.blockRemain) {
 | |
|       throw new Error('writing more to entry than is appropriate')
 | |
|     }
 | |
|     this.blockRemain -= writeLen
 | |
|     return super.write(data)
 | |
|   }
 | |
| 
 | |
|   end () {
 | |
|     if (this.blockRemain) {
 | |
|       super.write(Buffer.alloc(this.blockRemain))
 | |
|     }
 | |
|     return super.end()
 | |
|   }
 | |
| })
 | |
| 
 | |
| WriteEntry.Sync = WriteEntrySync
 | |
| WriteEntry.Tar = WriteEntryTar
 | |
| 
 | |
| const getType = stat =>
 | |
|   stat.isFile() ? 'File'
 | |
|   : stat.isDirectory() ? 'Directory'
 | |
|   : stat.isSymbolicLink() ? 'SymbolicLink'
 | |
|   : 'Unsupported'
 | |
| 
 | |
| module.exports = WriteEntry
 |