TidGi-Desktop/docs/features/FileProtocol.md
2025-10-08 23:19:58 +08:00

5.2 KiB

FileProtocol

Normally, link like

[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/setupViewFileProtocol.ts.

Load the file content

Image syntax like

[img[file://./files/1644384970572.jpeg]]

will ask view.webContent to send a request, which will be handled in handleViewFileContentLoading in src/services/view/setupViewFileProtocol.ts, we use onBeforeRequest to catch this request.

If the url in the request is absolute, we just let webContent load it as normal. We use callback({ cancel: false }); to hand back control to webContent.

If it is relative to workspace path, we use nativeService.formatFileUrlToAbsolutePath(details.url) to get the absolute path, and pass it to redirectURL to ask ``webContent` to load this new absolute path.

Deprecated ways

protocol.handle('filefix')

This implementation is buggy, that will crash app when loading pdf.

const fileTypeThatWillCrash = new Set(['.pdf']);
async function loadFileContentHandler(request: Request) {
  let { pathname } = new URL(request.url);
  pathname = decodeURIComponent(pathname);
  logger.info(`Loading file content from ${pathname}`, { function: 'handleViewFileContentLoading view.webContents.session.protocol.handle' });
  try {
    // mimeType will be `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7` so is useless, `contentType` will also be `null`
    // const mimeType = request.headers.get('accept');
    // const contentType = request.headers.get('Content-Type');
    const extname = path.extname(pathname);
    if (fileTypeThatWillCrash.has(extname)) {
      return new Response(undefined, { status: 500, statusText: `${extname} will crash electron, prevented loading.` });
    }
    const response = await net.fetch(pathToFileURL(pathname).toString(), {
      method: request.method,
      headers: request.headers,
      body: request.body,
      bypassCustomProtocolHandlers: true,
    });
    logger.info(`${pathname} loaded`, { function: 'handleViewFileContentLoading view.webContents.session.protocol.handle' });
    return response;
  } catch (error) {
    return new Response(undefined, { status: 404, statusText: (error as Error).message });
  }
}

try {
  /**
   * This function is called for every view, but seems register on two different view will throw error, so we check if it's already registered.
   */
  if (!view.webContents.session.protocol.isProtocolHandled('filefix')) {
    /**
     * Electron's bug, file protocol is not handle-able, won't get any callback. But things like `filea://` `filefix` works.
     */
    view.webContents.session.protocol.handle('filefix', loadFileContentHandler);
  }
  /**
   * Alternative `open://` protocol for a backup if `file://` doesn't work for some reason.
   */
  if (!view.webContents.session.protocol.isProtocolHandled('open')) {
    view.webContents.session.protocol.handle('open', loadFileContentHandler);
  }
} catch (error) {
  logger.error(`Failed to register protocol: ${(error as Error).message}`, { function: 'handleViewFileContentLoading' });
}

protocol.handle('file')

protocol.handle('file''s handler won't receive anything.

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

await app.whenReady();
protocol.handle('file', nativeService.handleFileProtocol.bind(nativeService));

works. But currently it is not.