From c00b7b62d6ea2e1213a0b02722f0c364c2622d7d Mon Sep 17 00:00:00 2001 From: Benedict Aas Date: Wed, 16 May 2018 22:28:20 +0100 Subject: [PATCH] fix: add missing files module We add a convenience module for file and path operations. Tests included. Change-Type: patch --- .../file-selector/file-selector.jsx | 7 +- lib/shared/files.js | 188 ++++++++++++++++++ tests/shared/files.spec.js | 49 +++++ 3 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 lib/shared/files.js create mode 100644 tests/shared/files.spec.js diff --git a/lib/gui/app/components/file-selector/file-selector/file-selector.jsx b/lib/gui/app/components/file-selector/file-selector/file-selector.jsx index 2ddcbb57..f46d84d3 100644 --- a/lib/gui/app/components/file-selector/file-selector/file-selector.jsx +++ b/lib/gui/app/components/file-selector/file-selector/file-selector.jsx @@ -18,6 +18,7 @@ const _ = require('lodash') const os = require('os') +const fs = require('fs') const path = require('path') const React = require('react') const propTypes = require('prop-types') @@ -224,7 +225,7 @@ class RecentFilesUnstyled extends React.PureComponent { render () { const existing = (fileObjs) => { return _.filter(fileObjs, (fileObj) => { - return files.exists(fileObj.fullpath) + return fs.existsSync(fileObj.fullpath) }) } @@ -460,7 +461,7 @@ class FileSelector extends React.PureComponent { } setFilesProgressively (dirname) { - return files.getDirectory(dirname).then((basenames) => { + return fs.readdirAsync(dirname).then((basenames) => { const fileObjs = basenames.map((basename) => { return { dirname: this.state.path, @@ -496,6 +497,8 @@ class FileSelector extends React.PureComponent { this.setFilesProgressively(file.fullpath).then(() => { this.setState({ path: file.fullpath }) + }).catch((error) => { + this.setState({ error: error.message }) }) } diff --git a/lib/shared/files.js b/lib/shared/files.js new file mode 100644 index 00000000..f7e7a9d4 --- /dev/null +++ b/lib/shared/files.js @@ -0,0 +1,188 @@ +/* + * Copyright 2018 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 _ = require('lodash') +const path = require('path') +const Bluebird = require('bluebird') +const fs = Bluebird.promisifyAll(require('fs')) + +/* eslint-disable lodash/prefer-lodash-method */ + +/** + * @summary Get file metadata + * @function + * @private + * + * @param {String} dirname - directory name + * @param {String} [basename] - custom basename to append + * @returns {Object} file metadata + * + * @example + * try { + * const file = files.getFileMetadataSync('/home/user') + * console.log(`Is ${file.basename} a directory? ${file.isDirectory}`) + * } catch (error) { + * console.error(error) + * } + */ +exports.getFileMetadataSync = (dirname, basename = '') => { + // TODO(Shou): use path.parse object information here + const fullpath = path.join(dirname, basename) + const pathObj = path.parse(fullpath) + + // TODO(Shou): this is not true for Windows, figure out Windows hidden files + const isHidden = pathObj.base.startsWith('.') + const stats = fs.statSync(fullpath) + + return { + basename: pathObj.base, + dirname: pathObj.dir, + fullpath, + extension: pathObj.ext.replace('.', ''), + name: pathObj.name, + isDirectory: stats.isDirectory(), + isHidden, + size: stats.size + } +} + +/** + * @summary Get file metadata asynchronously + * @function + * @private + * + * @param {String} fullpath - full path + * @returns {Promise} promise of file metadata + * + * @example + * files.getFileMetadataAsync('/home/user').then((file) => { + * console.log(`Is ${file.basename} a directory? ${file.isDirectory}`) + * }) + */ +exports.getFileMetadataAsync = (fullpath) => { + const pathObj = path.parse(fullpath) + + // NOTE(Shou): this is not true for Windows + const isHidden = pathObj.base.startsWith('.') + + return fs.statAsync(fullpath).then((stats) => { + return { + basename: pathObj.base, + dirname: pathObj.dir, + fullpath, + extension: pathObj.ext.replace('.', ''), + name: pathObj.name, + isDirectory: stats.isDirectory(), + isHidden, + size: stats.size + } + }) +} + +/** + * @summary Get file metadata for a list of filenames + * @function + * @public + * + * @description Note that this omits any file that errors + * + * @param {String} dirname - directory path + * @param {Array} basenames - file names + * @returns {Promise>} promise of file objects + * + * @example + * files.getAllFilesMetadataAsync(os.homedir(), [ 'file1.txt', 'file2.txt' ]) + */ +exports.getAllFilesMetadataAsync = (dirname, basenames) => { + return Bluebird.all(basenames.map((basename) => { + const metadata = exports.getFileMetadataAsync(path.join(dirname, basename)) + return metadata.reflect() + })).reduce((fileMetas, inspection) => { + if (inspection.isFulfilled()) { + return fileMetas.concat(inspection.value()) + } + + return fileMetas + }, []) +} + +/** + * @summary Split a path on it's separator(s) + * @function + * @public + * + * @param {String} fullpath - full path to split + * @param {Array} [subpaths] - this param shouldn't normally be used + * @returns {Array} + * + * @example + * console.log(splitPath(path.join(os.homedir(), 'Downloads')) + * // Linux + * > [ '/', 'home', 'user', 'Downloads' ] + * // Windows + * > [ 'C:', 'Users', 'user', 'Downloads' ] + */ +exports.splitPath = (fullpath, subpaths = []) => { + const { + base, + dir, + root + } = path.parse(fullpath) + const isAbsolute = path.isAbsolute(fullpath) + + // Takes care of 'relative/path' + if (!isAbsolute && dir === '') { + return [ base ].concat(subpaths) + + // Takes care of '/' + } else if (isAbsolute && base === '') { + return [ root ].concat(subpaths) + } + + return exports.splitPath(dir, [ base ].concat(subpaths)) +} + +/** + * @summary Get all subpaths contained in a path + * @function + * @private + * + * @param {String} fullpath - path string + * @returns {Array} - all subpaths as file objects + * + * @example + * const subpaths = files.subpaths('/home/user/Downloads') + * console.log(subpaths.map(file => file.fullpath)) + * // Linux/macOS + * > [ '/', '/home', '/home/user', '/home/user/Downloads' ] + * // Windows + * > [ 'C:', 'Users', 'user', 'Downloads' ] + */ +exports.subpaths = (fullpath) => { + if (!_.isString(fullpath)) { + return null + } + + const dirs = exports.splitPath(fullpath) + + return _.map(dirs, (dir, index) => { + // eslint-disable-next-line no-magic-numbers + const subdir = dirs.slice(0, index + 1) + return exports.getFileMetadataSync(path.join(...subdir)) + }) +} diff --git a/tests/shared/files.spec.js b/tests/shared/files.spec.js new file mode 100644 index 00000000..09498fe5 --- /dev/null +++ b/tests/shared/files.spec.js @@ -0,0 +1,49 @@ +/* + * Copyright 2018 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 m = require('mochainon') +const path = require('path') +const files = require('../../lib/shared/files') + +describe('Shared: Files', function () { + describe('.splitPath()', function () { + it('should handle a root directory', function () { + const { root } = path.parse(__dirname) + const dirs = files.splitPath(root) + m.chai.expect(dirs).to.deep.equal([ root ]) + }) + + it('should handle relative paths', function () { + const dirs = files.splitPath(path.join('relative', 'dir', 'test')) + m.chai.expect(dirs).to.deep.equal([ 'relative', 'dir', 'test' ]) + }) + + it('should handle absolute paths', function () { + let dir + if (process.platform === 'win32') { + dir = 'C:\\Users\\user\\Downloads' + const dirs = files.splitPath(dir) + m.chai.expect(dirs).to.deep.equal([ 'C:\\', 'Users', 'user', 'Downloads' ]) + } else { + dir = '/Users/user/Downloads' + const dirs = files.splitPath(dir) + m.chai.expect(dirs).to.deep.equal([ '/', 'Users', 'user', 'Downloads' ]) + } + }) + }) +})