Mehrere Bücher: Fehlschläge werden erneut versucht (Server-Neustart, kurzer Abbruch), Knopf „erneut versuchen“; Bücher bis 100 MB
All checks were successful
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 9m9s
Test / e2e (push) Successful in 2m31s

Ein Import aus der Cloud lief während eines Updates des Servers; alle Bücher
danach scheiterten in Sekunden. Jetzt bis zu drei weitere Versuche je Buch
(3, 8, 20 s), am Ende ein Knopf für die übrigen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-16 21:52:31 +02:00
parent f3b0080790
commit caeb67e886
5 changed files with 41 additions and 17 deletions

View file

@ -447,7 +447,7 @@ def upload_book(request):
return JsonResponse({'error': 'invalid base64 in data_ct'}, status=400) return JsonResponse({'error': 'invalid base64 in data_ct'}, status=400)
if raw_size > max_bytes: if raw_size > max_bytes:
return JsonResponse({'error': 'file too large (max 50 MB)'}, status=400) return JsonResponse({'error': f'file too large (max {settings.EBOOK_MAX_BYTES // (1024 * 1024)} MB)'}, status=400)
book = EBook.objects.create( book = EBook.objects.create(
user=request.user, user=request.user,

View file

@ -34,10 +34,10 @@ INSTALLED_APPS = [
'feeds', 'feeds',
] ]
EBOOK_MAX_BYTES = 50 * 1024 * 1024 # 50 MB EBOOK_MAX_BYTES = 100 * 1024 * 1024 # 100 MB (big scanned PDFs, complete editions)
# Encrypted uploads are base64-encoded (~33% overhead) so allow ~75 MB body # Encrypted uploads are base64-encoded (~33% overhead) so allow ~140 MB body
DATA_UPLOAD_MAX_MEMORY_SIZE = 75 * 1024 * 1024 DATA_UPLOAD_MAX_MEMORY_SIZE = 140 * 1024 * 1024
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',

View file

@ -137,7 +137,7 @@ const _GERMAN_ERRORS = {
'file must be .m3u or .m3u8': 'Die Datei muss eine .m3u- oder .m3u8-Datei sein.', 'file must be .m3u or .m3u8': 'Die Datei muss eine .m3u- oder .m3u8-Datei sein.',
'no file uploaded': 'Keine Datei hochgeladen.', 'no file uploaded': 'Keine Datei hochgeladen.',
'only jpeg, png, or webp allowed': 'Nur JPEG, PNG oder WebP erlaubt.', 'only jpeg, png, or webp allowed': 'Nur JPEG, PNG oder WebP erlaubt.',
'file too large (max 50 MB)': 'Datei zu groß (höchstens 50 MB).', 'file too large (max 100 MB)': 'Datei zu groß (höchstens 100 MB).',
'file too large (max 5 MB)': 'Datei zu groß (höchstens 5 MB).', 'file too large (max 5 MB)': 'Datei zu groß (höchstens 5 MB).',
'authentication required': 'Bitte anmelden.', 'authentication required': 'Bitte anmelden.',
'not found': 'Nicht gefunden.', 'not found': 'Nicht gefunden.',

View file

@ -589,24 +589,48 @@ function bookFileSelected(input) {
// and author) is left out. statusEl shows how far it is and, at the end, what came of it. // and author) is left out. statusEl shows how far it is and, at the end, what came of it.
async function _importMany(items, statusEl) { async function _importMany(items, statusEl) {
statusEl = statusEl || $('book-upload-status'); statusEl = statusEl || $('book-upload-status');
const done = {ok: 0, dup: 0, failed: []}; const done = {ok: 0, dup: 0, failed: [], big: []};
const retry = [];
const wait = ms => new Promise(r => setTimeout(r, ms));
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const it = items[i]; const it = items[i];
if (statusEl) statusEl.textContent = `Buch ${i + 1} von ${items.length}: „${it.name}“ …`; if (it.size && it.size > DIORA_CONFIG.ebookMaxBytes) { done.big.push(it.name); continue; }
if (it.size && it.size > DIORA_CONFIG.ebookMaxBytes) { done.failed.push(it.name + ' (zu groß)'); continue; } // A failure is tried again, a little later each time: the server may be restarting
// (an update) or the connection gone for a moment — not a reason to lose the rest.
let r = false;
for (let attempt = 0; attempt < 4 && r === false; attempt++) {
if (attempt) {
if (statusEl) statusEl.textContent = `Buch ${i + 1} von ${items.length}: „${it.name}“ – neuer Versuch …`;
await wait([0, 3000, 8000, 20000][attempt]);
}
if (statusEl && !attempt) statusEl.textContent = `Buch ${i + 1} von ${items.length}: „${it.name}“ …`;
try { try {
const r = await _importEbookBuffer(await it.buf(), it.name, null, {reload: false, skipKnown: true}); r = await _importEbookBuffer(await it.buf(), it.name, null, {reload: false, skipKnown: true});
} catch (e) {
r = false;
}
}
if (r === 'dup') done.dup++; if (r === 'dup') done.dup++;
else if (r) done.ok++; else if (r) done.ok++;
else done.failed.push(it.name); else { done.failed.push(it.name); retry.push(it); }
} catch (e) {
done.failed.push(it.name);
}
} }
const mb = Math.round(DIORA_CONFIG.ebookMaxBytes / 1024 / 1024);
const parts = [`✓ ${done.ok} ${done.ok === 1 ? 'Buch' : 'Bücher'} hinzugefügt`]; const parts = [`✓ ${done.ok} ${done.ok === 1 ? 'Buch' : 'Bücher'} hinzugefügt`];
if (done.dup) parts.push(`${done.dup} schon da`); if (done.dup) parts.push(`${done.dup} schon da`);
if (done.big.length) parts.push(`zu groß (über ${mb} MB): ${done.big.join(', ')}`);
if (done.failed.length) parts.push(`nicht geklappt: ${done.failed.join(', ')}`); if (done.failed.length) parts.push(`nicht geklappt: ${done.failed.join(', ')}`);
if (statusEl) statusEl.textContent = parts.join(' · '); if (statusEl) {
statusEl.textContent = parts.join(' · ');
if (retry.length) {
const again = document.createElement('button');
again.type = 'button';
again.className = 'btn btn-sm';
again.style.marginLeft = '8px';
again.textContent = `${retry.length} erneut versuchen`;
again.onclick = () => _importMany(retry, statusEl);
statusEl.appendChild(again);
}
}
if (done.ok) loadBookList(); if (done.ok) loadBookList();
return done; return done;
} }

View file

@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use. * diora service worker — caches the app shell for offline use.
*/ */
const CACHE = 'diora-v122'; const CACHE = 'diora-v123';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',
'/static/js/app/01-core.js', '/static/js/app/01-core.js',