Add localized emoji search
This commit is contained in:
parent
ce0fb22041
commit
e90553b3b3
17 changed files with 878 additions and 97 deletions
188
app/OptionalResourceService.ts
Normal file
188
app/OptionalResourceService.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { join, dirname } from 'node:path';
|
||||
import { mkdir, readFile, readdir, writeFile, unlink } from 'node:fs/promises';
|
||||
import { createHash, timingSafeEqual } from 'node:crypto';
|
||||
import { ipcMain } from 'electron';
|
||||
import LRU from 'lru-cache';
|
||||
import got from 'got';
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
import type {
|
||||
OptionalResourceType,
|
||||
OptionalResourcesDictType,
|
||||
} from '../ts/types/OptionalResource';
|
||||
import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource';
|
||||
import * as log from '../ts/logging/log';
|
||||
import { getGotOptions } from '../ts/updater/got';
|
||||
import { drop } from '../ts/util/drop';
|
||||
|
||||
const RESOURCES_DICT_PATH = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'build',
|
||||
'optional-resources.json'
|
||||
);
|
||||
|
||||
const MAX_CACHE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
export class OptionalResourceService {
|
||||
private maybeDeclaration: OptionalResourcesDictType | undefined;
|
||||
|
||||
private readonly cache = new LRU<string, Buffer>({
|
||||
max: MAX_CACHE_SIZE,
|
||||
|
||||
length: buf => buf.length,
|
||||
});
|
||||
|
||||
private readonly fileQueues = new Map<string, PQueue>();
|
||||
|
||||
constructor(private readonly resourcesDir: string) {
|
||||
ipcMain.handle('OptionalResourceService:getData', (_event, name) =>
|
||||
this.getData(name)
|
||||
);
|
||||
|
||||
drop(this.lazyInit());
|
||||
}
|
||||
|
||||
public static create(resourcesDir: string): OptionalResourceService {
|
||||
return new OptionalResourceService(resourcesDir);
|
||||
}
|
||||
|
||||
public async getData(name: string): Promise<Buffer | undefined> {
|
||||
await this.lazyInit();
|
||||
|
||||
const decl = this.declaration[name];
|
||||
if (!decl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inMemory = this.cache.get(name);
|
||||
if (inMemory) {
|
||||
return inMemory;
|
||||
}
|
||||
|
||||
const filePath = join(this.resourcesDir, name);
|
||||
return this.queueFileWork(filePath, async () => {
|
||||
try {
|
||||
const onDisk = await readFile(filePath);
|
||||
const digest = createHash('sha512').update(onDisk).digest();
|
||||
|
||||
// Same digest and size
|
||||
if (
|
||||
timingSafeEqual(digest, Buffer.from(decl.digest, 'base64')) &&
|
||||
onDisk.length === decl.size
|
||||
) {
|
||||
log.warn(`OptionalResourceService: loaded ${name} from disk`);
|
||||
this.cache.set(name, onDisk);
|
||||
return onDisk;
|
||||
}
|
||||
|
||||
log.warn(`OptionalResourceService: ${name} is no longer valid on disk`);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// We get here if file doesn't exist or if its digest/size is different
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch {
|
||||
// Just do our best effort and move forward
|
||||
}
|
||||
|
||||
return this.fetch(name, decl, filePath);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
private async lazyInit(): Promise<void> {
|
||||
if (this.maybeDeclaration !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readFile(RESOURCES_DICT_PATH, 'utf8'));
|
||||
this.maybeDeclaration = OptionalResourcesDictSchema.parse(json);
|
||||
|
||||
// Clean unknown resources
|
||||
let subPaths: Array<string>;
|
||||
try {
|
||||
subPaths = await readdir(this.resourcesDir);
|
||||
} catch (error) {
|
||||
// Directory wasn't created yet
|
||||
if (error.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
subPaths.map(async subPath => {
|
||||
if (this.declaration[subPath]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = join(this.resourcesDir, subPath);
|
||||
|
||||
try {
|
||||
await unlink(fullPath);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`OptionalResourceService: failed to cleanup ${subPath}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private get declaration(): OptionalResourcesDictType {
|
||||
if (this.maybeDeclaration === undefined) {
|
||||
throw new Error('optional-resources.json not loaded yet');
|
||||
}
|
||||
return this.maybeDeclaration;
|
||||
}
|
||||
|
||||
private async queueFileWork<R>(
|
||||
filePath: string,
|
||||
body: () => Promise<R>
|
||||
): Promise<R> {
|
||||
let queue = this.fileQueues.get(filePath);
|
||||
if (!queue) {
|
||||
queue = new PQueue({ concurrency: 1 });
|
||||
this.fileQueues.set(filePath, queue);
|
||||
}
|
||||
try {
|
||||
return await queue.add(body);
|
||||
} finally {
|
||||
if (queue.size === 0) {
|
||||
this.fileQueues.delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fetch(
|
||||
name: string,
|
||||
decl: OptionalResourceType,
|
||||
destPath: string
|
||||
): Promise<Buffer> {
|
||||
const result = await got(decl.url, getGotOptions()).buffer();
|
||||
|
||||
this.cache.set(name, result);
|
||||
|
||||
try {
|
||||
await mkdir(dirname(destPath), { recursive: true });
|
||||
await writeFile(destPath, result);
|
||||
} catch (error) {
|
||||
log.error('OptionalResourceService: failed to save file', error);
|
||||
// Still return the data that we just fetched
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue