mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-19 17:26:34 +00:00
Merge pull request #2080 from resin-io/sdk-write-prepare
feat(sdk): Move CLI writer logic into SDK writer
This commit is contained in:
commit
d233558b44
@ -16,24 +16,8 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const Bluebird = require('bluebird')
|
const Bluebird = require('bluebird')
|
||||||
const fs = Bluebird.promisifyAll(require('fs'))
|
|
||||||
const mountutils = Bluebird.promisifyAll(require('mountutils'))
|
|
||||||
const drivelist = Bluebird.promisifyAll(require('drivelist'))
|
|
||||||
const os = require('os')
|
|
||||||
const imageStream = require('../sdk/image-stream')
|
|
||||||
const errors = require('../shared/errors')
|
|
||||||
const constraints = require('../shared/drive-constraints')
|
|
||||||
const ImageWriter = require('../sdk/writer')
|
const ImageWriter = require('../sdk/writer')
|
||||||
const diskpart = require('./diskpart')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Timeout, in milliseconds, to wait before unmounting on success
|
|
||||||
* @constant
|
|
||||||
* @type {Number}
|
|
||||||
*/
|
|
||||||
const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Write an image to a disk drive
|
* @summary Write an image to a disk drive
|
||||||
@ -61,85 +45,18 @@ const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
|||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
exports.writeImage = (imagePath, drive, options, onProgress) => {
|
exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||||
return drivelist.listAsync().then((drives) => {
|
const writer = new ImageWriter({
|
||||||
const selectedDrive = _.find(drives, {
|
path: drive,
|
||||||
device: drive
|
imagePath,
|
||||||
})
|
verify: options.validateWriteOnSuccess,
|
||||||
|
checksumAlgorithms: [ 'crc32' ],
|
||||||
|
unmountOnSuccess: options.unmountOnSuccess
|
||||||
|
})
|
||||||
|
|
||||||
if (!selectedDrive) {
|
return new Bluebird((resolve, reject) => {
|
||||||
throw errors.createUserError({
|
writer.flash()
|
||||||
title: 'The selected drive was not found',
|
.on('error', reject)
|
||||||
description: `We can't find ${drive} in your system. Did you unplug the drive?`,
|
.on('progress', onProgress)
|
||||||
code: 'EUNPLUGGED'
|
.on('finish', resolve)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedDrive
|
|
||||||
}).then((driveObject) => {
|
|
||||||
return Bluebird.try(() => {
|
|
||||||
// Unmounting a drive in Windows means we can't write to it anymore
|
|
||||||
if (os.platform() === 'win32') {
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
return mountutils.unmountDiskAsync(driveObject.device)
|
|
||||||
}).then(() => {
|
|
||||||
return diskpart.clean(driveObject.device)
|
|
||||||
}).then(() => {
|
|
||||||
/* eslint-disable no-bitwise */
|
|
||||||
const flags = fs.constants.O_RDWR |
|
|
||||||
fs.constants.O_NONBLOCK |
|
|
||||||
fs.constants.O_SYNC
|
|
||||||
/* eslint-enable no-bitwise */
|
|
||||||
|
|
||||||
return fs.openAsync(driveObject.raw, flags)
|
|
||||||
}).then((driveFileDescriptor) => {
|
|
||||||
return imageStream.getFromFilePath(imagePath).then((image) => {
|
|
||||||
if (!constraints.isDriveLargeEnough(driveObject, image)) {
|
|
||||||
throw errors.createUserError({
|
|
||||||
title: 'The image you selected is too big for this drive',
|
|
||||||
description: 'Please connect a bigger drive and try again'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const writer = new ImageWriter({
|
|
||||||
image,
|
|
||||||
fd: driveFileDescriptor,
|
|
||||||
path: driveObject.raw,
|
|
||||||
verify: options.validateWriteOnSuccess,
|
|
||||||
checksumAlgorithms: [ 'crc32' ]
|
|
||||||
})
|
|
||||||
|
|
||||||
return writer.write()
|
|
||||||
}).then((writer) => {
|
|
||||||
return new Bluebird((resolve, reject) => {
|
|
||||||
writer.on('progress', onProgress)
|
|
||||||
writer.on('error', reject)
|
|
||||||
writer.on('finish', (results) => {
|
|
||||||
results.drive = driveObject
|
|
||||||
resolve(results)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}).tap(() => {
|
|
||||||
// Make sure the device stream file descriptor is closed
|
|
||||||
// before returning control the the caller. Not closing
|
|
||||||
// the file descriptor (and waiting for it) results in
|
|
||||||
// `EBUSY` errors when attempting to unmount the drive
|
|
||||||
// right afterwards in some Windows 7 systems.
|
|
||||||
return fs.closeAsync(driveFileDescriptor).then(() => {
|
|
||||||
if (!options.unmountOnSuccess) {
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closing a file descriptor on a drive containing mountable
|
|
||||||
// partitions causes macOS to mount the drive. If we try to
|
|
||||||
// unmount to quickly, then the drive might get re-mounted
|
|
||||||
// right afterwards.
|
|
||||||
return Bluebird.delay(UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
|
|
||||||
.return(driveObject.device)
|
|
||||||
.then(mountutils.unmountDiskAsync)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,11 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const os = require('os')
|
||||||
|
const fs = require('fs')
|
||||||
|
const EventEmitter = require('events').EventEmitter
|
||||||
|
const mountutils = require('mountutils')
|
||||||
|
const drivelist = require('drivelist')
|
||||||
const stream = require('readable-stream')
|
const stream = require('readable-stream')
|
||||||
const Pipage = require('pipage')
|
const Pipage = require('pipage')
|
||||||
const BlockMap = require('blockmap')
|
const BlockMap = require('blockmap')
|
||||||
@ -24,10 +29,54 @@ const BlockWriteStream = require('./block-write-stream')
|
|||||||
const BlockReadStream = require('./block-read-stream')
|
const BlockReadStream = require('./block-read-stream')
|
||||||
const ChecksumStream = require('./checksum-stream')
|
const ChecksumStream = require('./checksum-stream')
|
||||||
const ProgressStream = require('./progress-stream')
|
const ProgressStream = require('./progress-stream')
|
||||||
|
const imageStream = require('../image-stream')
|
||||||
|
const diskpart = require('../../cli/diskpart')
|
||||||
|
const constraints = require('../../shared/drive-constraints')
|
||||||
|
const errors = require('../../shared/errors')
|
||||||
const debug = require('debug')('etcher:writer')
|
const debug = require('debug')('etcher:writer')
|
||||||
const EventEmitter = require('events').EventEmitter
|
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
/* eslint-disable prefer-reflect */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Timeout, in milliseconds, to wait before unmounting on success
|
||||||
|
* @constant
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Helper function to run a set of async tasks in sequence
|
||||||
|
* @private
|
||||||
|
* @param {Array<Function>} tasks - set of tasks
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @example
|
||||||
|
* runSeries([
|
||||||
|
* (next) => first(next),
|
||||||
|
* (next) => second(next),
|
||||||
|
* ], (error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
const runSeries = (tasks, callback) => {
|
||||||
|
/**
|
||||||
|
* @summary Task runner
|
||||||
|
* @param {Error} [error] - error
|
||||||
|
* @example
|
||||||
|
* run()
|
||||||
|
*/
|
||||||
|
const run = (error) => {
|
||||||
|
const task = tasks.shift()
|
||||||
|
if (error || task == null) {
|
||||||
|
callback(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary ImageWriter class
|
* @summary ImageWriter class
|
||||||
* @class
|
* @class
|
||||||
@ -36,6 +85,11 @@ class ImageWriter extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @summary ImageWriter constructor
|
* @summary ImageWriter constructor
|
||||||
* @param {Object} options - options
|
* @param {Object} options - options
|
||||||
|
* @param {String} options.imagePath - disk image path
|
||||||
|
* @param {String} options.path - dest path
|
||||||
|
* @param {Boolean} options.verify - whether to verify the dest
|
||||||
|
* @param {Boolean} options.unmountOnSuccess - whether to unmount the dest after flashing
|
||||||
|
* @param {Array<String>} options.checksumAlgorithms - checksums to calculate
|
||||||
* @example
|
* @example
|
||||||
* new ImageWriter(options)
|
* new ImageWriter(options)
|
||||||
*/
|
*/
|
||||||
@ -48,13 +102,187 @@ class ImageWriter extends EventEmitter {
|
|||||||
this.pipeline = null
|
this.pipeline = null
|
||||||
this.target = null
|
this.target = null
|
||||||
|
|
||||||
this.hadError = false
|
|
||||||
|
|
||||||
this.bytesRead = 0
|
this.bytesRead = 0
|
||||||
this.bytesWritten = 0
|
this.bytesWritten = 0
|
||||||
this.checksum = {}
|
this.checksum = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Verify that the selected destination device exists
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @private
|
||||||
|
* @example
|
||||||
|
* writer.checkSelectedDevice((error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
checkSelectedDevice (callback) {
|
||||||
|
debug('state:device-select', this.options.path)
|
||||||
|
this.destinationDevice = null
|
||||||
|
drivelist.list((error, drives) => {
|
||||||
|
debug('state:device-select', this.options.path, error ? 'NOT OK' : 'OK')
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
callback.call(this, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDrive = _.find(drives, {
|
||||||
|
device: this.options.path
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!selectedDrive) {
|
||||||
|
const selectionError = errors.createUserError({
|
||||||
|
title: 'The selected drive was not found',
|
||||||
|
description: `We can't find ${this.options.path} in your system. Did you unplug the drive?`,
|
||||||
|
code: 'EUNPLUGGED'
|
||||||
|
})
|
||||||
|
debug('state:device-select', this.options.path, 'NOT OK')
|
||||||
|
callback.call(this, selectionError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destinationDevice = selectedDrive
|
||||||
|
|
||||||
|
callback.call(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Unmount the destination device
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @private
|
||||||
|
* @example
|
||||||
|
* writer.unmountDevice((error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
unmountDevice (callback) {
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
callback.call(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('state:unmount', this.destinationDevice.device)
|
||||||
|
|
||||||
|
mountutils.unmountDisk(this.destinationDevice.device, (error) => {
|
||||||
|
debug('state:unmount', this.destinationDevice.device, error ? 'NOT OK' : 'OK')
|
||||||
|
callback.call(this, error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Clean a device's partition table
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @private
|
||||||
|
* @example
|
||||||
|
* writer.removePartitionTable((error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
removePartitionTable (callback) {
|
||||||
|
if (os.platform() !== 'win32') {
|
||||||
|
callback.call(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('state:clean', this.destinationDevice.device)
|
||||||
|
|
||||||
|
diskpart.clean(this.destinationDevice.device).asCallback((error) => {
|
||||||
|
debug('state:clean', this.destinationDevice.device, error ? 'NOT OK' : 'OK')
|
||||||
|
callback.call(this, error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Open the source for reading
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @private
|
||||||
|
* @example
|
||||||
|
* writer.openSource((error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
openSource (callback) {
|
||||||
|
debug('state:source-open', this.options.imagePath)
|
||||||
|
imageStream.getFromFilePath(this.options.imagePath).asCallback((error, image) => {
|
||||||
|
debug('state:source-open', this.options.imagePath, error ? 'NOT OK' : 'OK')
|
||||||
|
if (error) {
|
||||||
|
callback.call(this, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!constraints.isDriveLargeEnough(this.destinationDevice, image)) {
|
||||||
|
const driveError = errors.createUserError({
|
||||||
|
title: 'The image you selected is too big for this drive',
|
||||||
|
description: 'Please connect a bigger drive and try again'
|
||||||
|
})
|
||||||
|
debug('state:source-open', this.options.imagePath, 'NOT OK')
|
||||||
|
callback.call(this, driveError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.image = image
|
||||||
|
|
||||||
|
callback.call(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Open the destination for writing
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @private
|
||||||
|
* @example
|
||||||
|
* writer.openDestination((error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
openDestination (callback) {
|
||||||
|
debug('state:destination-open', this.destinationDevice.raw)
|
||||||
|
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
const flags = fs.constants.O_RDWR |
|
||||||
|
fs.constants.O_NONBLOCK |
|
||||||
|
fs.constants.O_SYNC
|
||||||
|
/* eslint-enable no-bitwise */
|
||||||
|
|
||||||
|
fs.open(this.destinationDevice.raw, flags, (error, fd) => {
|
||||||
|
debug('state:destination-open', this.destinationDevice.raw, error ? 'NOT OK' : 'OK')
|
||||||
|
this.options.fd = fd
|
||||||
|
callback.call(this, error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Start the flashing process
|
||||||
|
* @returns {ImageWriter} imageWriter
|
||||||
|
* @example
|
||||||
|
* imageWriter.flash()
|
||||||
|
* .on('error', reject)
|
||||||
|
* .on('progress', onProgress)
|
||||||
|
* .on('finish', resolve)
|
||||||
|
*/
|
||||||
|
flash () {
|
||||||
|
const tasks = [
|
||||||
|
(next) => { this.checkSelectedDevice(next) },
|
||||||
|
(next) => { this.unmountDevice(next) },
|
||||||
|
(next) => { this.removePartitionTable(next) },
|
||||||
|
(next) => { this.openSource(next) },
|
||||||
|
(next) => { this.openDestination(next) }
|
||||||
|
]
|
||||||
|
|
||||||
|
runSeries(tasks, (error) => {
|
||||||
|
if (error) {
|
||||||
|
this.emit('error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.write()
|
||||||
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Start the writing process
|
* @summary Start the writing process
|
||||||
* @returns {ImageWriter} imageWriter
|
* @returns {ImageWriter} imageWriter
|
||||||
@ -62,15 +290,12 @@ class ImageWriter extends EventEmitter {
|
|||||||
* imageWriter.write()
|
* imageWriter.write()
|
||||||
*/
|
*/
|
||||||
write () {
|
write () {
|
||||||
this.hadError = false
|
|
||||||
|
|
||||||
this._createWritePipeline(this.options)
|
this._createWritePipeline(this.options)
|
||||||
.on('checksum', (checksum) => {
|
.on('checksum', (checksum) => {
|
||||||
debug('write:checksum', checksum)
|
debug('write:checksum', checksum)
|
||||||
this.checksum = checksum
|
this.checksum = checksum
|
||||||
})
|
})
|
||||||
.on('error', (error) => {
|
.on('error', (error) => {
|
||||||
this.hadError = true
|
|
||||||
this.emit('error', error)
|
this.emit('error', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -80,7 +305,7 @@ class ImageWriter extends EventEmitter {
|
|||||||
if (this.options.verify) {
|
if (this.options.verify) {
|
||||||
this.verify()
|
this.verify()
|
||||||
} else {
|
} else {
|
||||||
this._emitFinish()
|
this._finish()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -96,7 +321,6 @@ class ImageWriter extends EventEmitter {
|
|||||||
verify () {
|
verify () {
|
||||||
this._createVerifyPipeline(this.options)
|
this._createVerifyPipeline(this.options)
|
||||||
.on('error', (error) => {
|
.on('error', (error) => {
|
||||||
this.hadError = true
|
|
||||||
this.emit('error', error)
|
this.emit('error', error)
|
||||||
})
|
})
|
||||||
.on('checksum', (checksum) => {
|
.on('checksum', (checksum) => {
|
||||||
@ -106,13 +330,13 @@ class ImageWriter extends EventEmitter {
|
|||||||
error.code = 'EVALIDATION'
|
error.code = 'EVALIDATION'
|
||||||
this.emit('error', error)
|
this.emit('error', error)
|
||||||
}
|
}
|
||||||
this._emitFinish()
|
this._finish()
|
||||||
})
|
})
|
||||||
.on('finish', () => {
|
.on('finish', () => {
|
||||||
debug('verify:end')
|
debug('verify:end')
|
||||||
|
|
||||||
// NOTE: As the 'checksum' event only happens after
|
// NOTE: As the 'checksum' event only happens after
|
||||||
// the 'finish' event, we `._emitFinish()` there instead of here
|
// the 'finish' event, we `._finish()` there instead of here
|
||||||
})
|
})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
@ -125,22 +349,56 @@ class ImageWriter extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
abort () {
|
abort () {
|
||||||
if (this.source) {
|
if (this.source) {
|
||||||
this.emit('abort')
|
|
||||||
this.source.destroy()
|
this.source.destroy()
|
||||||
}
|
}
|
||||||
|
this.emit('abort')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Cleanup after writing; close file descriptors & unmount
|
||||||
|
* @param {Function} callback - callback(error)
|
||||||
|
* @private
|
||||||
|
* @example
|
||||||
|
* writer._cleanup((error) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
_cleanup (callback) {
|
||||||
|
debug('state:cleanup')
|
||||||
|
fs.close(this.options.fd, (closeError) => {
|
||||||
|
debug('state:cleanup', closeError ? 'NOT OK' : 'OK')
|
||||||
|
if (!this.options.unmountOnSuccess) {
|
||||||
|
callback.call(this, closeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing a file descriptor on a drive containing mountable
|
||||||
|
// partitions causes macOS to mount the drive. If we try to
|
||||||
|
// unmount too quickly, then the drive might get re-mounted
|
||||||
|
// right afterwards.
|
||||||
|
setTimeout(() => {
|
||||||
|
mountutils.unmountDisk(this.destinationDevice.device, (error) => {
|
||||||
|
debug('state:cleanup', error ? 'NOT OK' : 'OK')
|
||||||
|
callback.call(this, error)
|
||||||
|
})
|
||||||
|
}, UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Emits the `finish` event with state metadata
|
* @summary Emits the `finish` event with state metadata
|
||||||
* @private
|
* @private
|
||||||
* @example
|
* @example
|
||||||
* this._emitFinish()
|
* this._finish()
|
||||||
*/
|
*/
|
||||||
_emitFinish () {
|
_finish () {
|
||||||
this.emit('finish', {
|
this._cleanup(() => {
|
||||||
bytesRead: this.bytesRead,
|
this.emit('finish', {
|
||||||
bytesWritten: this.bytesWritten,
|
drive: this.destinationDevice,
|
||||||
checksum: this.checksum
|
bytesRead: this.bytesRead,
|
||||||
|
bytesWritten: this.bytesWritten,
|
||||||
|
checksum: this.checksum
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,9 +474,6 @@ class ImageWriter extends EventEmitter {
|
|||||||
|
|
||||||
const target = new BlockWriteStream({
|
const target = new BlockWriteStream({
|
||||||
fd: options.fd,
|
fd: options.fd,
|
||||||
path: options.path,
|
|
||||||
flags: options.flags,
|
|
||||||
mode: options.mode,
|
|
||||||
autoClose: false
|
autoClose: false
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -296,9 +551,6 @@ class ImageWriter extends EventEmitter {
|
|||||||
|
|
||||||
const source = new BlockReadStream({
|
const source = new BlockReadStream({
|
||||||
fd: options.fd,
|
fd: options.fd,
|
||||||
path: options.path,
|
|
||||||
flags: options.flags,
|
|
||||||
mode: options.mode,
|
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
start: 0,
|
start: 0,
|
||||||
end: size
|
end: size
|
||||||
|
Loading…
x
Reference in New Issue
Block a user