Add symbolic link support (#802)

* Allow symbolic links

* Update changelog
This commit is contained in:
Felix Häusler 2019-03-25 16:11:57 +01:00 committed by Ran Luo
parent 94066706f5
commit f115b7ea41
5 changed files with 98 additions and 24 deletions

View File

@ -79,6 +79,7 @@ This update **fixes a XSS security vulnerability** when exporting a document.
- Fixed multiple parser issues (update marked.js to v0.6.1)
- Fixed [...] is displayed in gray and orange (#432)
- Fixed an issue that relative images are not loaded after closing a tab
- Add symbolic link support
### 0.13.65

View File

@ -7,7 +7,7 @@ import appWindow from '../window'
import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../config'
import { loadMarkdownFile, writeFile, writeMarkdownFile } from '../utils/filesystem'
import appMenu from '../menu'
import { getPath, isMarkdownFile, log, isFile, isDirectory, getRecommendTitle } from '../utils'
import { getPath, isMarkdownFile, isMarkdownFileOrLink, normalizeAndResolvePath, log, isFile, isDirectory, getRecommendTitle } from '../utils'
import userPreference from '../preference'
import pandoc from '../utils/pandoc'
@ -213,7 +213,7 @@ ipcMain.on('AGANI::close-window', e => {
ipcMain.on('AGANI::window::drop', async (e, fileList) => {
const win = BrowserWindow.fromWebContents(e.sender)
for (const file of fileList) {
if (isMarkdownFile(file)) {
if (isMarkdownFileOrLink(file)) {
openFileOrFolder(win, file)
break
}
@ -327,12 +327,13 @@ export const print = win => {
}
export const openFileOrFolder = (win, pathname) => {
if (isFile(pathname)) {
const resolvedPath = normalizeAndResolvePath(pathname)
if (isFile(resolvedPath)) {
const { openFilesInNewWindow } = userPreference.getAll()
if (openFilesInNewWindow) {
appWindow.createWindow(pathname)
appWindow.createWindow(resolvedPath)
} else {
loadMarkdownFile(pathname).then(rawDocument => {
loadMarkdownFile(resolvedPath).then(rawDocument => {
newTab(win, rawDocument)
}).catch(err => {
// TODO: Handle error --> create a end-user error handler.
@ -340,10 +341,10 @@ export const openFileOrFolder = (win, pathname) => {
log(err)
})
}
} else if (isDirectory(pathname)) {
appWindow.createWindow(pathname)
} else if (isDirectory(resolvedPath)) {
appWindow.createWindow(resolvedPath)
} else {
console.error(`[ERROR] Cannot open unknown file: "${pathname}"`)
console.error(`[ERROR] Cannot open unknown file: "${resolvedPath}"`)
}
}

View File

@ -1,9 +1,8 @@
import path from 'path'
import { app, systemPreferences } from 'electron'
import appWindow from './window'
import { isOsx } from './config'
import { dockMenu } from './menus'
import { isDirectory, isMarkdownFile, getMenuItemById } from './utils'
import { isDirectory, isMarkdownFileOrLink, getMenuItemById, normalizeAndResolvePath } from './utils'
import { watchers } from './utils/imagePathAutoComplement'
import { selectTheme } from './actions/theme'
import preference from './preference'
@ -70,10 +69,16 @@ class App {
for (const arg of process.argv) {
if (arg.startsWith('--')) {
continue
} else if (isDirectory(arg) || isMarkdownFile(arg)) {
// Normalize path into an absolute path.
this.openFilesCache = [ path.resolve(arg) ]
} else if (isDirectory(arg) || isMarkdownFileOrLink(arg)) {
// Normalize and resolve the path or link target.
const resolved = normalizeAndResolvePath(arg)
if (resolved) {
// TODO: Allow to open multiple files.
this.openFilesCache = [ resolved ]
break
} else {
console.error(`[ERROR] Cannot resolve "${arg}".`)
}
}
}
}

View File

@ -64,32 +64,93 @@ export const hasSameKeys = (a, b) => {
return JSON.stringify(aKeys) === JSON.stringify(bKeys)
}
/**
* 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 (e) {
// No permissions
log(e)
return false
}
}
// returns true if the path is a file with read access.
/**
* 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 (e) {
// No permissions
log(e)
return false
}
}
// returns true if the file is a supported markdown file.
/**
* 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 (e) {
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 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
}
/**
* Normalize the path into an absolute path and resolves the link target if needed.
*
* @param {string} pathname The path or link path.
* @returns {string} Returns the absolute path and resolved link. If the link target
* cannot be resolved, an empty string is returned.
*/
export const normalizeAndResolvePath = pathname => {
if (isSymbolicLink(pathname)) {
const absPath = path.dirname(pathname)
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname))
if (isFile(targetPath) || isDirectory(targetPath)) {
return path.resolve(targetPath)
}
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
return ''
}
return path.resolve(pathname)
}
export const readJson = (filePath, printError) => {
try {
const content = fs.readFileSync(filePath, 'utf-8')

View File

@ -1,10 +1,9 @@
import path from 'path'
import { app, BrowserWindow, screen } from 'electron'
import windowStateKeeper from 'electron-window-state'
import { getOsLineEndingName, loadMarkdownFile, getDefaultTextDirection } from './utils/filesystem'
import appMenu from './menu'
import Watcher from './watcher'
import { isMarkdownFile, isDirectory, log } from './utils'
import { isMarkdownFile, isDirectory, normalizeAndResolvePath, log } from './utils'
import { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux } from './config'
import userPreference from './preference'
@ -48,10 +47,17 @@ class AppWindow {
}
}
/**
* Creates a new editor window.
*
* @param {string} [pathname] Path to a file, directory or link.
* @param {string} [markdown] Markdown content.
* @param {*} [options] BrowserWindow options.
*/
createWindow (pathname = null, markdown = '', options = {}) {
// Ensure path is normalized
if (pathname) {
pathname = path.resolve(pathname)
pathname = normalizeAndResolvePath(pathname)
}
const { windows } = this
@ -88,7 +94,7 @@ class AppWindow {
mainWindowState.manage(win)
win.show()
// open single mrkdown file
// open single markdown file
if (pathname && isMarkdownFile(pathname)) {
appMenu.addRecentlyUsedDocument(pathname)
loadMarkdownFile(pathname)