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 multiple parser issues (update marked.js to v0.6.1)
- Fixed [...] is displayed in gray and orange (#432) - Fixed [...] is displayed in gray and orange (#432)
- Fixed an issue that relative images are not loaded after closing a tab - Fixed an issue that relative images are not loaded after closing a tab
- Add symbolic link support
### 0.13.65 ### 0.13.65

View File

@ -7,7 +7,7 @@ import appWindow from '../window'
import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../config' import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../config'
import { loadMarkdownFile, writeFile, writeMarkdownFile } from '../utils/filesystem' import { loadMarkdownFile, writeFile, writeMarkdownFile } from '../utils/filesystem'
import appMenu from '../menu' 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 userPreference from '../preference'
import pandoc from '../utils/pandoc' import pandoc from '../utils/pandoc'
@ -213,7 +213,7 @@ ipcMain.on('AGANI::close-window', e => {
ipcMain.on('AGANI::window::drop', async (e, fileList) => { ipcMain.on('AGANI::window::drop', async (e, fileList) => {
const win = BrowserWindow.fromWebContents(e.sender) const win = BrowserWindow.fromWebContents(e.sender)
for (const file of fileList) { for (const file of fileList) {
if (isMarkdownFile(file)) { if (isMarkdownFileOrLink(file)) {
openFileOrFolder(win, file) openFileOrFolder(win, file)
break break
} }
@ -327,12 +327,13 @@ export const print = win => {
} }
export const openFileOrFolder = (win, pathname) => { export const openFileOrFolder = (win, pathname) => {
if (isFile(pathname)) { const resolvedPath = normalizeAndResolvePath(pathname)
if (isFile(resolvedPath)) {
const { openFilesInNewWindow } = userPreference.getAll() const { openFilesInNewWindow } = userPreference.getAll()
if (openFilesInNewWindow) { if (openFilesInNewWindow) {
appWindow.createWindow(pathname) appWindow.createWindow(resolvedPath)
} else { } else {
loadMarkdownFile(pathname).then(rawDocument => { loadMarkdownFile(resolvedPath).then(rawDocument => {
newTab(win, rawDocument) newTab(win, rawDocument)
}).catch(err => { }).catch(err => {
// TODO: Handle error --> create a end-user error handler. // TODO: Handle error --> create a end-user error handler.
@ -340,10 +341,10 @@ export const openFileOrFolder = (win, pathname) => {
log(err) log(err)
}) })
} }
} else if (isDirectory(pathname)) { } else if (isDirectory(resolvedPath)) {
appWindow.createWindow(pathname) appWindow.createWindow(resolvedPath)
} else { } 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 { app, systemPreferences } from 'electron'
import appWindow from './window' import appWindow from './window'
import { isOsx } from './config' import { isOsx } from './config'
import { dockMenu } from './menus' import { dockMenu } from './menus'
import { isDirectory, isMarkdownFile, getMenuItemById } from './utils' import { isDirectory, isMarkdownFileOrLink, getMenuItemById, normalizeAndResolvePath } from './utils'
import { watchers } from './utils/imagePathAutoComplement' import { watchers } from './utils/imagePathAutoComplement'
import { selectTheme } from './actions/theme' import { selectTheme } from './actions/theme'
import preference from './preference' import preference from './preference'
@ -70,10 +69,16 @@ class App {
for (const arg of process.argv) { for (const arg of process.argv) {
if (arg.startsWith('--')) { if (arg.startsWith('--')) {
continue continue
} else if (isDirectory(arg) || isMarkdownFile(arg)) { } else if (isDirectory(arg) || isMarkdownFileOrLink(arg)) {
// Normalize path into an absolute path. // Normalize and resolve the path or link target.
this.openFilesCache = [ path.resolve(arg) ] const resolved = normalizeAndResolvePath(arg)
break 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) 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 => { export const isDirectory = dirPath => {
try { try {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory() return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
} catch (e) { } catch (e) {
// No permissions
log(e)
return false 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 => { export const isFile = filepath => {
try { try {
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile() return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile()
} catch (e) { } catch (e) {
// No permissions
log(e)
return false 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 => { export const isMarkdownFile = filepath => {
return isFile(filepath) && hasMarkdownExtension(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) => { export const readJson = (filePath, printError) => {
try { try {
const content = fs.readFileSync(filePath, 'utf-8') const content = fs.readFileSync(filePath, 'utf-8')

View File

@ -1,10 +1,9 @@
import path from 'path'
import { app, BrowserWindow, screen } from 'electron' import { app, BrowserWindow, screen } from 'electron'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { getOsLineEndingName, loadMarkdownFile, getDefaultTextDirection } from './utils/filesystem' import { getOsLineEndingName, loadMarkdownFile, getDefaultTextDirection } from './utils/filesystem'
import appMenu from './menu' import appMenu from './menu'
import Watcher from './watcher' 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 { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux } from './config'
import userPreference from './preference' 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 = {}) { createWindow (pathname = null, markdown = '', options = {}) {
// Ensure path is normalized // Ensure path is normalized
if (pathname) { if (pathname) {
pathname = path.resolve(pathname) pathname = normalizeAndResolvePath(pathname)
} }
const { windows } = this const { windows } = this
@ -88,7 +94,7 @@ class AppWindow {
mainWindowState.manage(win) mainWindowState.manage(win)
win.show() win.show()
// open single mrkdown file // open single markdown file
if (pathname && isMarkdownFile(pathname)) { if (pathname && isMarkdownFile(pathname)) {
appMenu.addRecentlyUsedDocument(pathname) appMenu.addRecentlyUsedDocument(pathname)
loadMarkdownFile(pathname) loadMarkdownFile(pathname)