Files
ThreadNet-Web/apps/desktop/src/save-image.ts
T
hayyaksiandGitHub da500d4b7d Fetch authenticated media through the session for "Save image as" (#33997)
* Fetch authenticated media through the session for "Save image as"

The desktop "Save image as" context-menu item fetched http(s) images with the
main-process global fetch(), which bypasses the session's webRequest interceptors —
including the authenticated-media handlers in media-auth.ts that rewrite the download
URL and attach the Authorization header. On modern Synapse (authenticated media,
MSC3916) that fetch is rejected with 401/404, so saving an image failed (#32362).

Extract the save logic into a dedicated save-image.ts and fetch network URLs through
the image's Electron session (webContents.session.fetch) so the auth interceptors
apply; data: URLs are still decoded directly into a NativeImage. The module also keeps
the extension-aware encoding (jpg/jpeg/bmp/png) so its contract can be unit-tested.

* Use the contributor's own copyright header on the new save-image files
2026-07-14 11:26:06 +00:00

53 lines
2.3 KiB
TypeScript

/*
Copyright 2026 hayaksi1
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { nativeImage, type NativeImage, type Session } from "electron";
import fs from "node:fs";
import { pipeline } from "node:stream/promises";
/**
* Writes an Electron {@link NativeImage} to disk, encoding it based on the target file extension.
* Falls back to PNG for unknown extensions.
*/
export function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
switch (filePath.split(".").pop()?.toLowerCase()) {
case "jpg":
case "jpeg":
return fs.promises.writeFile(filePath, img.toJPEG(100));
case "bmp":
return fs.promises.writeFile(filePath, img.toBitmap());
case "png":
default:
return fs.promises.writeFile(filePath, img.toPNG());
}
}
/**
* Saves an image to a file on disk.
*
* `data:` URLs are decoded directly into a {@link NativeImage}. Network (`http(s):`) URLs are
* fetched through the supplied Electron {@link Session} rather than Node's global `fetch`, so that
* the session's `webRequest` interceptors apply — in particular the authenticated-media handlers in
* `media-auth.ts` which rewrite the download URL and attach the `Authorization` header. Using the
* main-process global `fetch` bypasses those interceptors and fails with 401/404 on modern Synapse
* (authenticated media, MSC3916). See https://github.com/element-hq/element-web/issues/32362.
*
* @param url - the `data:` or `http(s):` URL of the image to save
* @param filePath - the destination path on disk
* @param session - the Electron session whose `webRequest` interceptors should apply to the fetch
*/
export async function saveImageToFile(url: string, filePath: string, session: Session): Promise<void> {
if (url.startsWith("data:")) {
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
} else {
const resp = await session.fetch(url);
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
await pipeline(resp.body, fs.createWriteStream(filePath));
}
}