refactor: main filesystem code (#1104)

This commit is contained in:
Felix Häusler 2019-06-13 21:23:09 +02:00 committed by GitHub
parent 1a92d2de1b
commit ddc99aa00e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 227 additions and 244 deletions

View File

@ -114,16 +114,22 @@ For more scripts please see `package.json`.
- ES6 and "best practices" - ES6 and "best practices"
- 2 space indent - 2 space indent
- no semicolons
- JSDoc for documentation - JSDoc for documentation
## Project Structure ## Project Structure
- root: Configuration files - `.`: Configuration files
- `package.json`: Project settings - `package.json`: Project settings
- `build`: Contains generated binaries - `build/`: Contains generated binaries
- `dist`: Build files for deployment - `dist/`: Build files for deployment
- `docs`: Documentation and assets - `docs/`: Documentation and assets
- `node_modules`: Dependencies - `resources/`: Application assets using at build time
- `node_modules/`: Dependencies
- `src`: Mark Text source code - `src`: Mark Text source code
- `static`: Application assets (images, themes, etc) - `common/`: Common source files that only require Node.js APIs. Code from this folder can be used in all other folders except `muya`.
- `test`: Contains (unit) tests - `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

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -4,9 +4,9 @@ import { exec } from 'child_process'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import log from 'electron-log' import log from 'electron-log'
import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron' import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron'
import { isChildOfDirectory } from 'common/filesystem/paths'
import { isLinux, isOsx } from '../config' import { isLinux, isOsx } from '../config'
import parseArgs from '../cli/parser' import parseArgs from '../cli/parser'
import { isChildOfDirectory } from '../filesystem'
import { normalizeMarkdownPath } from '../filesystem/markdown' import { normalizeMarkdownPath } from '../filesystem/markdown'
import { getMenuItemById } from '../menu' import { getMenuItemById } from '../menu'
import { selectTheme } from '../menu/actions/theme' import { selectTheme } from '../menu/actions/theme'

View File

@ -1,6 +1,6 @@
import { app } from 'electron' import { app } from 'electron'
import EnvPaths from 'common/envPaths' import EnvPaths from 'common/envPaths'
import { ensureDirSync } from '../filesystem' import { ensureDirSync } from 'common/filesystem'
class AppPaths extends EnvPaths { class AppPaths extends EnvPaths {

View File

@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import os from 'os' import os from 'os'
import { isDirectory } from '../filesystem' import { isDirectory } from 'common/filesystem'
import parseArgs from './parser' import parseArgs from './parser'
import { dumpKeyboardInformation } from '../keyboard' import { dumpKeyboardInformation } from '../keyboard'
import { getPath } from '../utils' import { getPath } from '../utils'

View File

@ -34,28 +34,6 @@ export const defaultPreferenceWinOptions = {
titleBarStyle: 'hiddenInset' 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 = [ export const PANDOC_EXTENSIONS = [
'html', 'html',
'docx', 'docx',
@ -72,12 +50,6 @@ export const PANDOC_EXTENSIONS = [
'epub' 'epub'
] ]
// export const PROJECT_BLACK_LIST = [
// 'node_modules',
// '.git',
// '.DS_Store'
// ]
export const BLACK_LIST = [ export const BLACK_LIST = [
'$RECYCLE.BIN' '$RECYCLE.BIN'
] ]

View File

@ -6,8 +6,8 @@ import keytar from 'keytar'
import schema from './schema' import schema from './schema'
import Store from 'electron-store' import Store from 'electron-store'
import log from 'electron-log' import log from 'electron-log'
import { ensureDirSync } from '../filesystem' import { ensureDirSync } from 'common/filesystem'
import { IMAGE_EXTENSIONS } from '../config' import { IMAGE_EXTENSIONS } from 'common/filesystem/paths'
const DATA_CENTER_NAME = 'dataCenter' const DATA_CENTER_NAME = 'dataCenter'
@ -104,7 +104,7 @@ class DataCenter extends EventEmitter {
} }
/** /**
* *
* @param {string} key * @param {string} key
* return a promise * return a promise
*/ */

View File

@ -1,156 +1,6 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import { hasMarkdownExtension } from '../utils' import { isDirectory, isFile, isSymbolicLink } from 'common/filesystem'
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)
}
/** /**
* Normalize the path into an absolute path and resolves the link target if needed. * Normalize the path into an absolute path and resolves the link target if needed.

View File

@ -2,7 +2,9 @@ import fs from 'fs-extra'
import path from 'path' import path from 'path'
import log from 'electron-log' import log from 'electron-log'
import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from '../config' 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 => { const getLineEnding = lineEnding => {
if (lineEnding === 'lf') { if (lineEnding === 'lf') {

View File

@ -2,8 +2,9 @@ import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import log from 'electron-log' import log from 'electron-log'
import chokidar from 'chokidar' import chokidar from 'chokidar'
import { getUniqueId, hasMarkdownExtension } from '../utils' import { exists } from 'common/filesystem'
import { exists } from '../filesystem' import { hasMarkdownExtension } from 'common/filesystem/paths'
import { getUniqueId } from '../utils'
import { loadMarkdownFile } from '../filesystem/markdown' import { loadMarkdownFile } from '../filesystem/markdown'
import { isLinux } from '../config' import { isLinux } from '../config'

View File

@ -4,9 +4,9 @@ import path from 'path'
import log from 'electron-log' import log from 'electron-log'
import isAccelerator from 'electron-is-accelerator' import isAccelerator from 'electron-is-accelerator'
import electronLocalshortcut from '@hfelix/electron-localshortcut' import electronLocalshortcut from '@hfelix/electron-localshortcut'
import { isFile } from 'common/filesystem'
import { isOsx } from '../config' import { isOsx } from '../config'
import { getKeyboardLanguage, getVirtualLetters } from '../keyboard' import { getKeyboardLanguage, getVirtualLetters } from '../keyboard'
import { isFile } from '../filesystem'
// Problematic key bindings: // Problematic key bindings:
// Aidou: Ctrl+/ -> dead key // Aidou: Ctrl+/ -> dead key

View File

@ -3,8 +3,10 @@ import path from 'path'
import { promisify } from 'util' import { promisify } from 'util'
import { BrowserWindow, dialog, ipcMain, shell } from 'electron' import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../../config' import { isDirectory, isFile } from 'common/filesystem'
import { isDirectory, isFile, isMarkdownFile, isMarkdownFileOrLink, normalizeAndResolvePath, writeFile } from '../../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 { writeMarkdownFile } from '../../filesystem/markdown'
import { getPath, getRecommendTitleFromMarkdownString } from '../../utils' import { getPath, getRecommendTitleFromMarkdownString } from '../../utils'
import pandoc from '../../utils/pandoc' import pandoc from '../../utils/pandoc'
@ -417,7 +419,7 @@ export const openFile = win => {
properties: ['openFile', 'multiSelections'], properties: ['openFile', 'multiSelections'],
filters: [{ filters: [{
name: 'text', name: 'text',
extensions: EXTENSIONS extensions: MARKDOWN_EXTENSIONS
}] }]
}, paths => { }, paths => {
if (paths && Array.isArray(paths)) { if (paths && Array.isArray(paths)) {

View File

@ -2,8 +2,8 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import { app, ipcMain, Menu } from 'electron' import { app, ipcMain, Menu } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import { ensureDirSync, isDirectory, isFile } from 'common/filesystem'
import { isLinux } from '../config' import { isLinux } from '../config'
import { ensureDirSync, isDirectory, isFile } from '../filesystem'
import { parseMenu } from '../keyboard/shortcutHandler' import { parseMenu } from '../keyboard/shortcutHandler'
import configureMenu, { configSettingMenu } from '../menu/templates' import configureMenu, { configSettingMenu } from '../menu/templates'

View File

@ -1,8 +1,8 @@
import path from 'path' import path from 'path'
import { shell } from 'electron' import { shell } from 'electron'
import { isFile } from 'common/filesystem'
import * as actions from '../actions/help' import * as actions from '../actions/help'
import { checkUpdates } from '../actions/marktext' import { checkUpdates } from '../actions/marktext'
import { isFile } from '../../filesystem'
export default function () { export default function () {
const helpMenu = { const helpMenu = {

View File

@ -2,8 +2,9 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import { filter } from 'fuzzaldrin' import { filter } from 'fuzzaldrin'
import log from 'electron-log' import log from 'electron-log'
import { IMAGE_EXTENSIONS, BLACK_LIST } from '../config' import { isDirectory, isFile } from 'common/filesystem'
import { isDirectory, isFile } from '../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? // TODO(need::refactor): Refactor this file. Just return an array of directories and files without caching and watching?

View File

@ -1,5 +1,4 @@
import { app } from 'electron' import { app } from 'electron'
import { EXTENSIONS } from '../config'
const ID_PREFIX = 'mt-' const ID_PREFIX = 'mt-'
let id = 0 let id = 0
@ -37,16 +36,6 @@ export const getPath = name => {
return app.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) => { export const hasSameKeys = (a, b) => {
const aKeys = Object.keys(a).sort() const aKeys = Object.keys(a).sort()
const bKeys = Object.keys(b).sort() const bKeys = Object.keys(b).sort()

View File

@ -2,10 +2,10 @@ import path from 'path'
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { isChildOfDirectory, isSamePathSync } from 'common/filesystem/paths'
import BaseWindow, { WindowLifecycle, WindowType } from './base' import BaseWindow, { WindowLifecycle, WindowType } from './base'
import { ensureWindowPosition } from './utils' import { ensureWindowPosition } from './utils'
import { TITLE_BAR_HEIGHT, editorWinOptions, isLinux, isOsx } from '../config' import { TITLE_BAR_HEIGHT, editorWinOptions, isLinux, isOsx } from '../config'
import { isChildOfDirectory, isSamePathSync } from '../filesystem'
import { loadMarkdownFile } from '../filesystem/markdown' import { loadMarkdownFile } from '../filesystem/markdown'
class EditorWindow extends BaseWindow { class EditorWindow extends BaseWindow {

View File

@ -1,9 +1,9 @@
import { clipboard, ipcRenderer, shell } from 'electron' import { clipboard, ipcRenderer, shell } from 'electron'
import path from 'path' import path from 'path'
import equal from 'deep-equal' import equal from 'deep-equal'
import { isSamePathSync } from 'common/filesystem/paths'
import bus from '../bus' import bus from '../bus'
import { hasKeys, getUniqueId } from '../util' import { hasKeys, getUniqueId } from '../util'
import { isSamePathSync } from '../util/fileSystem'
import listToTree from '../util/listToTree' import listToTree from '../util/listToTree'
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help' import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
import notice from '../services/notification' import notice from '../services/notification'

View File

@ -2,7 +2,7 @@ import path from 'path'
import fse from 'fs-extra' import fse from 'fs-extra'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Octokit from '@octokit/rest' import Octokit from '@octokit/rest'
import { isImageFile } from '../../main/filesystem' import { isImageFile } from 'common/filesystem/paths'
import { dataURItoBlob, getContentHash } from './index' import { dataURItoBlob, getContentHash } from './index'
import axios from 'axios' import axios from 'axios'
@ -22,33 +22,6 @@ export const rename = (src, dest) => {
return fse.move(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) => { export const moveImageToFolder = async (pathname, image, dir) => {
const isPath = typeof image === 'string' const isPath = typeof image === 'string'
if (isPath) { if (isPath) {