Merge branch 'master' into master

This commit is contained in:
djazz
2024-07-21 17:20:49 +02:00
committed by GitHub
8 changed files with 1438 additions and 310 deletions

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine FROM node:22-alpine
# Create app directory # Create app directory
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -16,6 +16,12 @@ RUN wget https://web.archive.org/web/20150803131026if_/https://kindlegen.s3.amaz
chmod +x /usr/local/bin/kindlegen && \ chmod +x /usr/local/bin/kindlegen && \
rm -rf kindlegen rm -rf kindlegen
RUN apk add --no-cache pipx
ENV PATH="$PATH:/root/.local/bin"
RUN pipx install pdfCropMargins
# Copy files needed by npm install # Copy files needed by npm install
COPY package*.json ./ COPY package*.json ./

240
index.js
View File

@@ -1,18 +1,20 @@
#!/usr/bin/env node #!/usr/bin/env node
const http = require('http')
const Koa = require('koa') const Koa = require('koa')
const Router = require('@koa/router') const Router = require('@koa/router')
const multer = require('@koa/multer') const multer = require('@koa/multer')
const logger = require('koa-logger') const logger = require('koa-logger')
const sendfile = require('koa-sendfile') const sendfile = require('koa-sendfile')
const serve = require('koa-static') const serve = require('koa-static')
const mount = require('koa-mount')
const { mkdirp } = require('mkdirp') const { mkdirp } = require('mkdirp')
const fs = require('fs') const fs = require('fs')
const { spawn } = require('child_process') const { spawn } = require('child_process')
const { join, extname, basename, dirname } = require('path') const { join, extname, basename, dirname } = require('path')
const resolvepath = require('path').resolve
const FileType = require('file-type') const FileType = require('file-type')
const { transliterate } = require('transliteration') const { transliterate } = require('transliteration')
const sanitize = require('sanitize-filename')
const port = 3001 const port = 3001
const expireDelay = 30 // 30 seconds const expireDelay = 30 // 30 seconds
@@ -78,7 +80,12 @@ function expireKey (key) {
function flash (ctx, data) { function flash (ctx, data) {
console.log(data) console.log(data)
ctx.cookies.set('flash', encodeURIComponent(JSON.stringify(data)), {overwrite: true, httpOnly: false, sameSite: 'strict', maxAge: 10 * 1000}) //ctx.cookies.set('flash', encodeURIComponent(JSON.stringify(data)), {overwrite: true, httpOnly: false, sameSite: 'strict', maxAge: 10 * 1000})
ctx.response.status = data.success ? 200 : 400
if (!data.success) {
ctx.set("Connection", "close")
}
ctx.body = data.message
} }
const app = new Koa() const app = new Koa()
@@ -104,18 +111,18 @@ const upload = multer({
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
// Fixes charset // Fixes charset
// https://github.com/expressjs/multer/issues/1104#issuecomment-1152987772 // https://github.com/expressjs/multer/issues/1104#issuecomment-1152987772
file.originalname = doTransliterate(Buffer.from(file.originalname, 'latin1').toString('utf8')) file.originalname = sanitize(Buffer.from(file.originalname, 'latin1').toString('utf8'))
console.log('Incoming file:', file) console.log('Incoming file:', file)
const key = req.body.key.toUpperCase() const key = req.body.key.toUpperCase()
if (!app.context.keys.has(key)) { if (!app.context.keys.has(key)) {
console.error('FileFilter: Unknown key: ' + key) console.error('FileFilter: Unknown key: ' + key)
cb(null, false) cb("Unknown key " + key, false)
return return
} }
if ((!allowedTypes.includes(file.mimetype) && file.mimetype != "application/octet-stream") || !allowedExtensions.includes(extname(file.originalname.toLowerCase()).substring(1))) { if ((!allowedTypes.includes(file.mimetype) && file.mimetype != "application/octet-stream") || !allowedExtensions.includes(extname(file.originalname.toLowerCase()).substring(1))) {
console.error('FileFilter: File is of an invalid type ', file) console.error('FileFilter: File is of an invalid type ', file)
cb(null, false) cb("Invalid filetype: " + JSON.stringify(file), false)
return return
} }
cb(null, true) cb(null, true)
@@ -154,13 +161,39 @@ router.post('/generate', async ctx => {
if(ctx.keys.get(key) === info) removeKey(key) if(ctx.keys.get(key) === info) removeKey(key)
}, maxExpireDuration * 1000) }, maxExpireDuration * 1000)
ctx.cookies.set('key', key, {overwrite: true, httpOnly: false, sameSite: 'strict', maxAge: expireDelay * 1000})
ctx.body = key ctx.body = key
}) })
router.get('/download/:key', async ctx => { router.get('/download/:key', async ctx => {
const key = ctx.params.key.toUpperCase() const key = ctx.cookies.get('key')
if (!key) {
await next()
return
}
const info = ctx.keys.get(key) const info = ctx.keys.get(key)
if (!info || !info.file) { if (!info || !info.file) {
await next()
return
}
ctx.redirect('/' + encodeURIComponent(info.file.name));
})
async function downloadFile (ctx, next) {
const key = ctx.cookies.get('key')
if (!key) {
await next()
return
}
const filename = decodeURIComponent(ctx.params.filename)
const info = ctx.keys.get(key)
if (!info || !info.file || info.file.name !== filename) {
await next()
return return
} }
if (info.agent !== ctx.get('user-agent')) { if (info.agent !== ctx.get('user-agent')) {
@@ -168,20 +201,31 @@ router.get('/download/:key', async ctx => {
return return
} }
expireKey(key) expireKey(key)
// const fallback = basename(info.file.path) console.log('Sending file', [info.file.path, info.file.name])
const sanename = info.file.name.replace(/[^\.\w\-"'\(\)]/g, '_')
console.log('Sending file', [info.file.path, info.file.name, sanename])
await sendfile(ctx, info.file.path)
if (info.agent.includes('Kindle')) { if (info.agent.includes('Kindle')) {
// Kindle needs a safe name or it thinks it's an invalid file // Kindle needs a safe name or it thinks it's an invalid file
ctx.attachment(sanename) ctx.attachment(info.file.name)
} else {
// Kobo always uses fallback
ctx.attachment(info.file.name, {fallback: sanename})
} }
}) await sendfile(ctx, info.file.path)
}
router.post('/upload', async (ctx, next) => {
try {
await upload.single('file')(ctx, () => {})
} catch (err) {
flash(ctx, {
message: err,
success: false
})
// ctx.throw(400, err)
// ctx.res.end(err)
await next()
return
}
ctx.res.writeContinue()
router.post('/upload', upload.single('file'), async ctx => {
const key = ctx.request.body.key.toUpperCase() const key = ctx.request.body.key.toUpperCase()
if (ctx.request.file) { if (ctx.request.file) {
@@ -193,13 +237,13 @@ router.post('/upload', upload.single('file'), async ctx => {
message: 'Unknown key ' + key, message: 'Unknown key ' + key,
success: false success: false
}) })
ctx.redirect('back', '/')
if (ctx.request.file) { if (ctx.request.file) {
fs.unlink(ctx.request.file.path, (err) => { fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err) if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path) else console.log('Removed file', ctx.request.file.path)
}) })
} }
await next()
return return
} }
@@ -219,16 +263,17 @@ router.post('/upload', upload.single('file'), async ctx => {
if (ctx.request.file) { if (ctx.request.file) {
if (ctx.request.file.size === 0) { if (ctx.request.file.size === 0) {
flash(ctx, { let data = {
message: 'Invalid file submitted', message: 'Invalid file submitted (empty file)',
success: false, success: false,
key: key key: key
}) }
ctx.redirect('back', '/') flash(ctx, data)
fs.unlink(ctx.request.file.path, (err) => { fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err) if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path) else console.log('Removed file', ctx.request.file.path)
}) })
await next()
return return
} }
@@ -250,28 +295,46 @@ router.post('/upload', upload.single('file'), async ctx => {
success: false, success: false,
key: key key: key
}) })
ctx.redirect('back', '/')
fs.unlink(ctx.request.file.path, (err) => { fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err) if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path) else console.log('Removed file', ctx.request.file.path)
}) })
await next()
return return
} }
let data = null let data = null
filename = ctx.request.file.originalname filename = ctx.request.file.originalname
if (ctx.request.body.transliteration) {
filename = sanitize(doTransliterate(filename))
}
if (info.agent.includes('Kindle')) {
filename = filename.replace(/[^\.\w\-"'\(\)]/g, '_')
}
if (mimetype === TYPE_EPUB && info.agent.includes('Kindle')) { if (mimetype === TYPE_EPUB && info.agent.includes('Kindle') && ctx.request.body.kindlegen) {
// convert to .mobi // convert to .mobi
conversion = 'kindlegen' conversion = 'kindlegen'
const outname = ctx.request.file.path.replace(/\.epub$/i, '.mobi') const outname = ctx.request.file.path.replace(/\.epub$/i, '.mobi')
filename = filename.replace(/\.kepub\.epub$/i, '.epub').replace(/\.epub$/i, '.mobi') filename = filename.replace(/\.kepub\.epub$/i, '.epub').replace(/\.epub$/i, '.mobi')
let stderr = ''
data = await new Promise((resolve, reject) => { let p = new Promise((resolve, reject) => {
const kindlegen = spawn('kindlegen', [basename(ctx.request.file.path), '-dont_append_source', '-c1', '-o', basename(outname)], { const kindlegen = spawn('kindlegen', [basename(ctx.request.file.path), '-dont_append_source', '-c1', '-o', basename(outname)], {
stdio: 'inherit', // stdio: 'inherit',
cwd: dirname(ctx.request.file.path) cwd: dirname(ctx.request.file.path)
}) })
kindlegen.once('error', function (err) {
fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err)
else console.log('Removed 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'))
})
reject('kindlegen error: ' + err)
})
kindlegen.once('close', (code) => { kindlegen.once('close', (code) => {
fs.unlink(ctx.request.file.path, (err) => { fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err) if (err) console.error(err)
@@ -281,64 +344,134 @@ router.post('/upload', upload.single('file'), async ctx => {
if (err) console.error(err) if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path.replace(/\.epub$/i, '.mobi8')) else console.log('Removed file', ctx.request.file.path.replace(/\.epub$/i, '.mobi8'))
}) })
if (code !== 0) { if (code !== 0 && code !== 1) {
console.warn('kindlegen error code ' + code) reject('kindlegen error code: ' + code + '\n' + stderr)
return
} }
resolve(outname) resolve(outname)
}) })
kindlegen.stdout.on('data', function (str) {
stderr += str
console.log('kindlegen: ' + str)
})
kindlegen.stderr.on('data', function (str) {
stderr += str
console.log('kindlegen: ' + str)
})
}) })
try {
data = await p
} catch (err) {
flash(ctx, {
success: false,
message: err.replaceAll(basename(ctx.request.file.path), "infile.epub").replaceAll(basename(outname), "outfile.mobi")
})
return
}
} else if (mimetype === TYPE_EPUB && info.agent.includes('Kobo') && ctx.request.body.kepubify) { } else if (mimetype === TYPE_EPUB && (info.agent.includes('Kobo') || info.agent.toLowerCase().includes('tolino')) && ctx.request.body.kepubify) {
// convert to Kobo EPUB // convert to Kobo EPUB
conversion = 'kepubify' conversion = 'kepubify'
const outname = ctx.request.file.path.replace(/\.epub$/i, '.kepub.epub') const outname = ctx.request.file.path.replace(/\.epub$/i, '.kepub.epub')
filename = filename.replace(/\.kepub\.epub$/i, '.epub').replace(/\.epub$/i, '.kepub.epub') filename = filename.replace(/\.kepub\.epub$/i, '.epub').replace(/\.epub$/i, '.kepub.epub')
data = await new Promise((resolve, reject) => { let p = new Promise((resolve, reject) => {
let stderr = ''
const kepubify = spawn('kepubify', ['-v', '-u', '-o', basename(outname), basename(ctx.request.file.path)], { const kepubify = spawn('kepubify', ['-v', '-u', '-o', basename(outname), basename(ctx.request.file.path)], {
stdio: 'inherit', //stdio: 'inherit',
cwd: dirname(ctx.request.file.path) cwd: dirname(ctx.request.file.path)
}) })
kepubify.once('error', function (err) {
fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path)
})
reject('kepubify error: ' + err)
})
kepubify.once('close', (code) => { kepubify.once('close', (code) => {
fs.unlink(ctx.request.file.path, (err) => { fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err) if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path) else console.log('Removed file', ctx.request.file.path)
}) })
if (code !== 0) { if (code !== 0) {
reject('kepubify error code ' + code) reject('Kepubify error code: ' + code + '\n' + stderr)
return return
} }
resolve(outname) resolve(outname)
}) })
kepubify.stdout.on('data', function (str) {
stderr += str
console.log('kepubify: ' + str)
})
kepubify.stderr.on('data', function (str) {
stderr += str
console.log('kepubify: ' + str)
})
}) })
} else if (mimetype === 'application/pdf' && ctx.request.body.pdfCropMargins) { try {
data = await p
} catch (err) {
flash(ctx, {
success: false,
message: err.replaceAll(basename(ctx.request.file.path), "infile.epub").replaceAll(basename(outname), "outfile.kepub.epub")
})
return
}
data = await new Promise((resolve, reject) => { } else if (mimetype == 'application/pdf' && ctx.request.body.pdfcropmargins) {
const pdfcropmargins = spawn('pdfcropmargins', ['-s', '-u', basename(ctx.request.file.path)], { const dir = dirname(ctx.request.file.path)
stdio: 'inherit', const base = basename(ctx.request.file.path, '.pdf')
const outfile = resolvepath(join(dir, `${base}_cropped.pdf`))
let p = new Promise((resolve, reject) => {
let stderr = ''
const pdfcropmargins = spawn('pdfcropmargins', ['-s', '-u', '-o', outfile, basename(ctx.request.file.path)], {
// stdio: 'inherit',
cwd: dirname(ctx.request.file.path) cwd: dirname(ctx.request.file.path)
}) })
pdfcropmargins.once('error', function (err) {
fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path)
})
reject('pdfcropmargins error: ' + err)
})
pdfcropmargins.once('close', (code) => { pdfcropmargins.once('close', (code) => {
fs.unlink(ctx.request.file.path, (err) => { fs.unlink(ctx.request.file.path, (err) => {
if (err) console.error(err) if (err) console.error(err)
else console.log('Removed file', ctx.request.file.path) else console.log('Removed file', ctx.request.file.path)
}) })
if (code !== 0) { if (code !== 0) {
reject('pdfcropmargins error code ' + code) reject('pdfcropmargins error code: ' + code + '\n' + stderr)
return return
} }
const dir = dirname(ctx.request.file.path);
const base = basename(ctx.request.file.path, '.pdf'); resolve(outfile)
})
resolve(join(dir, `${base}_cropped.pdf`)) pdfcropmargins.stdout.on('data', function (str) {
stderr += str
console.log('pdfcropmargins: ' + str)
})
pdfcropmargins.stderr.on('data', function (str) {
stderr += str
console.log('pdfcropmargins: ' + str)
}) })
}) })
try {
data = await p
} catch (err) {
flash(ctx, {
success: false,
message: err.replaceAll(basename(ctx.request.file.path), "infile.pdf").replaceAll(outfile, "outfile.pdf")
})
return
}
} else { } else {
// No conversion // No conversion
data = ctx.request.file.path data = ctx.request.file.path
filename = filename.replace(/\.epub$/i, '.epub').replace(/\.pdf$/i, '.pdf')
} }
expireKey(key) expireKey(key)
@@ -359,7 +492,8 @@ router.post('/upload', upload.single('file'), async ctx => {
let messages = [] let messages = []
if (ctx.request.file) { if (ctx.request.file) {
messages.push('Upload successful! ' + (conversion ? ' Ebook was converted with ' + conversion + ' and sent' : ' Sent')+' to '+(info.agent.includes('Kobo') ? 'a Kobo device.' : (info.agent.includes('Kindle') ? 'a Kindle device.' : 'a device.'))) ctx.request.file.skip = true
messages.push('Upload successful! ' + (conversion ? 'Ebook was converted with ' + conversion + ' and sent' : 'Sent')+' to '+(info.agent.includes('Kobo') ? 'a Kobo device.' : (info.agent.includes('Kindle') ? 'a Kindle device.' : 'a device.')))
messages.push('Filename: ' + filename) messages.push('Filename: ' + filename)
} }
if (url) { if (url) {
@@ -372,7 +506,7 @@ router.post('/upload', upload.single('file'), async ctx => {
success: false, success: false,
key: key key: key
}) })
ctx.redirect('back', '/') await next()
return return
} }
@@ -382,7 +516,8 @@ router.post('/upload', upload.single('file'), async ctx => {
key: key, key: key,
url: url url: url
}) })
ctx.redirect('back', '/')
await next()
}) })
router.delete('/file/:key', async ctx => { router.delete('/file/:key', async ctx => {
@@ -399,6 +534,7 @@ router.get('/status/:key', async ctx => {
const key = ctx.params.key.toUpperCase() const key = ctx.params.key.toUpperCase()
const info = ctx.keys.get(key) const info = ctx.keys.get(key)
if (!info) { if (!info) {
ctx.response.status = 404
ctx.body = {error: 'Unknown key'} ctx.body = {error: 'Unknown key'}
return return
} }
@@ -408,6 +544,7 @@ router.get('/status/:key', async ctx => {
return return
} }
expireKey(key) expireKey(key)
ctx.cookies.set('key', key, {overwrite: true, httpOnly: false, sameSite: 'strict', maxAge: expireDelay * 1000})
ctx.body = { ctx.body = {
alive: info.alive, alive: info.alive,
file: info.file ? { file: info.file ? {
@@ -425,19 +562,28 @@ router.get('/receive', async ctx => {
router.get('/', async ctx => { router.get('/', async ctx => {
const agent = ctx.get('user-agent') const agent = ctx.get('user-agent')
console.log(ctx.ip, agent) console.log(ctx.ip, agent)
await sendfile(ctx, agent.includes('Kobo') || agent.includes('Kindle')? 'static/download.html' : 'static/upload.html') await sendfile(ctx, agent.includes('Kobo') || agent.includes('Kindle') || agent.toLowerCase().includes('tolino') ? 'static/download.html' : 'static/upload.html')
}) })
router.get('/:filename', downloadFile)
app.use(serve("static"))
app.use(router.routes()) app.use(router.routes())
app.use(router.allowedMethods()) app.use(router.allowedMethods())
app.use(serve("static"))
fs.rm('uploads', {recursive: true}, (err) => { fs.rm('uploads', {recursive: true}, (err) => {
if (err) throw err if (err) throw err
mkdirp('uploads').then (() => { mkdirp('uploads').then (() => {
app.listen(port) // app.listen(port)
const fn = app.callback()
const server = http.createServer(fn)
server.on('checkContinue', (req, res) => {
console.log("check continue!")
fn(req, res)
})
server.listen(port)
console.log('server is listening on port ' + port) console.log('server is listening on port ' + port)
}) })
}) })

1355
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,8 @@
"description": "Send ebooks to Kobo/Kindle ereaders easily", "description": "Send ebooks to Kobo/Kindle ereaders easily",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node index" "start": "node index",
"postinstall": "patch-package"
}, },
"author": "djazz", "author": "djazz",
"license": "MIT", "license": "MIT",
@@ -12,13 +13,14 @@
"@koa/multer": "^3.0.2", "@koa/multer": "^3.0.2",
"@koa/router": "^12.0.1", "@koa/router": "^12.0.1",
"file-type": "^16.5.4", "file-type": "^16.5.4",
"koa": "^2.15.0", "koa": "^2.15.3",
"koa-logger": "^3.2.1", "koa-logger": "^3.2.1",
"koa-mount": "^4.0.0",
"koa-sendfile": "^3.0.0", "koa-sendfile": "^3.0.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mkdirp": "^3.0.1", "mkdirp": "^3.0.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"patch-package": "^8.0.0",
"sanitize-filename": "^1.6.3",
"transliteration": "^2.3.5" "transliteration": "^2.3.5"
} }
} }

View File

@@ -0,0 +1,15 @@
diff --git a/node_modules/@koa/multer/index.js b/node_modules/@koa/multer/index.js
index d5be076..252208e 100644
--- a/node_modules/@koa/multer/index.js
+++ b/node_modules/@koa/multer/index.js
@@ -11,9 +11,7 @@
* Module dependencies.
*/
-let originalMulter = require('fix-esm').require('multer');
-
-if (originalMulter.default) originalMulter = originalMulter.default;
+let originalMulter = require('multer');
function multer(options) {
const m = originalMulter(options);

View File

@@ -78,12 +78,10 @@ function pollFile () {
downloads.style.display = 'none' downloads.style.display = 'none'
} }
var urls = data.urls && data.urls.length > 0 ? data.urls : null var urls = data.urls && data.urls.length > 0 ? data.urls : null
if (data.file || urls) {
} else {
}
if (data.file) { if (data.file) {
downloads.style.display = 'block' downloads.style.display = 'block'
downloadlink.textContent = data.file.name downloadlink.textContent = data.file.name
downloadlink.href = '/' + encodeURIComponent(data.file.name)
} else { } else {
downloads.style.display = 'none' downloads.style.display = 'none'
} }
@@ -111,7 +109,7 @@ function generateKey () {
if (x.responseText !== 'error' && x.status === 200) { if (x.responseText !== 'error' && x.status === 200) {
key = x.responseText key = x.responseText
keyOutput.textContent = key keyOutput.textContent = key
downloadlink.href = './download/' + key downloadlink.href = ''
if (pollTimer) clearInterval(pollTimer) if (pollTimer) clearInterval(pollTimer)
pollTimer = setInterval(pollFile, 5 * 1000) pollTimer = setInterval(pollFile, 5 * 1000)
} else { } else {

View File

@@ -104,6 +104,7 @@ input[type="url"], input[type="text"] {
#uploadstatus.error { #uploadstatus.error {
background-color: #FDD; background-color: #FDD;
border: 1px solid #F77; border: 1px solid #F77;
white-space: pre;
} }
td { td {
padding: 10px; padding: 10px;

View File

@@ -11,27 +11,28 @@
<div class="wrapper"> <div class="wrapper">
<h1 class="center">Send to Kobo/Kindle</h1> <h1 class="center">Send to Kobo/Kindle</h1>
<form action="./upload" method="post" enctype="multipart/form-data" id="uploadform">
<form action="./upload" method="post" enctype="multipart/form-data">
<table style="margin: 0 auto;" cellpadding=0 cellspacing=0> <table style="margin: 0 auto;" cellpadding=0 cellspacing=0>
<tr><td class="right"><label for="keyinput"><strong>Unique key</strong></label></td><td><input type="text" name="key" id="keyinput" autocomplete="off" pattern="...." placeholder="" required style="text-transform: uppercase;" maxlength=4/></td></tr> <tr><td class="right"><label for="keyinput"><strong>Unique key</strong></label></td><td><input type="text" name="key" id="keyinput" autocomplete="off" pattern="...." placeholder="" required style="text-transform: uppercase;" maxlength=4/></td></tr>
<tr><td class="right aligntop"><label for="fileinput"><strong>Ebook file</strong></label><br/><em>EPUB, MOBI, PDF,<br/>TXT, CBZ, CBR</em></td><td class="aligntop"><label for="fileinput" id="choosebtn">Choose file</label><input type="file" name="file" id="fileinput" accept=".txt,.epub,.mobi,.pdf,.cbz,.cbr,application/epub+zip,application/epub,application/x-mobipocket-ebook,application/pdf,application/vnd.comicbook+zip,application/vnd.comicbook-rar"/><br/><br/><div id="fileinfo"></div></td></tr> <tr><td class="right aligntop"><label for="fileinput"><strong>Ebook file</strong></label><br/><em>EPUB, MOBI, PDF,<br/>TXT, CBZ, CBR</em></td><td class="aligntop"><label for="fileinput" id="choosebtn">Choose file</label><input type="file" name="file" id="fileinput" accept=".txt,.epub,.mobi,.pdf,.cbz,.cbr,application/epub+zip,application/epub,application/x-mobipocket-ebook,application/pdf,application/vnd.comicbook+zip,application/vnd.comicbook-rar"/><br/><br/><div id="fileinfo"></div></td></tr>
<tr><td class="right"><label for="urlinput"><strong>Send url</strong></label></td><td><input type="url" name="url" id="urlinput" autocomplete="off" style="width: 100%"></td></tr> <tr><td class="right"><label for="urlinput"><strong>Send url</strong></label></td><td><input type="url" name="url" id="urlinput" autocomplete="off" style="width: 100%"></td></tr>
<tr><td class="right"><label for="kepubify"><strong>Kepubify</strong><br/><em>Kobo only</em></label></td><td><input type="checkbox" name="kepubify" id="kepubify" checked /></td></tr> <tr><td class="right"><input type="checkbox" name="kepubify" id="kepubify" checked /></td><td><label for="kepubify"><strong>Kepubify</strong> <em>Kobo only</em></label></td></tr>
<tr><td class="right"><label for="kepubify"><strong>pdfCropMargins</strong><br/><em>PDF files only</em></label></td><td><input type="checkbox" name="pdfCropMargins" id="pdfCropMargins" checked /></td></tr> <tr><td class="right"><input type="checkbox" name="kindlegen" id="kindlegen" checked /></td><td><label for="kindlegen"><strong>KindleGen</strong> <em>Kindle only</em></label></td></tr>
<tr><td class="right"><input type="checkbox" name="pdfcropmargins" id="pdfcropmargins" /></td><td><label for="pdfcropmargins"><strong>Crop margins of pdfs</strong></label></td></tr>
<tr><td class="right"><input type="checkbox" name="transliteration" id="transliteration" /></td><td><label for="transliteration"><strong>Transliteration of filename</strong></label></td></tr>
<tr class="center"><td colspan="2"><input type="submit" value="Upload and send" /></td></tr> <tr class="center"><td colspan="2"><input type="submit" value="Upload and send" /></td></tr>
</table> </table>
<div id="uploadstatus"></div> <div id="uploadstatus"></div>
</form> </form>
<div style="padding: 15px; padding-top: 0; text-align: justify;"> <div style="padding: 15px; padding-top: 0; text-align: justify;">
<p>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.</p> <p>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.</p>
<p>If you send an EPUB file to to a Kindle it will be converted to MOBI with KindleGen. If you send a MOBI file to a Kindle it will be sent unprocessed. If you send an EPUB file and tick the Kepubify checkbox, it will be converted into a Kobo EPUB using Kepubify. If you send a MOBI file to a Kobo, it will not be converted.</p> <p>If you send an EPUB file to to a Kindle it will be converted to MOBI with KindleGen. If you send a MOBI file to a Kindle it will be sent unprocessed. Files sent to Kindle eReaders will have their names stripped of special characters, a limitation of the Kindle browser. If you send an EPUB file and tick the Kepubify checkbox, it will be converted into a Kobo EPUB using Kepubify. If you send a MOBI file to a Kobo, it will not be converted.</p>
<p>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.</p> <p>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.</p>
<p>By using this tool you agree that the ebook you upload is processed on the server and stored for a short time.</p> <p>By using this tool you agree that the ebook you upload is processed on the server and stored for a short time.</p>
</div> </div>
<hr/> <hr/>
<div class="center"> <div class="center">
Created by djazz. Powered by <a href="https://koajs.com/" target="_blank">Koa</a>, <a href="https://pgaskin.net/kepubify/" target="_blank">Kepubify</a>, <a href="https://github.com/abarker/pdfCropMargins" target="_blank">pdfCropMargins</a> and <a href="https://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211" target="_blank">KindleGen</a><br/> Created by djazz. Powered by <a href="https://koajs.com/" target="_blank">Koa</a>, <a href="https://pgaskin.net/kepubify/" target="_blank">Kepubify</a>, <a href="https://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211" target="_blank">KindleGen</a> and <a href="https://github.com/abarker/pdfCropMargins" target="_blank">pdfCropMargins</a><br/>
Source code on <a href="https://github.com/daniel-j/send2ereader" target="_blank">Github</a> - <a id="siteurl">https://send.djazz.se</span> Source code on <a href="https://github.com/daniel-j/send2ereader" target="_blank">Github</a> - <a id="siteurl">https://send.djazz.se</span>
</div> </div>
</div> </div>
@@ -39,6 +40,7 @@
<div id="logs"></div> <div id="logs"></div>
<script> <script>
var uploadform = document.getElementById('uploadform')
var uploadstatus = document.getElementById('uploadstatus') var uploadstatus = document.getElementById('uploadstatus')
var keyinput = document.getElementById('keyinput') var keyinput = document.getElementById('keyinput')
var fileinput = document.getElementById('fileinput') var fileinput = document.getElementById('fileinput')
@@ -47,34 +49,49 @@ var fileinfo = document.getElementById('fileinfo')
var urlinput = document.getElementById('urlinput') var urlinput = document.getElementById('urlinput')
var siteurl = document.getElementById('siteurl') var siteurl = document.getElementById('siteurl')
var flash = getCookies().flash var flashtimer = null
// deleteCookie('flash')
if (flash) { function hideUploadStatus() {
if (flash.message) {
if (flash.success) {
uploadstatus.className = " success"
uploadstatus.innerHTML = flash.message
} else {
uploadstatus.className = " error"
uploadstatus.textContent = flash.message
}
uploadstatus.style.opacity = 1
}
keyinput.value = flash.key || ''
urlinput.value = flash.url || ''
}
uploadstatus.addEventListener('click', function () {
uploadstatus.style.opacity = 0 uploadstatus.style.opacity = 0
setTimeout(function () { clearTimeout(flashtimer)
flashtimer = setTimeout(function () {
uploadstatus.textContent = '' uploadstatus.textContent = ''
uploadstatus.className = '' uploadstatus.className = ''
}, 500) }, 500)
}
function handleFlash(flash) {
// if (!flash) getCookies().flash
console.log(flash)
clearTimeout(flashtimer)
if (flash) {
if (flash.message) {
if (flash.success) {
uploadstatus.className = " success"
uploadstatus.innerHTML = flash.message.trim()
} else {
uploadstatus.className = " error"
uploadstatus.textContent = flash.message.trim()
}
uploadstatus.style.opacity = 1
}
if (flash.key) {
keyinput.value = flash.key
}
if (flash.url) {
urlinput.value = flash.url
}
} else {
hideUploadStatus()
}
}
// handleFlash()
uploadstatus.addEventListener('click', function () {
hideUploadStatus()
}, false) }, false)
document.body.addEventListener("drop", function () {
log("file dropped")
}, false)
function fileinputChange () { function fileinputChange () {
if (!fileinput.files[0] || fileinput.files.length === 0) { if (!fileinput.files[0] || fileinput.files.length === 0) {
@@ -116,6 +133,50 @@ if (isIOS) {
fileinput.accept = '' fileinput.accept = ''
} }
uploadform.addEventListener('submit', function (e) {
hideUploadStatus()
e.preventDefault()
var fd = new FormData(uploadform)
var req = new XMLHttpRequest()
req.open('POST', uploadform.action, true)
req.upload.onprogress = function (e) {
if (e.lengthComputable) {
console.log("progress: " + e.loaded / e.total)
var complete = e.loaded / e.total == 1
handleFlash({
success: true,
message: !complete ? "Uploading file... " + Math.round(100 * e.loaded / e.total) + '%' : "Processing uploaded file... please wait"
})
}
}
req.onreadystatechange = function (e) {
console.log(req.readyState, req.status)
}
req.onload = function () {
console.log('upload ok', req.status, req.responseText, req.responseType)
handleFlash({
success: req.status == 200,
message: req.responseText
})
}
req.onerror = function () {
console.log('upload error', req.status, req.responseText, req.responseType)
handleFlash({
success: false,
message: "Upload error - is the key correct?"
})
}
req.onabort = function () {
console.log('aborted', req.status)
handleFlash({
success: false,
message: "Upload aborted"
})
}
req.send(fd)
return false
}, false)
siteurl.textContent = window.location.href siteurl.textContent = window.location.href
siteurl.href = siteurl.textContent siteurl.href = siteurl.textContent
siteurl.target = '_self' siteurl.target = '_self'