better filetype validation, support pdf/cbz/cbr/html/txt
This commit is contained in:
59
index.js
59
index.js
@@ -9,11 +9,18 @@ const mkdirp = require('mkdirp')
|
||||
const fs = require('fs')
|
||||
const { spawn } = require('child_process')
|
||||
const { extname, basename, dirname } = require('path')
|
||||
const FileType = require('file-type')
|
||||
|
||||
const port = 3001
|
||||
const expireDelay = 30 // 30 seconds
|
||||
const maxExpireDuration = 2 * 60 * 60 // 2 hours
|
||||
const maxFileSize = 1024 * 1024 * 400 // 400 MB
|
||||
const maxFileSize = 1024 * 1024 * 500 // 500 MB
|
||||
|
||||
const TYPE_EPUB = 'application/epub+zip'
|
||||
const TYPE_MOBI = 'application/x-mobipocket-ebook'
|
||||
|
||||
const allowedTypes = [TYPE_EPUB, TYPE_MOBI, 'application/pdf', 'application/vnd.comicbook+zip', 'application/vnd.comicbook-rar', 'text/html', 'text/plain', 'application/zip', 'application/x-rar-compressed']
|
||||
const allowedExtensions = ['epub', 'mobi', 'pdf', 'cbz', 'cbr', 'html', 'txt']
|
||||
|
||||
const keyChars = "3469ACEGHLMNPRTY"
|
||||
const keyLength = 4
|
||||
@@ -75,7 +82,6 @@ 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 + extname(file.originalname).toLowerCase())
|
||||
}
|
||||
}),
|
||||
@@ -84,15 +90,15 @@ const upload = multer({
|
||||
files: 1
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
console.log('Incoming file:', file)
|
||||
const key = req.body.key.toUpperCase()
|
||||
if (!app.context.keys.has(key)) {
|
||||
console.error('FileFilter: Unknown key: ' + key)
|
||||
cb(null, false)
|
||||
return
|
||||
}
|
||||
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)
|
||||
if (!allowedTypes.includes(file.mimetype) || !allowedExtensions.includes(extname(file.originalname.toLowerCase()).substr(1))) {
|
||||
console.error('FileFilter: File is of an invalid type ', file)
|
||||
cb(null, false)
|
||||
return
|
||||
}
|
||||
@@ -156,33 +162,56 @@ router.get('/download/:key', async ctx => {
|
||||
router.post('/upload', upload.single('file'), async ctx => {
|
||||
const key = ctx.request.body.key.toUpperCase()
|
||||
|
||||
if (ctx.request.file) {
|
||||
console.log('Uploaded file:', ctx.request.file)
|
||||
}
|
||||
|
||||
if (!ctx.keys.has(key)) {
|
||||
flash(ctx, {
|
||||
message: 'Unknown key ' + key,
|
||||
success: false
|
||||
})
|
||||
ctx.redirect('back', '/')
|
||||
if (ctx.request.file) {
|
||||
fs.unlink(ctx.request.file.path, (err) => {
|
||||
if (err) console.error(err)
|
||||
else console.log('Removed file', ctx.request.file.path)
|
||||
})
|
||||
}
|
||||
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', '/')
|
||||
if (ctx.request.file) {
|
||||
fs.unlink(ctx.request.file.path, (err) => {
|
||||
if (err) console.error(err)
|
||||
else console.log('Removed file', ctx.request.file.path)
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
const nameLower = ctx.request.file.originalname.toLowerCase()
|
||||
if (!nameLower.endsWith('.epub') && !nameLower.endsWith('.mobi')) {
|
||||
|
||||
const mimetype = ctx.request.file.mimetype
|
||||
|
||||
const type = await FileType.fromFile(ctx.request.file.path)
|
||||
|
||||
if (!type || !allowedTypes.includes(type.mime)) {
|
||||
flash(ctx, {
|
||||
message: 'Uploaded file does not end with .epub or .mobi ' + ctx.request.file.originalname,
|
||||
message: 'Uploaded file is of an invalid type: ' + ctx.request.file.originalname + ' (' + (type? type.mime : 'unknown mimetype') + ')',
|
||||
success: false,
|
||||
key: key
|
||||
})
|
||||
console.error(ctx.request.file)
|
||||
ctx.redirect('back', '/')
|
||||
fs.unlink(ctx.request.file.path, (err) => {
|
||||
if (err) console.error(err)
|
||||
else console.log('Removed file', ctx.request.file.path)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -193,7 +222,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
||||
let filename = ctx.request.file.originalname
|
||||
let conversion = null
|
||||
|
||||
if (nameLower.endsWith('.epub') && info.agent.includes('Kindle')) {
|
||||
if (mimetype === TYPE_EPUB && info.agent.includes('Kindle')) {
|
||||
// convert to .mobi
|
||||
conversion = 'kindlegen'
|
||||
const outname = ctx.request.file.path.replace(/\.epub$/i, '.mobi')
|
||||
@@ -207,7 +236,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
||||
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)
|
||||
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)
|
||||
@@ -221,7 +250,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
||||
})
|
||||
})
|
||||
|
||||
} else if (nameLower.endsWith('.epub') && info.agent.includes('Kobo') && ctx.request.body.kepubify) {
|
||||
} else if (mimetype === TYPE_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')
|
||||
@@ -235,7 +264,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
||||
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)
|
||||
else console.log('Removed file', ctx.request.file.path)
|
||||
})
|
||||
if (code !== 0) {
|
||||
reject('kepubify error code ' + code)
|
||||
@@ -264,7 +293,7 @@ router.post('/upload', upload.single('file'), async ctx => {
|
||||
// size: ctx.request.file.size,
|
||||
uploaded: new Date()
|
||||
}
|
||||
console.log(info.file)
|
||||
|
||||
flash(ctx, {
|
||||
message: 'Upload successful!<br/>'+(conversion ? ' Ebook was converted with ' + conversion + ' and sent' : ' Sent')+' to a '+(info.agent.includes('Kobo') ? 'Kobo' : 'Kindle')+' device.<br/>Filename: ' + filename,
|
||||
success: true,
|
||||
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -21,6 +21,16 @@
|
||||
"path-to-regexp": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"@tokenizer/token": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz",
|
||||
"integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w=="
|
||||
},
|
||||
"@types/debug": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
|
||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -240,6 +250,17 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"file-type": {
|
||||
"version": "14.7.1",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-14.7.1.tgz",
|
||||
"integrity": "sha512-sXAMgFk67fQLcetXustxfKX+PZgHIUFn96Xld9uH8aXPdX3xOp0/jg9OdouVTvQrf7mrn+wAa4jN/y9fUOOiRA==",
|
||||
"requires": {
|
||||
"readable-web-to-node-stream": "^2.0.0",
|
||||
"strtok3": "^6.0.3",
|
||||
"token-types": "^2.0.0",
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -295,6 +316,11 @@
|
||||
"resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz",
|
||||
"integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -305,6 +331,11 @@
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz",
|
||||
"integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw=="
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
@@ -529,6 +560,11 @@
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz",
|
||||
"integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="
|
||||
},
|
||||
"peek-readable": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz",
|
||||
"integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -545,6 +581,11 @@
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"readable-web-to-node-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA=="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
@@ -570,6 +611,16 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
},
|
||||
"strtok3": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.4.tgz",
|
||||
"integrity": "sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.1.1",
|
||||
"@types/debug": "^4.1.5",
|
||||
"peek-readable": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@@ -599,6 +650,15 @@
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"token-types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz",
|
||||
"integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.1.0",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
@@ -618,6 +678,14 @@
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||
"requires": {
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@koa/multer": "^3.0.0",
|
||||
"@koa/router": "^9.3.1",
|
||||
"file-type": "^14.7.1",
|
||||
"koa": "^2.13.0",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-sendfile": "^2.0.1",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||
<table style="margin: 0 auto;" cellpadding=0 cellspacing=0>
|
||||
<tr><td class="right"><label for="keyinput">Unique key</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="fileinput">EPUB/MOBI file</label></td><td><input type="file" name="file" id="fileinput" accept=".epub,.mobi,application/epub+zip,application/x-mobipocket-ebook" required /></td></tr>
|
||||
<tr><td class="right"><label for="fileinput">EPUB/MOBI file</label></td><td><input type="file" name="file" id="fileinput" accept=".epub,.mobi,.pdf,.cbz,.cbr,application/epub+zip,application/x-mobipocket-ebook,application/pdf,application/vnd.comicbook+zip,application/vnd.comicbook-rar" required /></td></tr>
|
||||
<tr><td></td><td id="fileinfo"></td></tr>
|
||||
<tr><td class="right"><label for="kepubify">Kepubify</label></td><td><input type="checkbox" name="kepubify" id="kepubify" checked /></td></tr>
|
||||
<tr><td></td><td><input type="submit" value="Upload and send" /></td></tr>
|
||||
|
||||
Reference in New Issue
Block a user