Books: repair flow also re-encrypts metadata from file
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 14s

replace-data endpoint now optionally accepts meta_ct/meta_iv.
The client extracts title/author from the uploaded file (same as
normal upload), re-encrypts metadata with the current key, and sends
both data and meta together — so broken books are fully restored
including their displayed title and author.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-05-25 17:06:52 +02:00
parent 4b47f6e67a
commit fe91000e7c
2 changed files with 44 additions and 5 deletions

View file

@ -111,15 +111,22 @@ def replace_book_data(request, pk):
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
return JsonResponse({'error': 'invalid JSON'}, status=400) return JsonResponse({'error': 'invalid JSON'}, status=400)
data_ct = body.get('data_ct', '') data_ct = body.get('data_ct', '')
data_iv = body.get('data_iv', '') data_iv = body.get('data_iv', '')
meta_ct = body.get('meta_ct', '')
meta_iv = body.get('meta_iv', '')
if not all([data_ct, data_iv]): if not all([data_ct, data_iv]):
return JsonResponse({'error': 'data_ct and data_iv required'}, status=400) return JsonResponse({'error': 'data_ct and data_iv required'}, status=400)
update_fields = ['data_ct', 'data_iv']
book.data_ct = data_ct book.data_ct = data_ct
book.data_iv = data_iv book.data_iv = data_iv
book.save(update_fields=['data_ct', 'data_iv']) if meta_ct and meta_iv:
book.meta_ct = meta_ct
book.meta_iv = meta_iv
update_fields += ['meta_ct', 'meta_iv']
book.save(update_fields=update_fields)
return JsonResponse({'ok': True}) return JsonResponse({'ok': True})

View file

@ -2879,11 +2879,43 @@ function repairBook(bookId) {
try { try {
const key = await getOrCreateEncKey(); const key = await getOrCreateEncKey();
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const enc = await encryptBytes(key, buf); const isPdf = /\.pdf$/i.test(file.name);
const type = isPdf ? 'pdf' : 'epub';
let title = file.name.replace(/\.(epub|pdf)$/i, ''), author = '';
try {
if (isPdf) {
const pdfDoc = await pdfjsLib.getDocument({data: new Uint8Array(buf.slice(0))}).promise;
const meta = await pdfDoc.getMetadata();
title = meta.info?.Title?.trim() || title;
author = meta.info?.Author?.trim() || '';
} else {
const zip = await JSZip.loadAsync(buf.slice(0));
const containerXml = await zip.file('META-INF/container.xml').async('text');
const opfPath = new DOMParser()
.parseFromString(containerXml, 'application/xml')
.querySelector('rootfile')?.getAttribute('full-path');
if (opfPath) {
const opfDoc = new DOMParser().parseFromString(
await zip.file(opfPath).async('text'), 'application/xml');
title = opfDoc.querySelector('metadata > title, metadata > *|title')?.textContent?.trim() || title;
author = opfDoc.querySelector('metadata > creator, metadata > *|creator')?.textContent?.trim() || '';
}
}
} catch (e) { /* keep filename as title */ }
const metaJson = new TextEncoder().encode(JSON.stringify({title, author, filename: file.name, type}));
const [metaEnc, dataEnc] = await Promise.all([
encryptBytes(key, metaJson),
encryptBytes(key, buf),
]);
const res = await fetch(`/books/${bookId}/replace-data/`, { const res = await fetch(`/books/${bookId}/replace-data/`, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
body: JSON.stringify({data_ct: enc.ciphertext, data_iv: enc.iv}), body: JSON.stringify({
data_ct: dataEnc.ciphertext, data_iv: dataEnc.iv,
meta_ct: metaEnc.ciphertext, meta_iv: metaEnc.iv,
}),
}); });
if (!(await res.json()).ok) throw new Error('Server error'); if (!(await res.json()).ok) throw new Error('Server error');
await _evictCachedBook(bookId); await _evictCachedBook(bookId);