mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix(import): report manifest shards the archive is missing
A shard listed in the manifest but absent from the zip was filtered out silently, so the surviving shards were treated as the whole export: inspection undercounted it and the job reported success having skipped every conversation in the missing file.
This commit is contained in:
parent
29f39b32de
commit
ea8cf18f61
3 changed files with 68 additions and 6 deletions
|
|
@ -255,3 +255,33 @@ describe('detectExportFormat', () => {
|
|||
expect(detectExportFormat('nope')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveLayout missing shards', () => {
|
||||
it('reports a manifest-listed shard the archive does not contain', () => {
|
||||
const manifest = parseManifest(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
logical_files: {
|
||||
'conversations.json': {
|
||||
files: ['conversations-000.json', 'conversations-001.json'],
|
||||
sharded: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const layout = resolveLayout([{ name: 'conversations-000.json', bytes: 2 }], manifest);
|
||||
|
||||
expect(layout.conversationShards).toEqual(['conversations-000.json']);
|
||||
expect(layout.missingShards).toEqual(['conversations-001.json']);
|
||||
});
|
||||
|
||||
it('reports nothing missing when the filename fallback is used', () => {
|
||||
const layout = resolveLayout([{ name: 'conversations.json', bytes: 2 }], null);
|
||||
|
||||
expect(layout.conversationShards).toEqual(['conversations.json']);
|
||||
expect(layout.missingShards).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ export interface ExportManifest {
|
|||
export interface ExportLayout {
|
||||
version: number | null;
|
||||
conversationShards: string[];
|
||||
/** Shards the manifest lists that the archive does not contain. Dropping
|
||||
* them silently made a truncated export look complete: the remaining shards
|
||||
* were treated as the whole thing, so inspection undercounted and the job
|
||||
* reported success having skipped the missing conversations. */
|
||||
missingShards: string[];
|
||||
assetNames: string | null;
|
||||
assetEntries: ArchiveEntry[];
|
||||
}
|
||||
|
|
@ -58,12 +63,25 @@ export function parseManifest(buffer: Buffer): ExportManifest | null {
|
|||
}
|
||||
}
|
||||
|
||||
function shardsFromManifest(manifest: ExportManifest, present: Set<string>): string[] {
|
||||
function shardsFromManifest(
|
||||
manifest: ExportManifest,
|
||||
present: Set<string>,
|
||||
): { found: string[]; missing: string[] } {
|
||||
const logical = manifest.logical_files[CONVERSATIONS_LOGICAL];
|
||||
if (!logical?.files || !Array.isArray(logical.files) || logical.files.length === 0) {
|
||||
return [];
|
||||
return { found: [], missing: [] };
|
||||
}
|
||||
return logical.files.filter((name) => present.has(name));
|
||||
|
||||
const found: string[] = [];
|
||||
const missing: string[] = [];
|
||||
for (const name of logical.files) {
|
||||
if (present.has(name)) {
|
||||
found.push(name);
|
||||
continue;
|
||||
}
|
||||
missing.push(name);
|
||||
}
|
||||
return { found, missing };
|
||||
}
|
||||
|
||||
function grokShards(entries: ArchiveEntry[]): string[] {
|
||||
|
|
@ -187,13 +205,20 @@ export function resolveLayout(
|
|||
): ExportLayout {
|
||||
const present = new Set(entries.map((entry) => entry.name));
|
||||
|
||||
const fromManifest = manifest ? shardsFromManifest(manifest, present) : [];
|
||||
const conversationShards =
|
||||
fromManifest.length > 0 ? fromManifest : shardsFromFilenames(entries, present);
|
||||
const fromManifest = manifest
|
||||
? shardsFromManifest(manifest, present)
|
||||
: { found: [], missing: [] };
|
||||
const useManifest = fromManifest.found.length > 0;
|
||||
const conversationShards = useManifest
|
||||
? fromManifest.found
|
||||
: shardsFromFilenames(entries, present);
|
||||
|
||||
return {
|
||||
version: manifest?.version ?? null,
|
||||
conversationShards,
|
||||
/** Only meaningful when the manifest was actually used: the filename
|
||||
* fallback has no declared list to be missing from. */
|
||||
missingShards: useManifest ? fromManifest.missing : [],
|
||||
assetNames: present.has(ASSET_NAMES_ENTRY) ? ASSET_NAMES_ENTRY : null,
|
||||
assetEntries: entries.filter((entry) => entry.name.endsWith('.dat')),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -223,6 +223,13 @@ export async function runImport(input: RunImportInput): Promise<ImportReport> {
|
|||
const manifest = hasManifest ? parseManifest(await archive.read(MANIFEST_ENTRY)) : null;
|
||||
const layout = resolveLayout(archive.entries, manifest);
|
||||
|
||||
/** A shard the manifest declares but the archive omits is missing data, not
|
||||
* an absent feature: without this the run imports what survived and reports
|
||||
* success, and the user never learns which conversations were skipped. */
|
||||
for (const shard of layout.missingShards) {
|
||||
report.errors.push(`${shard}: listed in the manifest but missing from the archive`);
|
||||
}
|
||||
|
||||
const providerRun: ProviderImportContext = {
|
||||
archive,
|
||||
shards: layout.conversationShards,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue