Files
Desktop/app/AssetService.main.ts
2026-04-14 16:39:11 -07:00

97 lines
2.9 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createReadStream } from 'node:fs';
import { join } from 'node:path';
import { protocol } from 'electron';
import type { OptionalResourceService } from './OptionalResourceService.main.ts';
import { getAppRootDir } from '../ts/util/appRootDir.main.ts';
import { toWebStream } from '../ts/util/toWebStream.node.ts';
import * as Errors from '../ts/types/errors.std.ts';
import { createLogger } from '../ts/logging/log.std.ts';
const log = createLogger('AssetService');
const LOCAL_ASSETS = new Set([
'fonts/signal-symbols/SignalSymbolsVariable.woff2',
'fonts/stories/Hatsuishi-Regular.woff2',
'fonts/stories/EBGaramond-Regular.ttf',
'fonts/stories/Parisienne-Regular.ttf',
'fonts/stories/BarlowCondensed-Medium.ttf',
'fonts/inter-v3.19/Inter-Medium.woff2',
'fonts/inter-v3.19/Inter-Regular.woff2',
'fonts/inter-v3.19/Inter-SemiBold.woff2',
'fonts/inter-v3.19/Inter-BoldItalic.woff2',
'fonts/inter-v3.19/Inter-Bold.woff2',
'fonts/inter-v3.19/Inter-Italic.woff2',
'fonts/inter-v3.19/Inter-SemiBoldItalic.woff2',
'fonts/mono-special/MonoSpecial-Regular.woff2',
'fonts/emoji.woff2',
]);
// pathname to optional resource name
const OPTIONAL_ASSETS = new Map([
['optional-fonts/emoji-large.woff2', 'emoji-font.woff2'],
]);
export class AssetService {
readonly #resourceService: OptionalResourceService;
private constructor(resourceService: OptionalResourceService) {
this.#resourceService = resourceService;
protocol.handle('asset', async req => {
const url = new URL(req.url);
try {
return await this.#fetch(url.pathname);
} catch (error) {
log.error('protocol handler error', Errors.toLogFormat(error));
return new Response('internal error', { status: 500 });
}
});
}
public static create(resourceService: OptionalResourceService): AssetService {
return new AssetService(resourceService);
}
async #fetch(pathname: string): Promise<Response> {
if (!pathname.startsWith('/')) {
return new Response('invalid pathname', { status: 400 });
}
const path = pathname.slice(1);
if (LOCAL_ASSETS.has(path)) {
const stream = createReadStream(
join(getAppRootDir(), ...path.split('/'))
);
return new Response(toWebStream(stream), {
status: 200,
headers: {
'cache-control': 'public, max-age=2592000, immutable',
},
});
}
const optional = OPTIONAL_ASSETS.get(path);
if (optional == null) {
return new Response('asset not found', { status: 404 });
}
const asset = await this.#resourceService.getData(optional);
if (!asset) {
return new Response('optional asset not found', { status: 404 });
}
return new Response(asset, {
status: 200,
headers: {
'cache-control': 'public, max-age=2592000, immutable',
},
});
}
}