Books: read DRM-free MOBI, AZW and AZW3; refuse Kindle files with DRM
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
7 files changed
+138
-21
No files matched your search
@@ -4,7 +4,7 @@ const fs = require('fs'), path = require('path'), crypto = require('crypto');
|
||||
const users = require('./users');
|
||||
|
||||
const MAX_BOOK = 300 * 1024 * 1024, MAX_COVER = 3 * 1024 * 1024, MAX_JSON = 2e6;
|
||||
const TYPES = { epub: 'application/epub+zip', pdf: 'application/pdf', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' };
|
||||
const TYPES = { epub: 'application/epub+zip', pdf: 'application/pdf', mobi: 'application/x-mobipocket-ebook', azw: 'application/vnd.amazon.ebook', azw3: 'application/vnd.amazon.mobi8-ebook', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' };
|
||||
const COVERS = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp' };
|
||||
|
||||
const dir = () => { const d = path.join(users.dir(), 'books'); fs.mkdirSync(d, { recursive: true, mode: 0o700 }); return d; };
|
||||
@@ -23,9 +23,13 @@ async function body(req, max) {
|
||||
for await (const c of req) { n += c.length; if (n > max) return null; chunks.push(c); }
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
const MOBI = new Set(['mobi', 'azw', 'azw3']);
|
||||
/** Kindle files with DRM have a non-zero encryption type in their first record (the PalmDOC header, 12 bytes in). Those cannot be read and are refused. */
|
||||
function mobiLocked(buf) { try { const rec0 = buf.readUInt32BE(78); return buf.readUInt16BE(rec0 + 12) !== 0; } catch { return true; } }
|
||||
/** A file must look like what its name says: PDFs start with %PDF, EPUB and DOCX are zip archives (and an EPUB says so in its first entry). */
|
||||
function looksLike(ext, buf) {
|
||||
if (ext === 'pdf') return buf.subarray(0, 1024).includes('%PDF-');
|
||||
if (MOBI.has(ext)) return buf.length > 100 && buf.toString('latin1', 60, 68) === 'BOOKMOBI';
|
||||
if (buf.length < 4 || buf[0] !== 0x50 || buf[1] !== 0x4b) return false;
|
||||
return ext === 'epub' ? buf.subarray(0, 200).includes('mimetypeapplication/epub+zip') || buf.includes('META-INF/container.xml') : buf.includes('word/document.xml');
|
||||
}
|
||||
@@ -51,9 +55,10 @@ async function handle(req, res) {
|
||||
|
||||
if (p === '/upload' && req.method === 'POST') {
|
||||
const name = clip(u.searchParams.get('name'), 200), ext = path.extname(name).slice(1).toLowerCase();
|
||||
if (!TYPES[ext]) return json(res, 400, { error: 'Only EPUB, PDF and DOCX books can be added' });
|
||||
if (!TYPES[ext]) return json(res, 400, { error: 'Only EPUB, PDF, DOCX, MOBI, AZW and AZW3 books can be added' });
|
||||
const buf = await body(req, MAX_BOOK); if (!buf) return json(res, 413, { error: 'That file is larger than 300 MB' });
|
||||
if (!looksLike(ext, buf)) return json(res, 400, { error: 'That does not look like a real ' + ext.toUpperCase() + ' file' });
|
||||
if (MOBI.has(ext) && mobiLocked(buf)) return json(res, 400, { error: 'This book has DRM (copy protection), which NE Hub cannot open. Only DRM-free books work.' });
|
||||
const hash = crypto.createHash('sha1').update(buf).digest('hex'), list = index(), dup = list.find(b => b.hash === hash);
|
||||
if (dup) return json(res, 200, { ...dup, duplicate: true });
|
||||
const id = crypto.randomBytes(8).toString('hex');
|
||||
|
||||
Reference in new issue
Block a user