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
			| 
											3 years ago
										 | '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 |