refactor: move file protocol related things to standalone file

This commit is contained in:
linonetwo 2023-06-20 22:11:39 +08:00 committed by lin onetwo
parent 9f7a4d9f4e
commit dc3c3efcfb
8 changed files with 214 additions and 80 deletions

View file

@ -85,4 +85,6 @@ Some library doesn't fit electron usage, we move their code to this repo and mod
## Code Tour
[FileProtocol](./features/FileProtocol.md)
TBD

View file

@ -0,0 +1,68 @@
# FileProtocol
## Click the link
Normally, link like
```wikitext
[ext[外部文件|file:///Users/linonetwo/Downloads/(OCRed)奖励的惩罚 ((美)科恩著) (Z-Library).pdf]]
[ext[外部文件夹|file:///Users/linonetwo/Downloads/]]
```
Will become external link that will open new window, so this feature is handled in `handleOpenFileExternalLink` in `src/services/view/setupViewEventHandlers.ts`.
## Load the file content
Image syntax like
```wikitext
[img[file://./files/1644384970572.jpeg]]
```
will ask `view.webContent` to send a request, which will be handled in `handleFileProtocol` in `src/services/view/setupViewSession.ts`.
We can switch to this
```ts
public async handleFileProtocol(request: GlobalRequest): Promise<GlobalResponse> {
logger.info('handleFileProtocol() getting url', { url: request.url });
const { pathname } = new URL(request.url);
logger.info('handleFileProtocol() handle file:// or open:// This url will open file in-wiki', { pathname });
let fileExists = fs.existsSync(pathname);
logger.info(`This file (decodeURI) ${fileExists ? '' : 'not '}exists`, { pathname });
if (fileExists) {
return await net.fetch(pathname);
}
logger.info(`try find file relative to workspace folder`);
const workspace = await this.workspaceService.getActiveWorkspace();
if (workspace === undefined) {
logger.error(`No active workspace, abort. Try loading pathname as-is.`, { pathname });
return await net.fetch(pathname);
}
const filePathInWorkspaceFolder = path.resolve(workspace.wikiFolderLocation, pathname);
fileExists = fs.existsSync(filePathInWorkspaceFolder);
logger.info(`This file ${fileExists ? '' : 'not '}exists in workspace folder.`, { filePathInWorkspaceFolder });
if (fileExists) {
return await net.fetch(filePathInWorkspaceFolder);
}
logger.info(`try find file relative to TidGi App folder`);
// on production, __dirname will be in .webpack/main
const inTidGiAppAbsoluteFilePath = path.join(app.getAppPath(), '.webpack', 'renderer', pathname);
fileExists = fs.existsSync(inTidGiAppAbsoluteFilePath);
if (fileExists) {
return await net.fetch(inTidGiAppAbsoluteFilePath);
}
logger.warn(`This url can't be loaded in-wiki. Try loading url as-is.`, { url: request.url });
return await net.fetch(request.url);
}
```
if
```ts
await app.whenReady();
protocol.handle('file', nativeService.handleFileProtocol.bind(nativeService));
```
works. But currently it is not. `protocol.handle('file'`'s handler won't receive anything.

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/require-await */
import { app, dialog, ipcMain, MessageBoxOptions, protocol, shell } from 'electron';
import { app, dialog, ipcMain, MessageBoxOptions, shell } from 'electron';
import fs from 'fs-extra';
import { inject, injectable } from 'inversify';
import path from 'path';
@ -248,4 +248,40 @@ ${message.message}
}
}
}
public async formatFileUrlToAbsolutePath(request: { url: string }, callback: (response: string) => void): Promise<void> {
logger.info('getting url', { url: request.url, function: 'formatFileUrlToAbsolutePath' });
const pathname = decodeURI(request.url.replace('open://', '').replace('file://', ''));
logger.info('handle file:// or open:// This url will open file in-wiki', { pathname, function: 'formatFileUrlToAbsolutePath' });
let fileExists = fs.existsSync(pathname);
logger.info(`This file (decodeURI) ${fileExists ? '' : 'not '}exists`, { pathname, function: 'formatFileUrlToAbsolutePath' });
if (fileExists) {
callback(pathname);
return;
}
logger.info(`try find file relative to workspace folder`, { pathname, function: 'formatFileUrlToAbsolutePath' });
const workspace = await this.workspaceService.getActiveWorkspace();
if (workspace === undefined) {
logger.error(`No active workspace, abort. Try loading pathname as-is.`, { pathname, function: 'formatFileUrlToAbsolutePath' });
callback(pathname);
return;
}
const filePathInWorkspaceFolder = path.resolve(workspace.wikiFolderLocation, pathname);
fileExists = fs.existsSync(filePathInWorkspaceFolder);
logger.info(`This file ${fileExists ? '' : 'not '}exists in workspace folder.`, { filePathInWorkspaceFolder, function: 'formatFileUrlToAbsolutePath' });
if (fileExists) {
callback(filePathInWorkspaceFolder);
return;
}
// on production, __dirname will be in .webpack/main
const inTidGiAppAbsoluteFilePath = path.join(app.getAppPath(), '.webpack', 'renderer', pathname);
logger.info(`try find file relative to TidGi App folder`, { inTidGiAppAbsoluteFilePath, function: 'formatFileUrlToAbsolutePath' });
fileExists = fs.existsSync(inTidGiAppAbsoluteFilePath);
if (fileExists) {
callback(inTidGiAppAbsoluteFilePath);
return;
}
logger.warn(`This url can't be loaded in-wiki. Try loading url as-is.`, { url: request.url, function: 'formatFileUrlToAbsolutePath' });
callback(request.url);
}
}

View file

@ -32,8 +32,12 @@ export interface INativeService {
* @param workspaceID Each wiki has its own worker, we use wiki's workspaceID to determine which worker to use. If not provided, will use current active workspace's ID
*/
executeZxScript$(zxWorkerArguments: IZxFileInput, workspaceID?: string): Observable<string>;
/**
* Handles in-app assets loading. This should be called after `app.whenReady()` is resolved.
* This handles file:// protocol when webview load image content, not handling file external link clicking.
*/
formatFileUrlToAbsolutePath(request: { url: string }, callback: (response: string) => void): Promise<void>;
getLocalHostUrlWithActualInfo(urlToReplace: string, workspaceID: string): Promise<string>;
handleFileProtocol(request: { url: string }, callback: (response: string) => void): Promise<void>;
log(level: string, message: string, meta?: Record<string, unknown>): Promise<void>;
open(uri: string, isDirectory?: boolean): Promise<void>;
openInEditor(filePath: string, editorName?: string | undefined): Promise<boolean>;

View file

@ -5,6 +5,15 @@ import { WindowNames } from '@services/windows/WindowProperties';
import { IWorkspace } from '@services/workspaces/interface';
import { ProxyPropertyType } from 'electron-ipc-cat/common';
export type INewWindowAction =
| {
action: 'deny';
}
| {
action: 'allow';
overrideBrowserWindowOptions?: Electron.BrowserWindowConstructorOptions | undefined;
};
/**
* BrowserView related things, the BrowserView is the webview like frame that renders our wiki website.
*/

View file

@ -25,6 +25,8 @@ import { IBrowserViewMetaData, windowDimension, WindowNames } from '@services/wi
import type { IWorkspaceService } from '@services/workspaces/interface';
import type { IWorkspaceViewService } from '@services/workspacesView/interface';
import { throttle } from 'lodash';
import { INewWindowAction } from './interface';
import { handleOpenFileExternalLink, handleViewFileContentLoading } from './setupViewFileProtocol';
export interface IViewContext {
loadInitialUrlWithCatch: () => Promise<void>;
@ -56,6 +58,7 @@ export default function setupViewEventHandlers(
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
handleViewFileContentLoading(view, nativeService);
view.webContents.on('did-start-loading', async () => {
const workspaceObject = await workspaceService.get(workspace.id);
// this event might be triggered
@ -306,15 +309,7 @@ function handleNewWindow(
newWindowContext: INewWindowContext,
disposition: 'default' | 'new-window' | 'foreground-tab' | 'background-tab' | 'save-to-disk' | 'other',
parentWebContents: Electron.WebContents,
):
| {
action: 'deny';
}
| {
action: 'allow';
overrideBrowserWindowOptions?: Electron.BrowserWindowConstructorOptions | undefined;
}
{
): INewWindowAction {
logger.debug(`Getting url that will open externally`, { nextUrl });
// don't show useless blank page
if (nextUrl.startsWith('about:blank')) {
@ -322,43 +317,10 @@ function handleNewWindow(
return { action: 'deny' };
}
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
const nextDomain = extractDomain(nextUrl);
/**
* Handles in-wiki file link opening.
* This does not handle web request with file:// protocol.
*
* `file://` may resulted in `nextDomain` being `about:blank#blocked`, so we use `open://` instead. But in MacOS it seem to works fine in most cases. Just leave open:// in case as a fallback for users.
*
* For file:/// in-app assets loading., see handleFileProtocol() in `src/services/native/index.ts`.
*/
if (nextUrl.startsWith('open://') || nextUrl.startsWith('file://')) {
logger.info('handleNewWindow() handle file:// or open:// This url will open file externally', { nextUrl, nextDomain, disposition });
const filePath = decodeURI(nextUrl.replace('open://', '').replace('file://', ''));
const fileExists = fsExtra.existsSync(filePath);
logger.info(`This file (decodeURI) ${fileExists ? '' : 'not '}exists`, { filePath });
if (fileExists) {
void shell.openPath(filePath);
return {
action: 'deny',
};
}
logger.info(`try find file relative to workspace folder`);
void workspaceService.getActiveWorkspace().then((workspace) => {
if (workspace !== undefined) {
const filePathInWorkspaceFolder = path.resolve(workspace.wikiFolderLocation, filePath);
const fileExistsInWorkspaceFolder = fsExtra.existsSync(filePathInWorkspaceFolder);
logger.info(`This file ${fileExistsInWorkspaceFolder ? '' : 'not '}exists in workspace folder.`, { filePathInWorkspaceFolder });
if (fileExistsInWorkspaceFolder) {
void shell.openPath(filePathInWorkspaceFolder);
}
}
});
return {
action: 'deny',
};
}
const handleOpenFileExternalLinkAction = handleOpenFileExternalLink(nextUrl, nextDomain, disposition);
if (handleOpenFileExternalLinkAction !== undefined) return handleOpenFileExternalLinkAction;
// open external url in browser
if (nextDomain !== undefined && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
logger.debug('handleNewWindow() openExternal', { nextUrl, nextDomain, disposition });

View file

@ -0,0 +1,87 @@
import { container } from '@services/container';
import { logger } from '@services/libs/log';
import { INativeService } from '@services/native/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IWorkspaceService } from '@services/workspaces/interface';
import { BrowserView, shell } from 'electron';
import fs from 'fs-extra';
import path from 'path';
import { INewWindowAction } from './interface';
/**
* Handles in-wiki file link opening.
* This does not handle web request with file:// protocol.
*
* `file://` may resulted in `nextDomain` being `about:blank#blocked`, so we use `open://` instead. But in MacOS it seem to works fine in most cases. Just leave open:// in case as a fallback for users.
*
* For file:/// in-app assets loading., see handleFileProtocol() in `src/services/native/index.ts`.
*/
export function handleOpenFileExternalLink(
nextUrl: string,
nextDomain: string | undefined,
disposition: 'default' | 'new-window' | 'foreground-tab' | 'background-tab' | 'save-to-disk' | 'other',
): INewWindowAction | undefined {
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
if (nextUrl.startsWith('open://') || nextUrl.startsWith('file://')) {
logger.info('handleNewWindow() handle file:// or open:// This url will open file externally', { nextUrl, nextDomain, disposition });
const filePath = decodeURI(nextUrl.replace('open://', '').replace('file://', ''));
const fileExists = fs.existsSync(filePath);
logger.info(`This file (decodeURI) ${fileExists ? '' : 'not '}exists`, { filePath });
if (fileExists) {
void shell.openPath(filePath);
return {
action: 'deny',
};
}
logger.info(`try find file relative to workspace folder`);
void workspaceService.getActiveWorkspace().then((workspace) => {
if (workspace !== undefined) {
const filePathInWorkspaceFolder = path.resolve(workspace.wikiFolderLocation, filePath);
const fileExistsInWorkspaceFolder = fs.existsSync(filePathInWorkspaceFolder);
logger.info(`This file ${fileExistsInWorkspaceFolder ? '' : 'not '}exists in workspace folder.`, { filePathInWorkspaceFolder });
if (fileExistsInWorkspaceFolder) {
void shell.openPath(filePathInWorkspaceFolder);
}
}
});
return {
action: 'deny',
};
}
}
/* eslint-disable n/no-callback-literal */
/**
* Handle file protocol in webview to request file content and show in the view.
*/
export function handleViewFileContentLoading(view: BrowserView, nativeService: INativeService) {
view.webContents.session.webRequest.onBeforeRequest((details, callback) => {
// DEBUG: console details
console.log(`details`, details);
if (details.url.startsWith('file://') || details.url.startsWith('open://')) {
void handleFileLink(details, nativeService, callback);
} else {
callback({
cancel: false,
});
}
});
}
async function handleFileLink(details: Electron.OnBeforeRequestListenerDetails, nativeService: INativeService, callback: (response: Electron.CallbackResponse) => void) {
await nativeService.formatFileUrlToAbsolutePath({ url: details.url }, (redirectURL: string) => {
// DEBUG: console redirectURL
console.log(`redirectURL`, redirectURL);
if (redirectURL === details.url) {
callback({
cancel: false,
});
} else {
callback({
cancel: false,
redirectURL,
});
}
});
}

View file

@ -2,15 +2,11 @@
import { session } from 'electron';
import { isMac } from '@/helpers/system';
import { container } from '@services/container';
import { INativeService } from '@services/native/interface';
import { IPreferences } from '@services/preferences/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IWorkspace } from '@services/workspaces/interface';
export function setupViewSession(workspace: IWorkspace, preferences: IPreferences) {
const { shareWorkspaceBrowsingData, spellcheck, spellcheckLanguages } = preferences;
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
// configure session, proxy & ad blocker
const partitionId = shareWorkspaceBrowsingData ? 'persist:shared' : `persist:${workspace.id}`;
@ -25,16 +21,6 @@ export function setupViewSession(workspace: IWorkspace, preferences: IPreference
assignFakeUserAgent(details);
callback({ cancel: false, requestHeaders: details.requestHeaders });
});
sessionOfView.webRequest.onBeforeRequest((details, callback) => {
if (details.url.startsWith('file://') || details.url.startsWith('open://')) {
void handleFileLink(details, nativeService, callback);
} else {
callback({
cancel: false,
});
}
});
handleFileProtocol(sessionOfView, nativeService);
return sessionOfView;
}
@ -46,23 +32,3 @@ function assignFakeUserAgent(details: Electron.OnBeforeSendHeadersListenerDetail
details.requestHeaders.Referer = details.url;
details.requestHeaders['User-Agent'] = FAKE_USER_AGENT;
}
function handleFileProtocol(sessionOfView: Electron.Session, nativeService: INativeService) {
// this normally nor called. In wiki file:// image will use `handleFileLink()` below.
sessionOfView.protocol.registerFileProtocol('file', nativeService.handleFileProtocol.bind(nativeService));
}
async function handleFileLink(details: Electron.OnBeforeRequestListenerDetails, nativeService: INativeService, callback: (response: Electron.CallbackResponse) => void) {
await nativeService.handleFileProtocol({ url: details.url }, (redirectURL: string) => {
if (redirectURL === details.url) {
callback({
cancel: false,
});
} else {
callback({
cancel: false,
redirectURL,
});
}
});
}