Mehrere Bücher: Fehlschläge werden erneut versucht (Server-Neustart, kurzer Abbruch), Knopf „erneut versuchen“; Bücher bis 100 MB
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:
parent
f3b0080790
commit
caeb67e886
5 changed files with 41 additions and 17 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
try {
|
// (an update) or the connection gone for a moment — not a reason to lose the rest.
|
||||||
const r = await _importEbookBuffer(await it.buf(), it.name, null, {reload: false, skipKnown: true});
|
let r = false;
|
||||||
if (r === 'dup') done.dup++;
|
for (let attempt = 0; attempt < 4 && r === false; attempt++) {
|
||||||
else if (r) done.ok++;
|
if (attempt) {
|
||||||
else done.failed.push(it.name);
|
if (statusEl) statusEl.textContent = `Buch ${i + 1} von ${items.length}: „${it.name}“ – neuer Versuch …`;
|
||||||
} catch (e) {
|
await wait([0, 3000, 8000, 20000][attempt]);
|
||||||
done.failed.push(it.name);
|
}
|
||||||
|
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`];
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue