improve error reporting, ajax form upload
This commit is contained in:
@@ -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
|
||||||
|
|||||||
65
index.js
65
index.js
@@ -1,12 +1,12 @@
|
|||||||
#!/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')
|
||||||
@@ -79,6 +79,11 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
@@ -110,12 +115,12 @@ const upload = multer({
|
|||||||
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: " + file, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cb(null, true)
|
cb(null, true)
|
||||||
@@ -181,7 +186,23 @@ router.get('/download/:key', async ctx => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/upload', upload.single('file'), async ctx => {
|
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()
|
||||||
|
|
||||||
const key = ctx.request.body.key.toUpperCase()
|
const key = ctx.request.body.key.toUpperCase()
|
||||||
|
|
||||||
if (ctx.request.file) {
|
if (ctx.request.file) {
|
||||||
@@ -193,13 +214,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 +240,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,11 +272,11 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +311,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
} 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')
|
||||||
@@ -336,7 +358,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) {
|
||||||
@@ -349,7 +372,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
|||||||
success: false,
|
success: false,
|
||||||
key: key
|
key: key
|
||||||
})
|
})
|
||||||
ctx.redirect('back', '/')
|
await next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +382,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 => {
|
||||||
@@ -376,6 +400,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
|
||||||
}
|
}
|
||||||
@@ -402,10 +427,9 @@ 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')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
app.use(router.routes())
|
app.use(router.routes())
|
||||||
app.use(router.allowedMethods())
|
app.use(router.allowedMethods())
|
||||||
|
|
||||||
@@ -414,7 +438,14 @@ 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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
1340
package-lock.json
generated
1340
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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,13 @@
|
|||||||
"@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.14.2",
|
"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",
|
||||||
"transliteration": "^2.3.5"
|
"transliteration": "^2.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
patches/@koa+multer+3.0.2.patch
Normal file
15
patches/@koa+multer+3.0.2.patch
Normal 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);
|
||||||
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
<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>
|
||||||
@@ -38,6 +37,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')
|
||||||
@@ -46,34 +46,46 @@ 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 handleFlash(flash) {
|
||||||
if (flash.message) {
|
if (!flash) flash = getCookies().flash
|
||||||
if (flash.success) {
|
console.log(flash)
|
||||||
uploadstatus.className = " success"
|
clearTimeout(flashtimer)
|
||||||
uploadstatus.innerHTML = flash.message
|
if (flash) {
|
||||||
} else {
|
if (flash.message) {
|
||||||
uploadstatus.className = " error"
|
if (flash.success) {
|
||||||
uploadstatus.textContent = flash.message
|
uploadstatus.className = " success"
|
||||||
|
uploadstatus.innerHTML = flash.message
|
||||||
|
} else {
|
||||||
|
uploadstatus.className = " error"
|
||||||
|
uploadstatus.textContent = flash.message
|
||||||
|
}
|
||||||
|
uploadstatus.style.opacity = 1
|
||||||
}
|
}
|
||||||
uploadstatus.style.opacity = 1
|
keyinput.value = flash.key || ''
|
||||||
|
urlinput.value = flash.url || ''
|
||||||
|
} else {
|
||||||
|
uploadstatus.style.opacity = 0
|
||||||
|
|
||||||
|
flashtimer = setTimeout(function () {
|
||||||
|
uploadstatus.textContent = ''
|
||||||
|
uploadstatus.className = ''
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
keyinput.value = flash.key || ''
|
|
||||||
urlinput.value = flash.url || ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFlash()
|
||||||
|
|
||||||
uploadstatus.addEventListener('click', function () {
|
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)
|
||||||
}, 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) {
|
||||||
@@ -115,6 +127,35 @@ if (isIOS) {
|
|||||||
fileinput.accept = ''
|
fileinput.accept = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadform.addEventListener('submit', function (e) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.onreadystatechange = function (e) {
|
||||||
|
console.log(req.readyState, req.status)
|
||||||
|
}
|
||||||
|
req.onload = function () {
|
||||||
|
console.log('upload ok', req.status, req.responseText, req.responseType)
|
||||||
|
handleFlash()
|
||||||
|
}
|
||||||
|
req.onerror = function () {
|
||||||
|
console.log('upload error', req.status)
|
||||||
|
handleFlash()
|
||||||
|
}
|
||||||
|
req.onabort = function () {
|
||||||
|
console.log('aborted', req.status)
|
||||||
|
handleFlash()
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
|||||||
Reference in New Issue
Block a user