mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-15 15:10:31 -08:00
refactor: move file protocol related things to standalone file
This commit is contained in:
parent
9f7a4d9f4e
commit
dc3c3efcfb
8 changed files with 214 additions and 80 deletions
|
|
@ -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
|
||||
|
|
|
|||
68
docs/features/FileProtocol.md
Normal file
68
docs/features/FileProtocol.md
Normal 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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
87
src/services/view/setupViewFileProtocol.ts
Normal file
87
src/services/view/setupViewFileProtocol.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue