diff --git a/download.html b/download.html index b332a7f..eed5355 100644 --- a/download.html +++ b/download.html @@ -1,7 +1,7 @@ - Send to Kobo + Send to Kobo/Kindle @@ -9,11 +9,11 @@
-

Send to Kobo

+

Send to Kobo/Kindle

- Unique key: -
- - - -
+
Unique key:
+
––––

@@ -39,6 +39,9 @@ function xhr(method, url, cb) { x.onload = function () { cb(x) } + x.onerror = function () { + cb(x) + } x.open(method, url, true) x.send(null) } @@ -48,13 +51,19 @@ function pollFile () { var data try { data = JSON.parse(x.responseText) - } catch (err) { } - if (data && data.error) { + } catch (err) { + keyOutput.textContent = '––––' + key = null + downloads.style.display = 'none' + return + } + if (data.error) { if (pollTimer) clearInterval(pollTimer) key = null - keyOutput.textContent = '- - - -' + keyOutput.textContent = '––––' + downloads.style.display = 'none' } - if (data && data.file) { + if (data.file) { downloadlink.textContent = data.file.name downloads.style.display = 'block' } else { @@ -64,17 +73,18 @@ function pollFile () { } function generateKey () { - keyOutput.textContent = '- - - -' + keyOutput.textContent = '––––' if (pollTimer) clearInterval(pollTimer) downloads.style.display = 'none' xhr('POST', '/generate', function (x) { - keyOutput.textContent = x.responseText - if (x.responseText !== 'error') { + if (x.responseText !== 'error' && x.status === 200) { key = x.responseText + keyOutput.textContent = key downloadlink.href = '/download/' + key if (pollTimer) clearInterval(pollTimer) pollTimer = setInterval(pollFile, 5 * 1000) } else { + keyOutput.textContent = '––––' key = null downloadlink.href = '' } diff --git a/index.js b/index.js index 4d88df4..7761a67 100644 --- a/index.js +++ b/index.js @@ -8,13 +8,14 @@ const sendfile = require('koa-sendfile') const mkdirp = require('mkdirp') const fs = require('fs') const { spawn } = require('child_process') +const { extname, basename, dirname } = require('path') const port = 3001 const expireDelay = 30 // 30 seconds const maxExpireDuration = 2 * 60 * 60 // 2 hours const maxFileSize = 1024 * 1024 * 400 // 400 MB -const keyChars = "234689ACEFGHKLMNPRTXYZ" +const keyChars = "3469ACEGHLMNPRTY" const keyLength = 4 function randomKey () { @@ -56,6 +57,11 @@ function expireKey (key) { return timer } +function flash (ctx, data) { + console.log(data) + ctx.cookies.set('flash', encodeURIComponent(JSON.stringify(data)), {overwrite: true, httpOnly: false}) +} + const app = new Koa() app.context.keys = new Map() app.use(logger()) @@ -70,7 +76,7 @@ const upload = multer({ filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.floor(Math.random() * 1E9) console.log(file) - cb(null, file.fieldname + '-' + uniqueSuffix + '.epub') + cb(null, file.fieldname + '-' + uniqueSuffix + extname(file.originalname).toLowerCase()) } }), limits: { @@ -84,8 +90,9 @@ const upload = multer({ cb(null, false) return } - if (!file.originalname.toLowerCase().endsWith('.epub')) { - console.error('FileFilter: Filename does not end with .epub: ' + file.originalname) + const nameLower = file.originalname.toLowerCase() + if (!nameLower.endsWith('.epub') && !nameLower.endsWith('.mobi')) { + console.error('FileFilter: Filename does not end with .epub or .mobi: ' + file.originalname) cb(null, false) return } @@ -95,8 +102,8 @@ const upload = multer({ router.post('/generate', async ctx => { const agent = ctx.get('user-agent') - if (!agent.includes('Kobo')) { - console.error('Non-Kobo device tried to generate a key: ' + agent) + if (!agent.includes('Kobo') && !agent.includes('Kindle')) { + console.error('Non-Kobo or Kindle device tried to generate a key: ' + agent) ctx.throw(403) } let key = null @@ -122,7 +129,9 @@ router.post('/generate', async ctx => { } ctx.keys.set(key, info) expireKey(key) - setTimeout(removeKey, maxExpireDuration * 1000, key) + setTimeout(() => { + if(ctx.keys.has(key)) removeKey(key) + }, maxExpireDuration * 1000) ctx.body = key }) @@ -138,9 +147,8 @@ router.get('/download/:key', async ctx => { return } expireKey(key) - console.log('Sending file!') + console.log('Sending file', info.file.path) await sendfile(ctx, info.file.path) - // ctx.type = 'application/epub+zip' ctx.attachment(info.file.name) }) @@ -148,31 +156,85 @@ router.post('/upload', upload.single('file'), async ctx => { const key = ctx.request.body.key.toUpperCase() if (!ctx.keys.has(key)) { - ctx.throw(400, 'Unknown key: ' + key) + flash(ctx, { + message: 'Unknown key ' + key, + success: false + }) + ctx.redirect('back', '/') + return } if (!ctx.request.file || ctx.request.file.size === 0) { + flash(ctx, { + message: 'Invalid file submitted', + success: false, + key: key + }) + if (ctx.request.file) console.error(ctx.request.file) + ctx.redirect('back', '/') + return + } + const nameLower = ctx.request.file.originalname.toLowerCase() + if (!nameLower.endsWith('.epub') && !nameLower.endsWith('.mobi')) { + flash(ctx, { + message: 'Uploaded file does not end with .epub or .mobi ' + ctx.request.file.originalname, + success: false, + key: key + }) console.error(ctx.request.file) - ctx.throw(400, 'Invalid or no file submitted') - } - if (!ctx.request.file.originalname.toLowerCase().endsWith('.epub')) { - ctx.throw(400, 'Uploaded file does not end with .epub ' + ctx.request.file.originalname) + ctx.redirect('back', '/') + return } + const info = ctx.keys.get(key) + expireKey(key) + let data = null let filename = ctx.request.file.originalname + let conversion = null - if (ctx.request.body.kepubify) { + if (nameLower.endsWith('.epub') && info.agent.includes('Kindle')) { + // convert to .mobi + conversion = 'kindlegen' + const outname = ctx.request.file.path.replace(/\.epub$/i, '.mobi') + filename = filename.replace(/\.kepub\.epub$/i, '.epub').replace(/\.epub$/i, '.mobi') + + data = await new Promise((resolve, reject) => { + const kindlegen = spawn('kindlegen', [basename(ctx.request.file.path), '-dont_append_source', '-c1', '-o', basename(outname)], { + stdio: 'inherit', + cwd: dirname(ctx.request.file.path) + }) + kindlegen.once('close', (code) => { + fs.unlink(ctx.request.file.path, (err) => { + if (err) console.error(err) + else console.log('Remove file', ctx.request.file.path) + }) + fs.unlink(ctx.request.file.path.replace(/\.epub$/i, '.mobi8'), (err) => { + if (err) console.error(err) + else console.log('Removed file', ctx.request.file.path.replace(/\.epub$/i, '.mobi8')) + }) + if (code !== 0) { + console.warn('kindlegen error code ' + code) + } + + resolve(outname) + }) + }) + + } else if (nameLower.endsWith('.epub') && info.agent.includes('Kobo') && ctx.request.body.kepubify) { + // convert to Kobo EPUB + conversion = 'kepubify' const outname = ctx.request.file.path.replace(/\.epub$/i, '.kepub.epub') - filename = filename.replace(/\.kepub\.epub$/i, '.epub').replace(/\.epub$/i, '.kepub.epub') data = await new Promise((resolve, reject) => { - const kepubify = spawn('kepubify', ['-v', '-u', '-o', outname, ctx.request.file.path], { - stdio: 'inherit' + const kepubify = spawn('kepubify', ['-v', '-u', '-o', basename(outname), basename(ctx.request.file.path)], { + stdio: 'inherit', + cwd: dirname(ctx.request.file.path) }) kepubify.once('close', (code) => { fs.unlink(ctx.request.file.path, (err) => { if (err) console.error(err) + else console.log('Remove file', ctx.request.file.path) }) if (code !== 0) { reject('kepubify error code ' + code) @@ -183,14 +245,15 @@ router.post('/upload', upload.single('file'), async ctx => { }) }) } else { + // No conversion data = ctx.request.file.path } expireKey(key) - const info = ctx.keys.get(key) if (info.file && info.file.path) { await new Promise((resolve, reject) => fs.unlink(info.file.path, (err) => { - if (err) reject(err) + if (err) return reject(err) + else console.log('Removed previously uploaded file', info.file.path) resolve() })) } @@ -201,6 +264,11 @@ router.post('/upload', upload.single('file'), async ctx => { uploaded: new Date() } console.log(info.file) + flash(ctx, { + message: 'Upload successful!
'+(conversion ? ' Ebook was converted with ' + conversion + ' and sent' : ' Sent')+' to a '+(info.agent.includes('Kobo') ? 'Kobo' : 'Kindle')+' device.
Filename: ' + filename, + success: true, + key: key + }) ctx.redirect('back', '/') }) @@ -243,7 +311,7 @@ router.get('/style.css', async ctx => { router.get('/', async ctx => { const agent = ctx.get('user-agent') console.log(agent) - await sendfile(ctx, agent.includes('Kobo') ? 'download.html' : 'upload.html') + await sendfile(ctx, agent.includes('Kobo') || agent.includes('Kindle')? 'download.html' : 'upload.html') }) diff --git a/style.css b/style.css index fb86358..c3a8a30 100644 --- a/style.css +++ b/style.css @@ -1,19 +1,27 @@ +body { + line-height: 1.3; + font-family: serif; +} + .wrapper { margin: 0 auto; padding: 0 20px; max-width: 650px; } h1 { + font-size: 2em; font-weight: normal; font-style: italic; } #key { - font-size: 4.5em; + font-size: 5em; display: block; - letter-spacing: 0.1em; - margin: 5px 0; - font-family: monospace; + letter-spacing: 0.2em; + margin: 10px 0; + font-family: sans-serif; + white-space: nowrap; + text-transform: uppercase; } .center { text-align: center; @@ -29,6 +37,8 @@ h1 { padding: 0.6em 1.3em; line-height: 1.6; display: inline-block; + font-family: inherit; + font-size: 1.2em; } #keygen:focus { background: black; @@ -47,4 +57,32 @@ h1 { text-align: center; font-family: monospace; letter-spacing: 0.1em; +} + +#uploadstatus { + opacity: 0; + transition: opacity .5s ease-in-out; + padding: 10px; + margin-top: 10px; + border-radius: 5px; + text-align: center; + cursor: pointer; + line-height: 1.7; +} + +#uploadstatus.success { + background-color: #DFD; + border: 1px solid #7F7; +} + +#uploadstatus.error { + background-color: #FDD; + border: 1px solid #F77; +} +td.right { + padding: 10px; +} +#fileinfo { + font-size: 0.9em; + font-style: italic; } \ No newline at end of file diff --git a/upload.html b/upload.html index 22adf19..5d54085 100644 --- a/upload.html +++ b/upload.html @@ -1,7 +1,7 @@ - Send to Kobo + Send to Kobo/Kindle @@ -9,27 +9,84 @@
-

Send to Kobo

+

Send to Kobo/Kindle

- - - +
Unique key
EPUB file
+ + + - +
+
-
-

Go this this page on your Kobo ereader and you see a unique key. Enter it in this form and upload an ebook and it will appear as a download link on the ereader.

-

Your ebook will be stored on the server as long as the Kobo is viewing the unique key and is connected to wifi. It will be deleted irrevocably when the key expires about 30 seconds after you close the browser, generate a new key or disable wifi on your Kobo.

+

Go this this page on your Kobo/Kindle ereader and you see a unique key. Enter it in this form and upload an ebook and it will appear as a download link on the ereader.

+

If you send an EPUB to to a Kindle it will be converted to MOBI with KindleGen. If you send a MOBI file it will be sent unprocessed. If you send an EPUB and check the Kepubify option it will be converted into a Kobo EPUB using Kepubify.

+

Your ebook will be stored on the server as long as your Kobo/Kindle is viewing the unique key and is connected to wifi. It will be deleted irrevocably when the key expires about 30 seconds after you close the browser, generate a new key or disable wifi on your ereader.

By using this tool you agree that the ebook you upload is processed on the server and stored for a short time.


- Created by djazz. Powered by Koa and Kepubify | Source code on Github + Created by djazz. Powered by Koa, Kepubify and KindleGen
Source code on Github
+