From ddc99aa00e54ec593809c819de25e1f6b37d1334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4usler?= Date: Thu, 13 Jun 2019 21:23:09 +0200 Subject: [PATCH] refactor: main filesystem code (#1104) --- .github/CONTRIBUTING.md | 20 ++- src/common/filesystem/index.js | 71 ++++++++++ src/common/filesystem/paths.js | 116 +++++++++++++++++ src/main/app/index.js | 2 +- src/main/app/paths.js | 2 +- src/main/cli/index.js | 2 +- src/main/config.js | 28 ---- src/main/dataCenter/index.js | 6 +- src/main/filesystem/index.js | 152 +--------------------- src/main/filesystem/markdown.js | 4 +- src/main/filesystem/watcher.js | 5 +- src/main/keyboard/shortcutHandler.js | 2 +- src/main/menu/actions/file.js | 8 +- src/main/menu/index.js | 2 +- src/main/menu/templates/help.js | 2 +- src/main/utils/imagePathAutoComplement.js | 5 +- src/main/utils/index.js | 11 -- src/main/windows/editor.js | 2 +- src/renderer/store/editor.js | 2 +- src/renderer/util/fileSystem.js | 29 +---- 20 files changed, 227 insertions(+), 244 deletions(-) create mode 100644 src/common/filesystem/index.js create mode 100644 src/common/filesystem/paths.js diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a59c2ea0..5c67d39c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -114,16 +114,22 @@ For more scripts please see `package.json`. - ES6 and "best practices" - 2 space indent +- no semicolons - JSDoc for documentation ## Project Structure -- root: Configuration files +- `.`: Configuration files - `package.json`: Project settings -- `build`: Contains generated binaries -- `dist`: Build files for deployment -- `docs`: Documentation and assets -- `node_modules`: Dependencies +- `build/`: Contains generated binaries +- `dist/`: Build files for deployment +- `docs/`: Documentation and assets +- `resources/`: Application assets using at build time +- `node_modules/`: Dependencies - `src`: Mark Text source code -- `static`: Application assets (images, themes, etc) -- `test`: Contains (unit) tests + - `common/`: Common source files that only require Node.js APIs. Code from this folder can be used in all other folders except `muya`. + - `main/`: Main process source files that require Electron main-process APIs. `main` files can use `common` source code. + - `muya/`: Mark Texts backend that only allow pure JavaScript, BOM and DOM APIs. Don't use Electron or Node.js APIs! + - `renderer`: Fontend that require Electron renderer-process APIs and may use `common` or `muya` source code. +- `static/`: Application assets (images, themes, etc) +- `test/`: Contains (unit) tests diff --git a/src/common/filesystem/index.js b/src/common/filesystem/index.js new file mode 100644 index 00000000..959ecc61 --- /dev/null +++ b/src/common/filesystem/index.js @@ -0,0 +1,71 @@ +import fs from 'fs-extra' + +/** + * Test whether or not the given path exists. + * + * @param {string} p The path to the file or directory. + * @returns {boolean} + */ +export const exists = async p => { + // Nodes fs.exists is deprecated. + try { + await fs.access(p) + return true + } catch(_) { + return false + } +} + +/** + * Ensure that a directory exist. + * + * @param {string} dirPath The directory path. + */ +export const ensureDirSync = dirPath => { + try { + fs.ensureDirSync(dirPath) + } catch (e) { + if (e.code !== 'EEXIST') { + throw e + } + } +} + +/** + * Returns true if the path is a directory with read access. + * + * @param {string} dirPath The directory path. + */ +export const isDirectory = dirPath => { + try { + return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory() + } catch (_) { + return false + } +} + +/** + * Returns true if the path is a file with read access. + * + * @param {string} filepath The file path. + */ +export const isFile = filepath => { + try { + return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile() + } catch (_) { + return false + } +} + +/** + * Returns true if the path is a symbolic link with read access. + * + * @param {string} filepath The link path. + */ +export const isSymbolicLink = filepath => { + try { + return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink() + } catch (_) { + return false + } +} diff --git a/src/common/filesystem/paths.js b/src/common/filesystem/paths.js new file mode 100644 index 00000000..8991b13c --- /dev/null +++ b/src/common/filesystem/paths.js @@ -0,0 +1,116 @@ +import fs from 'fs-extra' +import path from 'path' +import { isFile, isSymbolicLink } from './index' + +export const MARKDOWN_EXTENSIONS = [ + 'markdown', + 'mdown', + 'mkdn', + 'md', + 'mkd', + 'mdwn', + 'mdtxt', + 'mdtext', + 'text', + 'txt' +] + +export const IMAGE_EXTENSIONS = [ + 'jpeg', + 'jpg', + 'png', + 'gif', + 'svg', + 'webp' +] + +/** + * Returns true if the filename matches one of the markdown extensions. + * + * @param {string} filename Path or filename + */ +export const hasMarkdownExtension = filename => { + if (!filename || typeof filename !== 'string') return false + return MARKDOWN_EXTENSIONS.some(ext => filename.endsWith(`.${ext}`)) +} + +/** + * Returns ture if the path is an image file. + * + * @param {string} filepath The path + */ +export const isImageFile = filepath => { + const extname = path.extname(filepath) + return isFile(filepath) && IMAGE_EXTENSIONS.some(ext => { + const EXT_REG = new RegExp(ext, 'i') + return EXT_REG.test(extname) + }) +} + +/** + * Returns true if the path is a markdown file. + * + * @param {string} filepath The path or link path. + */ +export const isMarkdownFile = filepath => { + return isFile(filepath) && hasMarkdownExtension(filepath) +} + + +/** + * Returns true if the path is a markdown file or symbolic link to a markdown file. + * + * @param {string} filepath The path or link path. + */ +export const isMarkdownFileOrLink = filepath => { + if (!isFile(filepath)) return false + if (hasMarkdownExtension(filepath)) { + return true + } + + // Symbolic link to a markdown file + if (isSymbolicLink(filepath)) { + const targetPath = fs.readlinkSync(filepath) + return isFile(targetPath) && hasMarkdownExtension(targetPath) + } + return false +} + +/** + * Check if the both paths point to the same file. + * + * @param {string} pathA The first path. + * @param {string} pathB The second path. + * @param {boolean} [isNormalized] Are both paths already normalized. + */ +export const isSamePathSync = (pathA, pathB, isNormalized = false) => { + if (!pathA || !pathB) return false + const a = isNormalized ? pathA : path.normalize(pathA) + const b = isNormalized ? pathB : path.normalize(pathB) + if (a.length !== b.length) { + return false + } else if (a === b) { + return true + } else if (a.toLowerCase() === b.toLowerCase()) { + try { + const fiA = fs.statSync(a) + const fiB = fs.statSync(b) + return fiA.ino === fiB.ino + } catch (_) { + // Ignore error + } + } + return false +} + +/** + * Check whether a file or directory is a child of the given directory. + * + * @param {string} dir The parent directory. + * @param {string} child The file or directory path to check. + */ +export const isChildOfDirectory = (dir, child) => { + if (!dir || !child) return false + const relative = path.relative(dir, child) + return relative && !relative.startsWith('..') && !path.isAbsolute(relative) +} diff --git a/src/main/app/index.js b/src/main/app/index.js index 3ab1a66a..66b9cd96 100644 --- a/src/main/app/index.js +++ b/src/main/app/index.js @@ -4,9 +4,9 @@ import { exec } from 'child_process' import dayjs from 'dayjs' import log from 'electron-log' import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron' +import { isChildOfDirectory } from 'common/filesystem/paths' import { isLinux, isOsx } from '../config' import parseArgs from '../cli/parser' -import { isChildOfDirectory } from '../filesystem' import { normalizeMarkdownPath } from '../filesystem/markdown' import { getMenuItemById } from '../menu' import { selectTheme } from '../menu/actions/theme' diff --git a/src/main/app/paths.js b/src/main/app/paths.js index e71b422d..40541a86 100644 --- a/src/main/app/paths.js +++ b/src/main/app/paths.js @@ -1,6 +1,6 @@ import { app } from 'electron' import EnvPaths from 'common/envPaths' -import { ensureDirSync } from '../filesystem' +import { ensureDirSync } from 'common/filesystem' class AppPaths extends EnvPaths { diff --git a/src/main/cli/index.js b/src/main/cli/index.js index 4e8cdc34..51c8cc15 100644 --- a/src/main/cli/index.js +++ b/src/main/cli/index.js @@ -1,6 +1,6 @@ import path from 'path' import os from 'os' -import { isDirectory } from '../filesystem' +import { isDirectory } from 'common/filesystem' import parseArgs from './parser' import { dumpKeyboardInformation } from '../keyboard' import { getPath } from '../utils' diff --git a/src/main/config.js b/src/main/config.js index 7f15ba20..e310a51f 100644 --- a/src/main/config.js +++ b/src/main/config.js @@ -34,28 +34,6 @@ export const defaultPreferenceWinOptions = { titleBarStyle: 'hiddenInset' } -export const EXTENSIONS = [ - 'markdown', - 'mdown', - 'mkdn', - 'md', - 'mkd', - 'mdwn', - 'mdtxt', - 'mdtext', - 'text', - 'txt' -] - -export const IMAGE_EXTENSIONS = [ - 'jpeg', - 'jpg', - 'png', - 'gif', - 'svg', - 'webp' -] - export const PANDOC_EXTENSIONS = [ 'html', 'docx', @@ -72,12 +50,6 @@ export const PANDOC_EXTENSIONS = [ 'epub' ] -// export const PROJECT_BLACK_LIST = [ -// 'node_modules', -// '.git', -// '.DS_Store' -// ] - export const BLACK_LIST = [ '$RECYCLE.BIN' ] diff --git a/src/main/dataCenter/index.js b/src/main/dataCenter/index.js index 8bbdff5a..e1dbc501 100644 --- a/src/main/dataCenter/index.js +++ b/src/main/dataCenter/index.js @@ -6,8 +6,8 @@ import keytar from 'keytar' import schema from './schema' import Store from 'electron-store' import log from 'electron-log' -import { ensureDirSync } from '../filesystem' -import { IMAGE_EXTENSIONS } from '../config' +import { ensureDirSync } from 'common/filesystem' +import { IMAGE_EXTENSIONS } from 'common/filesystem/paths' const DATA_CENTER_NAME = 'dataCenter' @@ -104,7 +104,7 @@ class DataCenter extends EventEmitter { } /** - * + * * @param {string} key * return a promise */ diff --git a/src/main/filesystem/index.js b/src/main/filesystem/index.js index ce391788..690c0160 100644 --- a/src/main/filesystem/index.js +++ b/src/main/filesystem/index.js @@ -1,156 +1,6 @@ import fs from 'fs-extra' import path from 'path' -import { hasMarkdownExtension } from '../utils' -import { IMAGE_EXTENSIONS } from '../config' - -/** - * Test whether or not the given path exists. - * - * @param {string} p The path to the file or directory. - * @returns {boolean} - */ -export const exists = async p => { - // fs.exists is deprecated. - try { - await fs.access(p) - return true - } catch(_) { - return false - } -} - -/** - * Ensure that a directory exist. - * - * @param {string} dirPath The directory path. - */ -export const ensureDirSync = dirPath => { - try { - fs.ensureDirSync(dirPath) - } catch (e) { - if (e.code !== 'EEXIST') { - throw e - } - } -} - -/** - * Returns true if the path is a directory with read access. - * - * @param {string} dirPath The directory path. - */ -export const isDirectory = dirPath => { - try { - return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory() - } catch (_) { - return false - } -} - -/** - * Returns true if the path is a file with read access. - * - * @param {string} filepath The file path. - */ -export const isFile = filepath => { - try { - return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile() - } catch (_) { - return false - } -} - -/** - * Returns true if the path is a symbolic link with read access. - * - * @param {string} filepath The link path. - */ -export const isSymbolicLink = filepath => { - try { - return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink() - } catch (_) { - return false - } -} - -/** - * Returns true if the path is a markdown file. - * - * @param {string} filepath The path or link path. - */ -export const isMarkdownFile = filepath => { - return isFile(filepath) && hasMarkdownExtension(filepath) -} -/** - * Returns ture if the path is an image file. - * - * @param {string} filepath The path - */ -export const isImageFile = filepath => { - const extname = path.extname(filepath) - return isFile(filepath) && IMAGE_EXTENSIONS.some(ext => { - const EXT_REG = new RegExp(ext, 'i') - return EXT_REG.test(extname) - }) -} - -/** - * Returns true if the path is a markdown file or symbolic link to a markdown file. - * - * @param {string} filepath The path or link path. - */ -export const isMarkdownFileOrLink = filepath => { - if (!isFile(filepath)) return false - if (hasMarkdownExtension(filepath)) { - return true - } - - // Symbolic link to a markdown file - if (isSymbolicLink(filepath)) { - const targetPath = fs.readlinkSync(filepath) - return isFile(targetPath) && hasMarkdownExtension(targetPath) - } - return false -} - -/** - * Check if the both paths point to the same file. - * - * @param {string} pathA The first path. - * @param {string} pathB The second path. - * @param {boolean} [isNormalized] Are both paths already normalized. - */ -export const isSamePathSync = (pathA, pathB, isNormalized = false) => { - if (!pathA || !pathB) return false - const a = isNormalized ? pathA : path.normalize(pathA) - const b = isNormalized ? pathB : path.normalize(pathB) - if (a.length !== b.length) { - return false - } else if (a === b) { - return true - } else if (a.toLowerCase() === b.toLowerCase()) { - try { - const fiA = fs.statSync(a) - const fiB = fs.statSync(b) - return fiA.ino === fiB.ino - } catch (_) { - // Ignore error - } - } - return false -} - -/** - * Check whether a file or directory is a child of the given directory. - * - * @param {string} dir The parent directory. - * @param {string} child The file or directory path to check. - */ -export const isChildOfDirectory = (dir, child) => { - if (!dir || !child) return false - const relative = path.relative(dir, child) - return relative && !relative.startsWith('..') && !path.isAbsolute(relative) -} +import { isDirectory, isFile, isSymbolicLink } from 'common/filesystem' /** * Normalize the path into an absolute path and resolves the link target if needed. diff --git a/src/main/filesystem/markdown.js b/src/main/filesystem/markdown.js index 9ce2f439..0cdc3a0f 100644 --- a/src/main/filesystem/markdown.js +++ b/src/main/filesystem/markdown.js @@ -2,7 +2,9 @@ import fs from 'fs-extra' import path from 'path' import log from 'electron-log' import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from '../config' -import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath , writeFile } from '../filesystem' +import { isDirectory } from 'common/filesystem' +import { isMarkdownFileOrLink } from 'common/filesystem/paths' +import { normalizeAndResolvePath, writeFile } from '../filesystem' const getLineEnding = lineEnding => { if (lineEnding === 'lf') { diff --git a/src/main/filesystem/watcher.js b/src/main/filesystem/watcher.js index d28ed751..ced577c5 100644 --- a/src/main/filesystem/watcher.js +++ b/src/main/filesystem/watcher.js @@ -2,8 +2,9 @@ import path from 'path' import fs from 'fs-extra' import log from 'electron-log' import chokidar from 'chokidar' -import { getUniqueId, hasMarkdownExtension } from '../utils' -import { exists } from '../filesystem' +import { exists } from 'common/filesystem' +import { hasMarkdownExtension } from 'common/filesystem/paths' +import { getUniqueId } from '../utils' import { loadMarkdownFile } from '../filesystem/markdown' import { isLinux } from '../config' diff --git a/src/main/keyboard/shortcutHandler.js b/src/main/keyboard/shortcutHandler.js index 820f4522..964823e4 100644 --- a/src/main/keyboard/shortcutHandler.js +++ b/src/main/keyboard/shortcutHandler.js @@ -4,9 +4,9 @@ import path from 'path' import log from 'electron-log' import isAccelerator from 'electron-is-accelerator' import electronLocalshortcut from '@hfelix/electron-localshortcut' +import { isFile } from 'common/filesystem' import { isOsx } from '../config' import { getKeyboardLanguage, getVirtualLetters } from '../keyboard' -import { isFile } from '../filesystem' // Problematic key bindings: // Aidou: Ctrl+/ -> dead key diff --git a/src/main/menu/actions/file.js b/src/main/menu/actions/file.js index f462ea89..fb2c6284 100644 --- a/src/main/menu/actions/file.js +++ b/src/main/menu/actions/file.js @@ -3,8 +3,10 @@ import path from 'path' import { promisify } from 'util' import { BrowserWindow, dialog, ipcMain, shell } from 'electron' import log from 'electron-log' -import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../../config' -import { isDirectory, isFile, isMarkdownFile, isMarkdownFileOrLink, normalizeAndResolvePath, writeFile } from '../../filesystem' +import { isDirectory, isFile } from 'common/filesystem' +import { MARKDOWN_EXTENSIONS, isMarkdownFile, isMarkdownFileOrLink } from 'common/filesystem/paths' +import { EXTENSION_HASN, PANDOC_EXTENSIONS, URL_REG } from '../../config' +import { normalizeAndResolvePath, writeFile } from '../../filesystem' import { writeMarkdownFile } from '../../filesystem/markdown' import { getPath, getRecommendTitleFromMarkdownString } from '../../utils' import pandoc from '../../utils/pandoc' @@ -417,7 +419,7 @@ export const openFile = win => { properties: ['openFile', 'multiSelections'], filters: [{ name: 'text', - extensions: EXTENSIONS + extensions: MARKDOWN_EXTENSIONS }] }, paths => { if (paths && Array.isArray(paths)) { diff --git a/src/main/menu/index.js b/src/main/menu/index.js index fb54cc30..93ce59b8 100644 --- a/src/main/menu/index.js +++ b/src/main/menu/index.js @@ -2,8 +2,8 @@ import fs from 'fs' import path from 'path' import { app, ipcMain, Menu } from 'electron' import log from 'electron-log' +import { ensureDirSync, isDirectory, isFile } from 'common/filesystem' import { isLinux } from '../config' -import { ensureDirSync, isDirectory, isFile } from '../filesystem' import { parseMenu } from '../keyboard/shortcutHandler' import configureMenu, { configSettingMenu } from '../menu/templates' diff --git a/src/main/menu/templates/help.js b/src/main/menu/templates/help.js index 3f6d0e0e..dcaf173d 100755 --- a/src/main/menu/templates/help.js +++ b/src/main/menu/templates/help.js @@ -1,8 +1,8 @@ import path from 'path' import { shell } from 'electron' +import { isFile } from 'common/filesystem' import * as actions from '../actions/help' import { checkUpdates } from '../actions/marktext' -import { isFile } from '../../filesystem' export default function () { const helpMenu = { diff --git a/src/main/utils/imagePathAutoComplement.js b/src/main/utils/imagePathAutoComplement.js index 2a316817..e9a3d69b 100644 --- a/src/main/utils/imagePathAutoComplement.js +++ b/src/main/utils/imagePathAutoComplement.js @@ -2,8 +2,9 @@ import fs from 'fs' import path from 'path' import { filter } from 'fuzzaldrin' import log from 'electron-log' -import { IMAGE_EXTENSIONS, BLACK_LIST } from '../config' -import { isDirectory, isFile } from '../filesystem' +import { isDirectory, isFile } from 'common/filesystem' +import { IMAGE_EXTENSIONS } from 'common/filesystem/paths' +import { BLACK_LIST } from '../config' // TODO(need::refactor): Refactor this file. Just return an array of directories and files without caching and watching? diff --git a/src/main/utils/index.js b/src/main/utils/index.js index 9579000b..cdb86775 100644 --- a/src/main/utils/index.js +++ b/src/main/utils/index.js @@ -1,5 +1,4 @@ import { app } from 'electron' -import { EXTENSIONS } from '../config' const ID_PREFIX = 'mt-' let id = 0 @@ -37,16 +36,6 @@ export const getPath = name => { return app.getPath(name) } -/** - * Returns true if the filename matches one of the markdown extensions. - * - * @param {string} filename Path or filename - */ -export const hasMarkdownExtension = filename => { - if (!filename || typeof filename !== 'string') return false - return EXTENSIONS.some(ext => filename.endsWith(`.${ext}`)) -} - export const hasSameKeys = (a, b) => { const aKeys = Object.keys(a).sort() const bKeys = Object.keys(b).sort() diff --git a/src/main/windows/editor.js b/src/main/windows/editor.js index 65890eae..d5890b47 100644 --- a/src/main/windows/editor.js +++ b/src/main/windows/editor.js @@ -2,10 +2,10 @@ import path from 'path' import { BrowserWindow, dialog, ipcMain } from 'electron' import log from 'electron-log' import windowStateKeeper from 'electron-window-state' +import { isChildOfDirectory, isSamePathSync } from 'common/filesystem/paths' import BaseWindow, { WindowLifecycle, WindowType } from './base' import { ensureWindowPosition } from './utils' import { TITLE_BAR_HEIGHT, editorWinOptions, isLinux, isOsx } from '../config' -import { isChildOfDirectory, isSamePathSync } from '../filesystem' import { loadMarkdownFile } from '../filesystem/markdown' class EditorWindow extends BaseWindow { diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index 32d02390..d9836602 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -1,9 +1,9 @@ import { clipboard, ipcRenderer, shell } from 'electron' import path from 'path' import equal from 'deep-equal' +import { isSamePathSync } from 'common/filesystem/paths' import bus from '../bus' import { hasKeys, getUniqueId } from '../util' -import { isSamePathSync } from '../util/fileSystem' import listToTree from '../util/listToTree' import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help' import notice from '../services/notification' diff --git a/src/renderer/util/fileSystem.js b/src/renderer/util/fileSystem.js index 3756d25b..32814218 100644 --- a/src/renderer/util/fileSystem.js +++ b/src/renderer/util/fileSystem.js @@ -2,7 +2,7 @@ import path from 'path' import fse from 'fs-extra' import dayjs from 'dayjs' import Octokit from '@octokit/rest' -import { isImageFile } from '../../main/filesystem' +import { isImageFile } from 'common/filesystem/paths' import { dataURItoBlob, getContentHash } from './index' import axios from 'axios' @@ -22,33 +22,6 @@ export const rename = (src, dest) => { return fse.move(src, dest) } -/** - * Check if the both paths point to the same file. - * - * @param {string} pathA The first path. - * @param {string} pathB The second path. - * @param {boolean} [isNormalized] Are both paths already normalized. - */ -export const isSamePathSync = (pathA, pathB, isNormalized = false) => { - if (!pathA || !pathB) return false - const a = isNormalized ? pathA : path.normalize(pathA) - const b = isNormalized ? pathB : path.normalize(pathB) - if (a.length !== b.length) { - return false - } else if (a === b) { - return true - } else if (a.toLowerCase() === b.toLowerCase()) { - try { - const fiA = fse.statSync(a) - const fiB = fse.statSync(b) - return fiA.ino === fiB.ino - } catch (_) { - // Ignore error - } - } - return false -} - export const moveImageToFolder = async (pathname, image, dir) => { const isPath = typeof image === 'string' if (isPath) {