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.

391 lines
11 KiB

1 month ago
module.exports = Writer
var fs = require('graceful-fs')
var inherits = require('inherits')
var rimraf = require('rimraf')
var mkdir = require('mkdirp')
var path = require('path')
var umask = process.platform === 'win32' ? 0 : process.umask()
var getType = require('./get-type.js')
var Abstract = require('./abstract.js')
// Must do this *before* loading the child classes
inherits(Writer, Abstract)
Writer.dirmode = parseInt('0777', 8) & (~umask)
Writer.filemode = parseInt('0666', 8) & (~umask)
var DirWriter = require('./dir-writer.js')
var LinkWriter = require('./link-writer.js')
var FileWriter = require('./file-writer.js')
var ProxyWriter = require('./proxy-writer.js')
// props is the desired state. current is optionally the current stat,
// provided here so that subclasses can avoid statting the target
// more than necessary.
function Writer (props, current) {
var self = this
if (typeof props === 'string') {
props = { path: props }
}
// polymorphism.
// call fstream.Writer(dir) to get a DirWriter object, etc.
var type = getType(props)
var ClassType = Writer
switch (type) {
case 'Directory':
ClassType = DirWriter
break
case 'File':
ClassType = FileWriter
break
case 'Link':
case 'SymbolicLink':
ClassType = LinkWriter
break
case null:
default:
// Don't know yet what type to create, so we wrap in a proxy.
ClassType = ProxyWriter
break
}
if (!(self instanceof ClassType)) return new ClassType(props)
// now get down to business.
Abstract.call(self)
if (!props.path) self.error('Must provide a path', null, true)
// props is what we want to set.
// set some convenience properties as well.
self.type = props.type
self.props = props
self.depth = props.depth || 0
self.clobber = props.clobber === false ? props.clobber : true
self.parent = props.parent || null
self.root = props.root || (props.parent && props.parent.root) || self
self._path = self.path = path.resolve(props.path)
if (process.platform === 'win32') {
self.path = self._path = self.path.replace(/\?/g, '_')
if (self._path.length >= 260) {
self._swallowErrors = true
self._path = '\\\\?\\' + self.path.replace(/\//g, '\\')
}
}
self.basename = path.basename(props.path)
self.dirname = path.dirname(props.path)
self.linkpath = props.linkpath || null
props.parent = props.root = null
// console.error("\n\n\n%s setting size to", props.path, props.size)
self.size = props.size
if (typeof props.mode === 'string') {
props.mode = parseInt(props.mode, 8)
}
self.readable = false
self.writable = true
// buffer until ready, or while handling another entry
self._buffer = []
self.ready = false
self.filter = typeof props.filter === 'function' ? props.filter : null
// start the ball rolling.
// this checks what's there already, and then calls
// self._create() to call the impl-specific creation stuff.
self._stat(current)
}
// Calling this means that it's something we can't create.
// Just assert that it's already there, otherwise raise a warning.
Writer.prototype._create = function () {
var self = this
fs[self.props.follow ? 'stat' : 'lstat'](self._path, function (er) {
if (er) {
return self.warn('Cannot create ' + self._path + '\n' +
'Unsupported type: ' + self.type, 'ENOTSUP')
}
self._finish()
})
}
Writer.prototype._stat = function (current) {
var self = this
var props = self.props
var stat = props.follow ? 'stat' : 'lstat'
var who = self._proxy || self
if (current) statCb(null, current)
else fs[stat](self._path, statCb)
function statCb (er, current) {
if (self.filter && !self.filter.call(who, who, current)) {
self._aborted = true
self.emit('end')
self.emit('close')
return
}
// if it's not there, great. We'll just create it.
// if it is there, then we'll need to change whatever differs
if (er || !current) {
return create(self)
}
self._old = current
var currentType = getType(current)
// if it's a type change, then we need to clobber or error.
// if it's not a type change, then let the impl take care of it.
if (currentType !== self.type || self.type === 'File' && current.nlink > 1) {
return rimraf(self._path, function (er) {
if (er) return self.error(er)
self._old = null
create(self)
})
}
// otherwise, just handle in the app-specific way
// this creates a fs.WriteStream, or mkdir's, or whatever
create(self)
}
}
function create (self) {
// console.error("W create", self._path, Writer.dirmode)
// XXX Need to clobber non-dirs that are in the way,
// unless { clobber: false } in the props.
mkdir(path.dirname(self._path), Writer.dirmode, function (er, made) {
// console.error("W created", path.dirname(self._path), er)
if (er) return self.error(er)
// later on, we have to set the mode and owner for these
self._madeDir = made
return self._create()
})
}
function endChmod (self, want, current, path, cb) {
var wantMode = want.mode
var chmod = want.follow || self.type !== 'SymbolicLink'
? 'chmod' : 'lchmod'
if (!fs[chmod]) return cb()
if (typeof wantMode !== 'number') return cb()
var curMode = current.mode & parseInt('0777', 8)
wantMode = wantMode & parseInt('0777', 8)
if (wantMode === curMode) return cb()
fs[chmod](path, wantMode, cb)
}
function endChown (self, want, current, path, cb) {
// Don't even try it unless root. Too easy to EPERM.
if (process.platform === 'win32') return cb()
if (!process.getuid || process.getuid() !== 0) return cb()
if (typeof want.uid !== 'number' &&
typeof want.gid !== 'number') return cb()
if (current.uid === want.uid &&
current.gid === want.gid) return cb()
var chown = (self.props.follow || self.type !== 'SymbolicLink')
? 'chown' : 'lchown'
if (!fs[chown]) return cb()
if (typeof want.uid !== 'number') want.uid = current.uid
if (typeof want.gid !== 'number') want.gid = current.gid
fs[chown](path, want.uid, want.gid, cb)
}
function endUtimes (self, want, current, path, cb) {
if (!fs.utimes || process.platform === 'win32') return cb()
var utimes = (want.follow || self.type !== 'SymbolicLink')
? 'utimes' : 'lutimes'
if (utimes === 'lutimes' && !fs[utimes]) {
utimes = 'utimes'
}
if (!fs[utimes]) return cb()
var curA = current.atime
var curM = current.mtime
var meA = want.atime
var meM = want.mtime
if (meA === undefined) meA = curA
if (meM === undefined) meM = curM
if (!isDate(meA)) meA = new Date(meA)
if (!isDate(meM)) meA = new Date(meM)
if (meA.getTime() === curA.getTime() &&
meM.getTime() === curM.getTime()) return cb()
fs[utimes](path, meA, meM, cb)
}
// XXX This function is beastly. Break it up!
Writer.prototype._finish = function () {
var self = this
if (self._finishing) return
self._finishing = true
// console.error(" W Finish", self._path, self.size)
// set up all the things.
// At this point, we're already done writing whatever we've gotta write,
// adding files to the dir, etc.
var todo = 0
var errState = null
var done = false
if (self._old) {
// the times will almost *certainly* have changed.
// adds the utimes syscall, but remove another stat.
self._old.atime = new Date(0)
self._old.mtime = new Date(0)
// console.error(" W Finish Stale Stat", self._path, self.size)
setProps(self._old)
} else {
var stat = self.props.follow ? 'stat' : 'lstat'
// console.error(" W Finish Stating", self._path, self.size)
fs[stat](self._path, function (er, current) {
// console.error(" W Finish Stated", self._path, self.size, current)
if (er) {
// if we're in the process of writing out a
// directory, it's very possible that the thing we're linking to
// doesn't exist yet (especially if it was intended as a symlink),
// so swallow ENOENT errors here and just soldier on.
if (er.code === 'ENOENT' &&
(self.type === 'Link' || self.type === 'SymbolicLink') &&
process.platform === 'win32') {
self.ready = true
self.emit('ready')
self.emit('end')
self.emit('close')
self.end = self._finish = function () {}
return
} else return self.error(er)
}
setProps(self._old = current)
})
}
return
function setProps (current) {
todo += 3
endChmod(self, self.props, current, self._path, next('chmod'))
endChown(self, self.props, current, self._path, next('chown'))
endUtimes(self, self.props, current, self._path, next('utimes'))
}
function next (what) {
return function (er) {
// console.error(" W Finish", what, todo)
if (errState) return
if (er) {
er.fstream_finish_call = what
return self.error(errState = er)
}
if (--todo > 0) return
if (done) return
done = true
// we may still need to set the mode/etc. on some parent dirs
// that were created previously. delay end/close until then.
if (!self._madeDir) return end()
else endMadeDir(self, self._path, end)
function end (er) {
if (er) {
er.fstream_finish_call = 'setupMadeDir'
return self.error(er)
}
// all the props have been set, so we're completely done.
self.emit('end')
self.emit('close')
}
}
}
}
function endMadeDir (self, p, cb) {
var made = self._madeDir
// everything *between* made and path.dirname(self._path)
// needs to be set up. Note that this may just be one dir.
var d = path.dirname(p)
endMadeDir_(self, d, function (er) {
if (er) return cb(er)
if (d === made) {
return cb()
}
endMadeDir(self, d, cb)
})
}
function endMadeDir_ (self, p, cb) {
var dirProps = {}
Object.keys(self.props).forEach(function (k) {
dirProps[k] = self.props[k]
// only make non-readable dirs if explicitly requested.
if (k === 'mode' && self.type !== 'Directory') {
dirProps[k] = dirProps[k] | parseInt('0111', 8)
}
})
var todo = 3
var errState = null
fs.stat(p, function (er, current) {
if (er) return cb(errState = er)
endChmod(self, dirProps, current, p, next)
endChown(self, dirProps, current, p, next)
endUtimes(self, dirProps, current, p, next)
})
function next (er) {
if (errState) return
if (er) return cb(errState = er)
if (--todo === 0) return cb()
}
}
Writer.prototype.pipe = function () {
this.error("Can't pipe from writable stream")
}
Writer.prototype.add = function () {
this.error("Can't add to non-Directory type")
}
Writer.prototype.write = function () {
return true
}
function objectToString (d) {
return Object.prototype.toString.call(d)
}
function isDate (d) {
return typeof d === 'object' && objectToString(d) === '[object Date]'
}