}
- */
-const icons = {
- faFileAlt: fontAwesome.icon(faFileAlt).html[0],
- faFolder: fontAwesome.icon(faFolder).html[0],
- faAngleLeft: fontAwesome.icon(faAngleLeft).html[0],
- faHdd: fontAwesome.icon(faHdd).html[0]
-}
-
-/**
- * @summary Icon React component
- * @class
- * @type {ReactElement}
- */
-class UnstyledIcon extends React.PureComponent {
- render () {
- const { type, ...restProps } = this.props
-
- return (
-
- )
- }
-}
-
-/**
- * @summary Icon Styled component
- * @function
- * @type {StyledComponent}
- */
-const Icon = styled(UnstyledIcon)`
- color: ${props => props.color};
- font-size: ${props => props.size};
-`
+const debug = require('debug')('etcher:gui:file-selector')
/**
* @summary Flex styled component
@@ -149,76 +55,6 @@ const Flex = styled.div`
flex-grow: ${ props => props.grow };
`
-class UnstyledFileLink extends React.PureComponent {
- constructor (props) {
- super(props)
-
- this.highlightFile = this.highlightFile.bind(this)
- this.selectFile = this.selectFile.bind(this)
- }
-
- render () {
- const icon = this.props.file.isDirectory ? 'faFolder' : 'faFileAlt'
-
- return (
-
-
-
- { middleEllipsis(this.props.file.basename || '', FILENAME_CHAR_LIMIT_SHORT) }
-
- { prettyBytes(this.props.file.size || 0) }
-
- )
- }
-
- highlightFile () {
- this.props.highlightFile(this.props.file)
- }
-
- selectFile () {
- this.props.selectFile(this.props.file)
- }
-}
-
-const FileLink = styled(UnstyledFileLink)`
- width: 100px;
- max-height: 128px;
- margin: 5px 10px;
- padding: 5px;
- background-color: ${ props => props.highlight ? colors.highlight.background : 'none' };
- transition: 0.15s background-color ease-out;
- color: ${ props => props.highlight ? colors.highlight.color : colors.primary.color };
- cursor: pointer;
- border-radius: 5px;
-
- > span:first-of-type {
- align-self: center;
- line-height: 1;
- margin-bottom: 6px;
- color: ${ props => props.highlight ? colors.highlight.color : colors.soft.color }
- }
-
- > span:last-of-type {
- display: flex;
- justify-content: center;
- text-align: center;
- word-break: break-all;
- font-size: 16px;
- }
-
- > div:last-child {
- background-color: ${ props => props.highlight ? colors.highlight.background : 'none' }
- color: ${ props => props.highlight ? colors.highlight.color : colors.primary.subColor }
- text-align: center;
- font-size: 12px;
- }
-`
-
const Header = Flex.extend`
margin: 10px 15px 0;
@@ -238,527 +74,177 @@ const Footer = Flex.extend`
}
`
-class UnstyledFileListWrap extends React.PureComponent {
- constructor (props) {
- super(props)
-
- this.scrollElem = null
-
- this.setScrollElem = this.setScrollElem.bind(this)
- }
-
- render () {
- return (
-
- { this.props.children }
-
- )
- }
-
- setScrollElem (scrollElem) {
- this.scrollElem = scrollElem
- }
-
- componentDidUpdate (prevProps) {
- if (prevProps.path !== this.props.path && this.scrollElem) {
- this.scrollElem.scrollTop = 0
- }
- }
-}
-
-const FileListWrap = styled(UnstyledFileListWrap)`
- overflow-x: hidden;
- overflow-y: auto;
- padding: 0 20px;
-`
-
-class RecentFileLink extends React.PureComponent {
- constructor (props) {
- super(props)
-
- this.select = this.select.bind(this)
- }
-
- render () {
- return (
-
- { middleEllipsis(path.basename(this.props.title), FILENAME_CHAR_LIMIT_SHORT) }
-
- )
- }
-
- select () {
- files.getFileMetadataAsync(this.props.fullpath).then(this.props.selectFile)
- }
-}
-
-class RecentFilesUnstyled extends React.PureComponent {
- constructor (props) {
- super(props)
-
- this.state = {
- recents: recentStorage.get('recents', []),
- favorites: recentStorage.get('favorites', [])
- }
- }
-
- render () {
- return (
-
- Recent
- {
- _.map(this.state.recents, (file) => {
- return (
-
- )
- })
- }
- Favorite
- {
- _.map(this.state.favorites.slice(0, 4), (file) => {
- return (
-
- )
- })
- }
-
- )
- }
-
- componentWillMount () {
- window.addEventListener('storage', this.onStorage)
- }
-
- componentWillUnmount () {
- window.removeEventListener('storage', this.onStorage)
- }
-
- componentDidMount () {
- Bluebird.reduce(this.state.recents, (newRecents, recent) => {
- return files.existsAsync(recent.fullpath).then((exists) => {
- if (exists) {
- return newRecents.concat(recent)
- }
-
- return newRecents
- })
- }, []).then((recents) => {
- this.setState({ recents })
- })
-
- Bluebird.reduce(this.state.favorites, (newFavorites, favorite) => {
- return files.existsAsync(favorite.fullpath).then((exists) => {
- if (exists) {
- return newFavorites.concat(favorite)
- }
-
- return newFavorites
- })
- }, []).then((favorites) => {
- this.setState({ favorites })
- })
- }
-
- onStorage (event) {
- if (event.key === RECENT_FILES_KEY) {
- this.setState(event.newValue)
- }
- }
-}
-
-const RecentFiles = styled(RecentFilesUnstyled)`
- display: flex;
- flex: 0 0 auto;
- flex-direction: column;
- align-items: flex-start;
- width: 130px;
- background-color: ${ colors.secondary.background };
- padding: 20px;
- color: ${ colors.secondary.color };
-
- > h5 {
- color: ${ colors.secondary.title };
- font-size: 11px;
- font-weight: 500;
- text-transform: uppercase;
- margin-bottom: 15px;
- }
-
- > h5:last-of-type {
- margin-top: 20px;
- }
-
- > button {
- margin-bottom: 10px;
- text-align: start;
- font-size: 16px;
- }
-`
-
-const labels = {
- '/': 'Root',
- 'mountpoints': 'Mountpoints'
-}
-
-class Crumb extends React.PureComponent {
- constructor (props) {
- super(props)
-
- this.selectFile = this.selectFile.bind(this)
- }
-
- render () {
- return (
-
-
- { middleEllipsis(labels[this.props.dir.fullpath] || this.props.dir.basename, FILENAME_CHAR_LIMIT_SHORT) }
-
-
- )
- }
-
- selectFile () {
- this.props.selectFile(this.props.dir)
- }
-}
-
-class UnstyledBreadcrumbs extends React.PureComponent {
- render () {
- const folderConstraints = this.props.constraints.length
- ? this.props.constraints
- : [ path.parse(this.props.path).root ]
-
- const dirs = files.subpaths(this.props.path).filter((subpath) => {
- // Guard against displaying folders outside the constrained folders
- return folderConstraints.some((folderConstraint) => {
- return !path.relative(folderConstraint, subpath.fullpath).startsWith('..')
- })
- })
-
- return (
-
- { dirs.length > MAX_DIR_CRUMBS ? '... / ' : null }
- {
- _.map(dirs.slice(-MAX_DIR_CRUMBS), (dir, index) => {
- return (
-
- )
- })
- }
-
- )
- }
-}
-
-const Breadcrumbs = styled(UnstyledBreadcrumbs)`
- font-size: 18px;
-
- & > button:not(:last-child)::after {
- content: '/';
- margin: 9px;
- }
-`
-
class FileSelector extends React.PureComponent {
constructor (props) {
super(props)
-
+ this.highlighted = null
this.state = {
path: props.path,
files: [],
- history: [],
- highlighted: null,
- error: null
+ }
+ }
+
+ confirmSelection () {
+ if (this.highlighted) {
+ this.selectFile(this.highlighted)
+ }
+ }
+
+ close () {
+ this.props.close()
+ }
+
+ componentDidUpdate () {
+ debug('FileSelector:componentDidUpdate')
+ }
+
+ navigate (newPath) {
+ debug('FileSelector:navigate', newPath)
+ this.setState({ path: newPath })
+ }
+
+ navigateUp () {
+ const newPath = path.join( this.state.path, '..' )
+ debug('FileSelector:navigateUp', this.state.path, '->', newPath)
+ this.setState({ path: newPath })
+ }
+
+ selectImage (image) {
+ debug('FileSelector:selectImage', image)
+
+ if (!supportedFormats.isSupportedImage(image.path)) {
+ const invalidImageError = errors.createUserError({
+ title: 'Invalid image',
+ description: messages.error.invalidImage(image)
+ })
+
+ osDialog.showError(invalidImageError)
+ analytics.logEvent('Invalid image', image)
+ return
}
- // Filters schema
- this.schema = {
- type: 'object',
- properties: {
- basename: {
- type: 'string'
- },
- isHidden: {
- type: 'boolean'
- },
- isDirectory: {
- type: 'boolean'
- }
+ return Bluebird.try(() => {
+ let message = null
+
+ if (supportedFormats.looksLikeWindowsImage(image.path)) {
+ analytics.logEvent('Possibly Windows image', image)
+ message = messages.warning.looksLikeWindowsImage()
+ } else if (!image.hasMBR) {
+ analytics.logEvent('Missing partition table', image)
+ message = messages.warning.missingPartitionTable()
}
+
+ if (message) {
+ // TODO: `Continue` should be on a red background (dangerous action) instead of `Change`.
+ // We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel`
+ return osDialog.showWarning({
+ confirmationLabel: 'Change',
+ rejectionLabel: 'Continue',
+ title: 'Warning',
+ description: message
+ })
+ }
+
+ return false
+ }).then((shouldChange) => {
+ if (shouldChange) {
+ return
+ }
+
+ selectionState.selectImage(image)
+
+ this.close()
+
+ // An easy way so we can quickly identify if we're making use of
+ // certain features without printing pages of text to DevTools.
+ image.logo = Boolean(image.logo)
+ image.bmap = Boolean(image.bmap)
+
+ analytics.logEvent('Select image', image)
+ }).catch(exceptionReporter.report)
+ }
+
+ selectFile (file) {
+ debug('FileSelector:selectFile', file)
+
+ if (file.isDirectory) {
+ this.navigate(file.path)
+ return
}
- this.closeModal = this.closeModal.bind(this)
- this.resolveModal = this.resolveModal.bind(this)
- this.browsePath = this.browsePath.bind(this)
- this.selectFile = this.selectFile.bind(this)
- this.highlightFile = this.highlightFile.bind(this)
- this.previousDirectory = this.previousDirectory.bind(this)
+ debug('FileSelector:getImageMetadata', file)
+
+ imageStream.getImageMetadata(file.path)
+ .then((imageMetadata) => {
+ debug('FileSelector:getImageMetadata', imageMetadata)
+ return this.selectImage(imageMetadata)
+ })
+ .catch((error) => {
+ debug('FileSelector:getImageMetadata', error)
+ const imageError = errors.createUserError({
+ title: 'Error opening image',
+ description: messages.error.openImage(path.basename(file.path), error.message)
+ })
+
+ osDialog.showError(imageError)
+ analytics.logException(error)
+ })
+ }
+
+ onHighlight (file) {
+ this.highlighted = file
}
render () {
- const items = this.state.files
-
const styles = {
display: 'flex',
height: 'calc(100vh - 20px)',
}
-
- const errorModal = (
- this.setState({ error: null }) }
- action="Dismiss"
- primaryButtonProps={ { danger: true, primary: false } }
- >
- { _.get(this.state.error, [ 'message' ]) || this.state.error }
-
- )
-
return (
-
-
+
+ {/**/}
-
+ onClick={ ::this.navigateUp }>
+
Back
-
+
-
- {
- items.map((item) => {
- return (
-
- )
- })
- }
-
+
- { this.state.error ? errorModal : null }
)
}
-
- componentDidMount () {
- this.setFilesProgressively(this.state.path)
- }
-
- closeModal () {
- this.props.close()
- }
-
- resolveModal () {
- this.selectFile(this.state.highlighted)
- }
-
- setFilesProgressively (dirname) {
- return fs.readdirAsync(dirname).then((basenames) => {
- return files.getAllFilesMetadataAsync(dirname, basenames)
- }).then((fileObjs) => {
- // Sort folders first and ignore case
- fileObjs.sort((fileA, fileB) => {
- // NOTE(Shou): the multiplication is an arbitrarily large enough number
- // to ensure folders have precedence over filenames
- const directoryPrecedence = (-Number(fileA.isDirectory) + Number(fileB.isDirectory)) * 3
- return directoryPrecedence + fileA.basename.localeCompare(fileB.basename, undefined, { sensitivity: 'base' })
- })
- this.setState({ files: fileObjs })
- })
- }
-
- browsePath (file) {
- analytics.logEvent('File browse', _.omit(file, [
- 'fullpath',
- 'basename',
- 'dirname'
- ]))
-
- const folderConstraints = this.props.constraints.length
- ? this.props.constraints
- : [ path.parse(this.props.path).root ]
-
- // Guard against browsing outside the constrained folder
- const isWithinAnyConstraint = folderConstraints.some((folderConstraint) => {
- return !path.relative(folderConstraint, file.fullpath).startsWith('..')
- })
- if (!isWithinAnyConstraint) {
- const error = `Cannot browse outside constrained folders ${folderConstraints}`
- analytics.logException(new Error(error))
- this.setState({ error })
- return
- }
-
- this.setFilesProgressively(file.fullpath).then(() => {
- this.setState({ path: file.fullpath })
- }).catch((error) => {
- this.setState({ error: error.message })
- })
- }
-
- selectFile (file, event) {
- if (file === null) {
- analytics.logEvent('File dismiss', null)
- } else if (file.isDirectory) {
- const prevFile = files.getFileMetadataSync(this.state.path)
- this.setState({
- history: this.state.history.concat(prevFile)
- })
- this.browsePath(file)
- } else {
- analytics.logEvent('File select', file.basename)
-
- imageStream.getImageMetadata(file.fullpath)
- .then((image) => {
- if (!supportedFormats.isSupportedImage(image.path)) {
- const invalidImageError = errors.createUserError({
- title: 'Invalid image',
- description: messages.error.invalidImage(image)
- })
-
- this.setState({ error: invalidImageError })
- analytics.logEvent('Invalid image', image)
- return
- }
-
- return Bluebird.try(() => {
- let message = null
-
- if (supportedFormats.looksLikeWindowsImage(image.path)) {
- analytics.logEvent('Possibly Windows image', image)
- message = messages.warning.looksLikeWindowsImage()
- } else if (!image.hasMBR) {
- analytics.logEvent('Missing partition table', image)
- message = messages.warning.missingPartitionTable()
- }
-
- if (message) {
- // TODO(Shou): `Continue` should be on a red background (dangerous action) instead of `Change`.
- // We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel`
- this.setState({ error: message })
- return
- }
-
- return image
- }).then((image) => {
- if (image) {
- selectionState.selectImage(image)
- }
- })
- })
- .catch((error) => {
- const imageError = errors.createUserError({
- title: 'Error opening image',
- description: messages.error.openImage(file.basename, error.message)
- })
-
- this.setState({ error: imageError })
- analytics.logException(error)
- })
-
- // Add folder to recently used
- recentStorage.modify('recents', (recents) => {
- const newRecents = _.uniqBy([ file ].concat(recents), (recentFile) => {
- return recentFile.dirname
- })
-
- // NOTE(Shou): we want to limit how many recent directories are stored - since we only
- // display four and don't rely on past state, we can only store four
- return newRecents.slice(0, 4)
- }, [])
-
- // Add file to potential favorites list
- recentStorage.modify('favorites', (favorites) => {
- const favorite = _.find(favorites, (favoriteFile) => {
- return favoriteFile.fullpath === file.fullpath
- }) || _.assign({}, file, { frequency: 1 })
-
- const newFavorites = _.uniqBy([ favorite ].concat(favorites), (favoriteFile) => {
- return favoriteFile.fullpath
- })
-
- // NOTE(Shou): we want to limit how many favorite files are stored - since we
- // *do* rely on past state, we need to store a reasonable amount of favorites so
- // they can be sorted by frequency. We only display four.
- return newFavorites.slice(0, 256)
- }, [])
-
- this.closeModal()
- }
- }
-
- highlightFile (file) {
- this.setState({ highlighted: file })
- }
-
- previousDirectory () {
- analytics.logEvent('Prev directory', null)
- const dir = this.state.history.shift()
- this.setState({ history: this.state.history })
-
- if (dir) {
- this.browsePath(dir)
- }
- }
}
FileSelector.propTypes = {
path: propTypes.string,
-
close: propTypes.func,
-
constraints: propTypes.arrayOf(propTypes.string)
}
diff --git a/lib/gui/app/components/file-selector/file-selector/path-breadcrumbs.jsx b/lib/gui/app/components/file-selector/file-selector/path-breadcrumbs.jsx
new file mode 100644
index 00000000..4c2c3c83
--- /dev/null
+++ b/lib/gui/app/components/file-selector/file-selector/path-breadcrumbs.jsx
@@ -0,0 +1,119 @@
+/*
+ * 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 path = require('path')
+
+const React = require('react')
+const propTypes = require('prop-types')
+const styled = require('styled-components').default
+const rendition = require('rendition')
+
+const middleEllipsis = require('../../../utils/middle-ellipsis')
+
+/**
+ * @summary How many directories to show with the breadcrumbs
+ * @type {Number}
+ * @constant
+ * @private
+ */
+const MAX_DIR_CRUMBS = 3
+
+/**
+ * @summary Character limit of a filename before a middle-ellipsis is added
+ * @constant
+ * @private
+ */
+const FILENAME_CHAR_LIMIT_SHORT = 15
+
+function splitComponents(dirname, root) {
+ const components = []
+ let basename = null
+ root = root || path.parse(dirname).root
+ while( dirname !== root ) {
+ basename = path.basename(dirname)
+ components.unshift({
+ path: dirname,
+ basename: basename,
+ name: basename
+ })
+ dirname = path.join( dirname, '..' )
+ }
+ if (components.length < MAX_DIR_CRUMBS) {
+ components.unshift({
+ path: root,
+ basename: root,
+ name: 'Root'
+ })
+ }
+ return components
+}
+
+class Crumb extends React.PureComponent {
+ constructor (props) {
+ super(props)
+ }
+
+ render () {
+ return (
+
+
+ { middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
+
+
+ )
+ }
+
+ navigate () {
+ this.props.navigate(this.props.dir.path)
+ }
+}
+
+class UnstyledBreadcrumbs extends React.PureComponent {
+ render () {
+ const components = splitComponents(this.props.path).slice(-MAX_DIR_CRUMBS)
+ return (
+
+ {
+ components.map((dir, index) => {
+ return (
+
+ )
+ })
+ }
+
+ )
+ }
+}
+
+const Breadcrumbs = styled(UnstyledBreadcrumbs)`
+ font-size: 18px;
+
+ & > button:not(:last-child)::after {
+ content: '/';
+ margin: 9px;
+ }
+`
+
+module.exports = Breadcrumbs
diff --git a/lib/gui/app/components/file-selector/file-selector/recent-files.jsx b/lib/gui/app/components/file-selector/file-selector/recent-files.jsx
new file mode 100644
index 00000000..0ac6e0ac
--- /dev/null
+++ b/lib/gui/app/components/file-selector/file-selector/recent-files.jsx
@@ -0,0 +1,125 @@
+/*
+ * 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 React = require('react')
+const propTypes = require('prop-types')
+const styled = require('styled-components').default
+const rendition = require('rendition')
+const colors = require('./colors')
+
+const middleEllipsis = require('../../../utils/middle-ellipsis')
+
+/**
+ * @summary Flex styled component
+ * @function
+ * @type {ReactElement}
+ */
+const Flex = styled.div`
+ display: flex;
+ flex: ${ props => props.flex };
+ flex-direction: ${ props => props.direction };
+ justify-content: ${ props => props.justifyContent };
+ align-items: ${ props => props.alignItems };
+ flex-wrap: ${ props => props.wrap };
+ flex-grow: ${ props => props.grow };
+`
+
+class RecentFileLink extends React.PureComponent {
+ constructor (props) {
+ super(props)
+ }
+
+ render () {
+ const file = this.props.file
+ return (
+
+ { middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
+
+ )
+ }
+
+ select () {
+ this.props.onSelect(this.props.file)
+ }
+}
+
+class UnstyledRecentFiles extends React.PureComponent {
+ constructor(props) {
+ super(props)
+ this.state = {
+ recent: [],
+ favorites: []
+ }
+ }
+
+ render () {
+ return (
+
+ Recent
+ {
+ this.state.recent.map((file) => {
+
+ })
+ }
+ Favorite
+ {
+ this.state.favorites.map((file) => {
+
+ })
+ }
+
+ )
+ }
+}
+
+const RecentFiles = styled(UnstyledRecentFiles)`
+ display: flex;
+ flex: 0 0 auto;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 130px;
+ background-color: ${ colors.secondary.background };
+ padding: 20px;
+ color: ${ colors.secondary.color };
+
+ > h5 {
+ color: ${ colors.secondary.title };
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+ margin-bottom: 15px;
+ }
+
+ > h5:last-of-type {
+ margin-top: 20px;
+ }
+
+ > button {
+ margin-bottom: 10px;
+ text-align: start;
+ font-size: 16px;
+ }
+`
+
+module.exports = RecentFiles
diff --git a/lib/gui/app/components/file-selector/services/file-selector.js b/lib/gui/app/components/file-selector/services/file-selector.js
index 8bcc5dda..0f27fbe6 100644
--- a/lib/gui/app/components/file-selector/services/file-selector.js
+++ b/lib/gui/app/components/file-selector/services/file-selector.js
@@ -48,5 +48,6 @@ module.exports = function (ModalService, $q) {
if (modal) {
modal.close()
}
+ modal = null
}
}
diff --git a/lib/gui/app/models/files.js b/lib/gui/app/models/files.js
index 92ef5df3..53a8d44d 100644
--- a/lib/gui/app/models/files.js
+++ b/lib/gui/app/models/files.js
@@ -16,133 +16,91 @@
'use strict'
-const _ = require('lodash')
+const fs = require('fs')
const path = require('path')
-const Bluebird = require('bluebird')
-const fs = Bluebird.promisifyAll(require('fs'))
/* eslint-disable lodash/prefer-lodash-method */
+/* eslint-disable no-undefined */
+
+const CONCURRENCY = 10
+
+const collator = new Intl.Collator(undefined, {
+ sensitivity: 'case'
+})
/**
- * @summary Async exists function
- * @function
- * @public
- *
- * @description
- * This is a promise for convenience, as it never fails with an exception/catch.
- *
- * @param {String} fullpath - full path
- * @returns {Boolean}
+ * @summary Sort files by their names / stats
+ * @param {FileEntry} fileA - first file
+ * @param {FileEntry} fileB - second file
+ * @returns {Number}
*
* @example
- * files.existsAsync('/home/user/file').then(console.log)
- * > true
+ * files.readdirAsync(dirname).then((files) => {
+ * return files.sort(sortFiles)
+ * })
*/
-exports.existsAsync = (fullpath) => {
- return new Bluebird((resolve, reject) => {
- return fs.accessAsync(fullpath, fs.constants.F_OK).then(() => {
- resolve(true)
- }).catch(() => {
- resolve(false)
- })
- })
+const sortFiles = (fileA, fileB) => {
+ return (fileB.isDirectory - fileA.isDirectory) ||
+ collator.compare(fileA.basename, fileB.basename)
}
/**
- * @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)
- * }
+ * @summary FileEntry struct
+ * @class
+ * @type {FileEntry}
*/
-exports.getFileMetadataSync = (dirname, basename = '') => {
- // TODO(Shou): use path.parse object information here
- const fullpath = path.join(dirname, basename)
- const pathObj = path.parse(fullpath)
+class FileEntry {
+ /**
+ * @summary FileEntry
+ * @param {String} filename - filename
+ * @param {fs.Stats} stats - stats
+ *
+ * @example
+ * new FileEntry(filename, stats)
+ */
+ constructor (filename, stats) {
+ const components = path.parse(filename)
- // 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,
- name: pathObj.name,
- isDirectory: stats.isDirectory(),
- isHidden,
- size: stats.size
+ this.path = filename
+ this.dirname = components.dir
+ this.basename = components.base
+ this.name = components.name
+ this.ext = components.ext
+ this.isHidden = components.name.startsWith('.')
+ this.isFile = stats.isFile()
+ this.isDirectory = stats.isDirectory()
+ this.size = stats.size
+ this.stats = stats
}
}
/**
- * @summary Get file metadata asynchronously
- * @function
- * @private
- *
- * @param {String} fullpath - full path
- * @returns {Promise