From b61109a269ad12946c62e913becee94946b09081 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 29 Mar 2019 14:40:35 +0100 Subject: [PATCH] Fix reading images from network drives on windows Change-type: patch --- lib/gui/app/os/windows-network-drives.js | 122 ++++++++++++++++++ .../pages/main/controllers/image-selection.js | 63 ++++----- tests/data/wmic-output.txt | Bin 0 -> 152 bytes tests/gui/os/windows-network-drives.spec.js | 47 +++++++ 4 files changed, 203 insertions(+), 29 deletions(-) create mode 100644 lib/gui/app/os/windows-network-drives.js create mode 100755 tests/data/wmic-output.txt create mode 100644 tests/gui/os/windows-network-drives.spec.js diff --git a/lib/gui/app/os/windows-network-drives.js b/lib/gui/app/os/windows-network-drives.js new file mode 100644 index 00000000..2d41d85f --- /dev/null +++ b/lib/gui/app/os/windows-network-drives.js @@ -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} - { 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>} - '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 +} diff --git a/lib/gui/app/pages/main/controllers/image-selection.js b/lib/gui/app/pages/main/controllers/image-selection.js index e7ad1313..3060c713 100644 --- a/lib/gui/app/pages/main/controllers/image-selection.js +++ b/lib/gui/app/pages/main/controllers/image-selection.js @@ -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 + } + } } /** diff --git a/tests/data/wmic-output.txt b/tests/data/wmic-output.txt new file mode 100755 index 0000000000000000000000000000000000000000..b6fbbd1d922bf2fcf6e8aa793ff681b2aaa08866 GIT binary patch literal 152 zcmezW&xIkCp^PDuAsI+}GPp1(0AT<_5ko#$GzBPH#Nfw}$dC(Uf#mQ*UIs3PC { + 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() + }) +})