diff --git a/lib/gui/app/os/windows-network-drives.js b/lib/gui/app/os/windows-network-drives.js deleted file mode 100755 index 700fbf8b..00000000 --- a/lib/gui/app/os/windows-network-drives.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2019 balena.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 Bluebird = require('bluebird') -const cp = require('child_process') -const fs = require('fs') -const _ = require('lodash') -const os = require('os') -const Path = require('path') -const process = require('process') -const { promisify } = require('util') - -const { tmpFileDisposer } = require('../../../shared/utils') - -const readFileAsync = promisify(fs.readFile) - -const execAsync = promisify(cp.exec) - -/** - * @summary Returns wmic's output for network drives - * @function - * - * @returns {Promise} - * - * @example - * const output = await getWmicNetworkDrivesOutput() - */ -exports.getWmicNetworkDrivesOutput = async () => { - // Exported for tests. - // When trying to read wmic's stdout directly from node, it is encoded with the current - // console codepage (depending on the computer). - // Decoding this would require getting this codepage somehow and using iconv as node - // doesn't know how to read cp850 directly for example. - // We could also use wmic's "/output:" switch but it doesn't work when the filename - // contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example). - // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. - const options = { - - // Close the file once it's created - discardDescriptor: true, - - // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") - prefix: 'tmp' - } - return Bluebird.using(tmpFileDisposer(options), async ({ path }) => { - const command = [ - Path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'), - 'path', - 'Win32_LogicalDisk', - 'Where', - 'DriveType="4"', - 'get', - 'DeviceID,ProviderName', - '>', - `"${path}"` - ] - await execAsync(command.join(' '), { windowsHide: true }) - return readFileAsync(path, 'ucs2') - }) -} - -/** - * @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 exports.getWmicNetworkDrivesOutput() - const couples = _.chain(result) - .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}`) - } - // eslint-disable-next-line no-magic-numbers - return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ] - }) - // eslint-disable-next-line no-magic-numbers - .filter((couple) => couple[1].length > 0) - .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/os/windows-network-drives.ts b/lib/gui/app/os/windows-network-drives.ts new file mode 100755 index 00000000..6fffb515 --- /dev/null +++ b/lib/gui/app/os/windows-network-drives.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2019 balena.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. + */ + +import { using } from 'bluebird'; +import { exec } from 'child_process'; +import { readFile } from 'fs'; +import { chain, trim } from 'lodash'; +import { platform } from 'os'; +import { join } from 'path'; +import { env } from 'process'; +import { promisify } from 'util'; + +import { tmpFileDisposer } from '../../../shared/utils'; + +const readFileAsync = promisify(readFile); + +const execAsync = promisify(exec); + +/** + * @summary Returns wmic's output for network drives + */ +export async function getWmicNetworkDrivesOutput(): Promise { + // Exported for tests. + // When trying to read wmic's stdout directly from node, it is encoded with the current + // console codepage (depending on the computer). + // Decoding this would require getting this codepage somehow and using iconv as node + // doesn't know how to read cp850 directly for example. + // We could also use wmic's "/output:" switch but it doesn't work when the filename + // contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example). + // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. + const options = { + // Close the file once it's created + discardDescriptor: true, + // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") + prefix: 'tmp', + }; + return using(tmpFileDisposer(options), async ({ path }) => { + const command = [ + join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'), + 'path', + 'Win32_LogicalDisk', + 'Where', + 'DriveType="4"', + 'get', + 'DeviceID,ProviderName', + '>', + `"${path}"`, + ]; + await execAsync(command.join(' '), { windowsHide: true }); + return readFileAsync(path, 'ucs2'); + }); +} + +/** + * @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public' + */ +async function getWindowsNetworkDrives(): Promise> { + // Use getWindowsNetworkDrives from "exports." so it can be mocked in tests + const result = await exports.getWmicNetworkDrivesOutput(); + const couples: Array<[string, string]> = chain(result) + .split('\n') + // Remove header line + .slice(1) + // Remove extra spaces / tabs / carriage returns + .invokeMap(String.prototype.trim) + // Filter out empty lines + .compact() + .map((str: string): [string, string] => { + const colonPosition = str.indexOf(':'); + if (colonPosition === -1) { + throw new Error(`Can't parse wmic output: ${result}`); + } + return [ + str.slice(0, colonPosition + 1), + trim(str.slice(colonPosition + 1)), + ]; + }) + .filter(couple => couple[1].length > 0) + .value(); + return new Map(couples); +} + +/** + * @summary Replaces network drive letter with network drive location in the provided filePath on Windows + */ +export async function replaceWindowsNetworkDriveLetter( + filePath: string, +): Promise { + let result = filePath; + if (platform() === 'win32') { + const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath); + if (matches !== null) { + const [, drive, relativePath] = matches; + const drives = await getWindowsNetworkDrives(); + const location = drives.get(drive); + if (location !== undefined) { + result = `${location}\\${relativePath}`; + } + } + } + return result; +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c2122d65..0c756ab7 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1193,9 +1193,9 @@ "dev": true }, "@types/node": { - "version": "6.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.9.tgz", - "integrity": "sha512-leP/gxHunuazPdZaCvsCefPQxinqUDsCxCR5xaDUrY2MkYxQRFZZwU5e7GojyYsGB7QVtCi7iVEl/hoFXQYc+w==" + "version": "12.12.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz", + "integrity": "sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -6054,6 +6054,13 @@ "unzip-stream": "^0.3.0", "xxhash": "^0.3.0", "yauzl": "^2.9.2" + }, + "dependencies": { + "@types/node": { + "version": "6.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.9.tgz", + "integrity": "sha512-leP/gxHunuazPdZaCvsCefPQxinqUDsCxCR5xaDUrY2MkYxQRFZZwU5e7GojyYsGB7QVtCi7iVEl/hoFXQYc+w==" + } } }, "event-emitter": { @@ -10037,6 +10044,13 @@ "@types/node": "^6.0.112", "@types/usb": "^1.5.1", "debug": "^3.1.0" + }, + "dependencies": { + "@types/node": { + "version": "6.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.9.tgz", + "integrity": "sha512-leP/gxHunuazPdZaCvsCefPQxinqUDsCxCR5xaDUrY2MkYxQRFZZwU5e7GojyYsGB7QVtCi7iVEl/hoFXQYc+w==" + } } }, "node-releases": { diff --git a/package.json b/package.json index 5321edc0..e0915bca 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@babel/plugin-proposal-function-bind": "^7.2.0", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", + "@types/node": "^12.12.24", "@types/react-dom": "^16.8.4", "babel-loader": "^8.0.4", "chalk": "^1.1.3", diff --git a/tests/gui/os/windows-network-drives.spec.js b/tests/gui/os/windows-network-drives.spec.js index 628e1a8f..6494f553 100644 --- a/tests/gui/os/windows-network-drives.spec.js +++ b/tests/gui/os/windows-network-drives.spec.js @@ -22,6 +22,7 @@ const m = require('mochainon') const { env } = require('process') const { promisify } = require('util') +// eslint-disable-next-line node/no-missing-require const wnd = require('../../../lib/gui/app/os/windows-network-drives') const readFileAsync = promisify(readFile)