mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-23 19:26:33 +00:00
Fix reading images from network drives on windows
Change-type: patch
This commit is contained in:
parent
164fd8f022
commit
b61109a269
122
lib/gui/app/os/windows-network-drives.js
Normal file
122
lib/gui/app/os/windows-network-drives.js
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const cp = require('child_process')
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
|
||||
/**
|
||||
* @summary Promisified child_process.execFile
|
||||
* @function
|
||||
*
|
||||
* @param {String} file - command
|
||||
* @param {String[]} args - arguments
|
||||
* @param {Object} options - child_process.execFile options
|
||||
*
|
||||
* @returns {Promise<Object>} - { stdout, stderr }
|
||||
*
|
||||
* @example
|
||||
* execFileAsync('ls', [ '.' ])
|
||||
* .then(console.log);
|
||||
*/
|
||||
const execFileAsync = async (file, args, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cp.execFile(
|
||||
file,
|
||||
args,
|
||||
options,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve({ stdout, stderr })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary returns a Map of drive letter -> network locations on Windows
|
||||
* @function
|
||||
*
|
||||
* @returns {Promise<Map<String, String>>} - 'Z:' -> '\\\\192.168.0.1\\Public'
|
||||
*
|
||||
* @example
|
||||
* getWindowsNetworkDrives()
|
||||
* .then(console.log);
|
||||
*/
|
||||
const getWindowsNetworkDrives = async () => {
|
||||
const result = await execFileAsync(
|
||||
'wmic',
|
||||
[ 'path', 'Win32_LogicalDisk', 'Where', 'DriveType="4"', 'get', 'DeviceID,ProviderName' ],
|
||||
{ windowsHide: true, windowsVerbatimArguments: true }
|
||||
)
|
||||
const couples = _.chain(result.stdout)
|
||||
.split('\n')
|
||||
|
||||
// Remove header line
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
.slice(1)
|
||||
|
||||
// Remove extra spaces / tabs / carriage returns
|
||||
.invokeMap(String.prototype.trim)
|
||||
|
||||
// Filter out empty lines
|
||||
.compact()
|
||||
.map((str) => {
|
||||
const colonPosition = str.indexOf(':')
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (colonPosition === -1) {
|
||||
throw new Error(`Can't parse wmic output: ${result.stdout}`)
|
||||
}
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ]
|
||||
})
|
||||
.value()
|
||||
return new Map(couples)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Replaces network drive letter with network drive location in the provided filePath on Windows
|
||||
* @function
|
||||
*
|
||||
* @param {String} filePath - file path
|
||||
*
|
||||
* @returns {String} - updated file path
|
||||
*
|
||||
* @example
|
||||
* replaceWindowsNetworkDriveLetter('Z:\\some-file')
|
||||
* .then(console.log);
|
||||
*/
|
||||
exports.replaceWindowsNetworkDriveLetter = async (filePath) => {
|
||||
let result = filePath
|
||||
if (os.platform() === 'win32') {
|
||||
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath)
|
||||
if (matches !== null) {
|
||||
const [ , drive, relativePath ] = matches
|
||||
const drives = await getWindowsNetworkDrives()
|
||||
const location = drives.get(drive)
|
||||
// eslint-disable-next-line no-undefined
|
||||
if (location !== undefined) {
|
||||
result = `${location}\\${relativePath}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
@ -29,6 +29,7 @@ const analytics = require('../../../modules/analytics')
|
||||
const settings = require('../../../models/settings')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
const { replaceWindowsNetworkDriveLetter } = require('../../../os/windows-network-drives')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
|
||||
module.exports = function (
|
||||
@ -145,7 +146,13 @@ module.exports = function (
|
||||
* @example
|
||||
* ImageSelectionController.selectImageByPath('path/to/image.img');
|
||||
*/
|
||||
this.selectImageByPath = (imagePath) => {
|
||||
this.selectImageByPath = async (imagePath) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath)
|
||||
} catch (error) {
|
||||
analytics.logException(error)
|
||||
}
|
||||
if (!supportedFormats.isSupportedImage(imagePath)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
@ -158,35 +165,33 @@ module.exports = function (
|
||||
}
|
||||
|
||||
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
||||
source.getInnerSource()
|
||||
.then((innerSource) => {
|
||||
return innerSource.getMetadata()
|
||||
.then((metadata) => {
|
||||
return innerSource.getPartitionTable()
|
||||
.then((partitionTable) => {
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true
|
||||
metadata.partitions = partitionTable.partitions
|
||||
}
|
||||
metadata.path = imagePath
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
metadata.extension = path.extname(imagePath).slice(1)
|
||||
this.selectImage(metadata)
|
||||
$timeout()
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(path.basename(imagePath), error.message)
|
||||
})
|
||||
osDialog.showError(imageError)
|
||||
analytics.logException(error)
|
||||
})
|
||||
.finally(() => {
|
||||
return source.close().catch(_.noop)
|
||||
try {
|
||||
const innerSource = await source.getInnerSource()
|
||||
const metadata = await innerSource.getMetadata()
|
||||
const partitionTable = await innerSource.getPartitionTable()
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true
|
||||
metadata.partitions = partitionTable.partitions
|
||||
}
|
||||
metadata.path = imagePath
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
metadata.extension = path.extname(imagePath).slice(1)
|
||||
this.selectImage(metadata)
|
||||
$timeout()
|
||||
} catch (error) {
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(path.basename(imagePath), error.message)
|
||||
})
|
||||
osDialog.showError(imageError)
|
||||
analytics.logException(error)
|
||||
} finally {
|
||||
try {
|
||||
await source.close()
|
||||
} catch (error) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
BIN
tests/data/wmic-output.txt
Executable file
BIN
tests/data/wmic-output.txt
Executable file
Binary file not shown.
47
tests/gui/os/windows-network-drives.spec.js
Normal file
47
tests/gui/os/windows-network-drives.spec.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const { readFile } = require('fs')
|
||||
const os = require('os')
|
||||
const cp = require('child_process')
|
||||
const m = require('mochainon')
|
||||
const { promisify } = require('util')
|
||||
|
||||
const { replaceWindowsNetworkDriveLetter } = require('../../../lib/gui/app/os/windows-network-drives')
|
||||
|
||||
const readFileAsync = promisify(readFile)
|
||||
|
||||
describe('Network drives on Windows', () => {
|
||||
before(async () => {
|
||||
this.osPlatformStub = m.sinon.stub(os, 'platform')
|
||||
this.osPlatformStub.returns('win32')
|
||||
const wmicOutput = await readFileAsync('tests/data/wmic-output.txt', { encoding: 'ucs2' })
|
||||
this.execFileStub = m.sinon.stub(cp, 'execFile')
|
||||
this.execFileStub.callsArgWith(3, null, wmicOutput)
|
||||
})
|
||||
|
||||
it('should parse network drive mapping on Windows', async () => {
|
||||
m.chai.expect(await replaceWindowsNetworkDriveLetter('Z:\\some-folder\\some-file'))
|
||||
.to.equal('\\\\192.168.1.1\\Public\\some-folder\\some-file')
|
||||
})
|
||||
|
||||
after(() => {
|
||||
this.osPlatformStub.restore()
|
||||
this.execFileStub.restore()
|
||||
})
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user