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.
546 lines
15 KiB
546 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
|
|
|