diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 02ff8c46..e09f4285 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -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 diff --git a/src/main/actions/file.js b/src/main/actions/file.js index a07ebad1..b471e352 100644 --- a/src/main/actions/file.js +++ b/src/main/actions/file.js @@ -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}"`) } } diff --git a/src/main/app.js b/src/main/app.js index fed5c070..43fdd8ec 100644 --- a/src/main/app.js +++ b/src/main/app.js @@ -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) ] - break + } 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}".`) + } } } } diff --git a/src/main/utils/index.js b/src/main/utils/index.js index 7ac955ed..0bb16f0f 100644 --- a/src/main/utils/index.js +++ b/src/main/utils/index.js @@ -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') diff --git a/src/main/window.js b/src/main/window.js index 534b6863..89e27b21 100644 --- a/src/main/window.js +++ b/src/main/window.js @@ -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)