Files
readest/apps/readest-app/src/services/sync/replicaBinaryUpload.ts
T
Huang Xin 744d5b3a03 fix(dict): restore MDD eager init; CSP-safe audio handler; gate binary uploads on sync category (#4337)
* fix(dict): restore MDD eager init; CSP-safe Vocabulary.com audio handler

Two MDict fixes:

1. **Regression**: #4334 turned on `MDD.create(..., { lazy: true })` so
   the audio / resource side of every MDict would skip the upfront
   key-block decode + sort. That's the right trade-off on the MDX side
   (millions of headwords, ~80 s init saving), but it doesn't transfer
   to MDDs: js-mdict's lazy lookup binary-searches `keyInfoList`
   envelopes with `comp` (localeCompare), and MDDs store keys in
   byte-sorted order with `\` prefixes and case folding. The two orders
   disagree just often enough that `mdd.locateBytes('sound.png')`
   misses on resource paths that do exist — the icon's CSS
   `background-image: url(sound.png)` then stays unresolved and the
   speaker glyph disappears. Bisected to 93abca89 (#4334). Drop the
   flag for MDDs; MDXs keep the lazy win.

2. **New**: a CSP-safe replacement for the `onclick="v0r.v(this,'KEY')"`
   audio handler used by Vocabulary.com-derived MDicts. The dict's
   `j.js` is never loaded inside our shadow root (we don't execute
   MDX-supplied JavaScript), so without intervention the inline
   handler would throw `ReferenceError: v0r is not defined` on every
   click. The new helper parses the audio key from any matching
   onclick, strips that one attribute, and binds our own listener that
   probes the companion MDD for `<KEY>.mp3` / `<KEY>.m4a` / etc. and
   plays the bytes through an `<Audio>` element. Other unrelated
   inline handlers are deliberately left alone — they were no-ops
   already (their helper JS isn't loaded), and CSS rules sometimes
   match on `[onclick]` attribute presence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): skip binary upload when sync category is disabled

`publishReplicaUpsert` already gates the metadata row on
`isSyncCategoryEnabled(kind)` so a user who turned dictionary / font /
texture sync off in Manage Sync never publishes a row for that kind.
`queueReplicaBinaryUpload` had no such gate, so the binary side still
fired: a 250 MB MDict bundle or a few hundred MB of font files would
still upload to cloud storage with no replica row to reference them.

Mirror the gate here. The `kind` parameter (`dictionary`, `font`,
`texture`) lines up with `SyncCategory` names verbatim, so a single
`isSyncCategoryEnabled(kind)` check at the top of the function is all
it takes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:13:18 +02:00

97 lines
3.5 KiB
TypeScript

import { transferManager } from '@/services/transferManager';
import { getReplicaAdapter } from './replicaRegistry';
import { isSyncCategoryEnabled } from './syncCategories';
import { getAccessToken } from '@/utils/access';
import type { AppService, BaseDir } from '@/types/system';
import type { ReplicaTransferFile } from '@/store/transferStore';
import type { ClosableFile } from '@/utils/file';
/**
* Open the file just to read its `.size` (Tauri streams metadata; the
* body isn't read). Used as a fallback when the adapter's
* enumerateFiles doesn't carry a byteSize — e.g. the dictionary
* adapter, which doesn't track per-file sizes on its records.
*/
const resolveByteSize = async (
appService: AppService,
lfp: string,
base: BaseDir,
): Promise<number> => {
const file = await appService.openFile(lfp, base);
const size = file.size;
const closable = file as ClosableFile;
if (closable && closable.close) {
await closable.close();
}
return size;
};
interface ReplicaBinaryRecord {
contentId?: string;
name: string;
reincarnation?: string;
}
/**
* Queue a replica's binary files for upload via TransferManager.
* Generic across kinds — uses the adapter registry to dispatch:
* - adapter.binary.enumerateFiles to list the per-record files
* - adapter.binary.localBaseDir for the upload base
*
* Resolves missing byteSize entries via openFile so progress reporting
* is accurate. The TransferManager fires `replica-transfer-complete`
* on success; replicaTransferIntegration converts that into a
* publishReplicaManifest call (binaries first, manifest last).
*
* No-op when:
* - the user has turned the matching sync category off in Manage Sync
* (`dictionary`, `font`, `texture` — i.e. backgrounds). Skipping
* here keeps potentially large bundle uploads from leaving the
* device when the user has opted out, and matches the metadata-side
* gate already enforced by `publishReplicaUpsert`.
* - the record lacks contentId (legacy / unsynced)
* - the kind isn't registered or has no binary capability
* - TransferManager isn't initialized yet (pre-library mount)
*
* Caller is responsible for ordering this AFTER publishReplicaUpsert
* so the metadata row exists before the manifest commit fires.
*/
export const queueReplicaBinaryUpload = async <T extends ReplicaBinaryRecord>(
kind: string,
record: T,
appService: AppService,
): Promise<string | null> => {
if (!isSyncCategoryEnabled(kind)) return null;
if (!record.contentId) return null;
if (!(await getAccessToken())) return null;
if (!transferManager.isReady()) return null;
const adapter = getReplicaAdapter<T>(kind);
if (!adapter?.binary) return null;
const base = adapter.binary.localBaseDir;
const enumerated = adapter.binary.enumerateFiles(record);
if (enumerated.length === 0) return null;
const files: ReplicaTransferFile[] = await Promise.all(
enumerated.map(async (f) => ({
logical: f.logical,
lfp: f.lfp,
byteSize: f.byteSize > 0 ? f.byteSize : await resolveByteSize(appService, f.lfp, base),
})),
);
return transferManager.queueReplicaUpload(kind, record.contentId, record.name, files, base, {
reincarnation: record.reincarnation,
});
};
/**
* Backwards-compatible alias for the dictionary-specific helper that
* call sites used before the kind-agnostic refactor.
*/
export const queueDictionaryBinaryUpload = <T extends ReplicaBinaryRecord>(
dict: T,
appService: AppService,
): Promise<string | null> => queueReplicaBinaryUpload('dictionary', dict, appService);