diff --git a/books/views.py b/books/views.py index 8ed0203..701798b 100644 --- a/books/views.py +++ b/books/views.py @@ -447,7 +447,7 @@ def upload_book(request): return JsonResponse({'error': 'invalid base64 in data_ct'}, status=400) 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( user=request.user, diff --git a/diora/settings.py b/diora/settings.py index 5bd586c..88ce3af 100644 --- a/diora/settings.py +++ b/diora/settings.py @@ -34,10 +34,10 @@ INSTALLED_APPS = [ '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 -DATA_UPLOAD_MAX_MEMORY_SIZE = 75 * 1024 * 1024 +# Encrypted uploads are base64-encoded (~33% overhead) so allow ~140 MB body +DATA_UPLOAD_MAX_MEMORY_SIZE = 140 * 1024 * 1024 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', diff --git a/static/js/app/01-core.js b/static/js/app/01-core.js index 4dbbfef..6c15919 100644 --- a/static/js/app/01-core.js +++ b/static/js/app/01-core.js @@ -137,7 +137,7 @@ const _GERMAN_ERRORS = { 'file must be .m3u or .m3u8': 'Die Datei muss eine .m3u- oder .m3u8-Datei sein.', 'no file uploaded': 'Keine Datei hochgeladen.', '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).', 'authentication required': 'Bitte anmelden.', 'not found': 'Nicht gefunden.', diff --git a/static/js/app/52-book-list.js b/static/js/app/52-book-list.js index 38ba91b..7479f32 100644 --- a/static/js/app/52-book-list.js +++ b/static/js/app/52-book-list.js @@ -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. async function _importMany(items, statusEl) { 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++) { 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.failed.push(it.name + ' (zu groß)'); continue; } - try { - const r = await _importEbookBuffer(await it.buf(), it.name, null, {reload: false, skipKnown: true}); - if (r === 'dup') done.dup++; - else if (r) done.ok++; - else done.failed.push(it.name); - } catch (e) { - done.failed.push(it.name); + if (it.size && it.size > DIORA_CONFIG.ebookMaxBytes) { done.big.push(it.name); 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 { + r = await _importEbookBuffer(await it.buf(), it.name, null, {reload: false, skipKnown: true}); + } catch (e) { + r = false; + } } + if (r === 'dup') done.dup++; + else if (r) done.ok++; + else { done.failed.push(it.name); retry.push(it); } } + const mb = Math.round(DIORA_CONFIG.ebookMaxBytes / 1024 / 1024); const parts = [`✓ ${done.ok} ${done.ok === 1 ? 'Buch' : 'Bücher'} hinzugefügt`]; 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 (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(); return done; } diff --git a/static/js/sw.js b/static/js/sw.js index 80ad434..a69672f 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v122'; +const CACHE = 'diora-v123'; const SHELL = [ '/static/css/app.css', '/static/js/app/01-core.js',