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"
- 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

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 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'

View File

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

View File

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

View File

@ -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'
]

View File

@ -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
*/

View File

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

View File

@ -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') {

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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