commit
f3f46ae0ad
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
const HttpError = require('../errors/HttpError')
|
||||
const { asyncRead, hmacSign } = require('../util')
|
||||
const { hmacSign } = require('../util')
|
||||
const request = require('request')
|
||||
const uriRegex = /^(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||
|
||||
|
@ -19,37 +19,37 @@ module.exports = {
|
|||
|
||||
if (!host) throw new HttpError(400, 'Missing hostname')
|
||||
if (!body) throw new HttpError(400, 'Missing body')
|
||||
if (!body.metadata) throw new HttpError(400, 'Missing OPDS metadata')
|
||||
if (!body.metadata['@type'] || body.metadata['@type'] !== 'http://schema.org/Book') throw new HttpError(400, 'Invalid \'@type\': expected \'http://schema.org/Book\'')
|
||||
|
||||
const bookExists = await Book.findOne(body)
|
||||
const query = {
|
||||
hostname: host,
|
||||
title: body.metadata.title,
|
||||
author: body.metadata.author,
|
||||
publisher: body.metadata.publisher,
|
||||
identifier: body.metadata.identifier,
|
||||
version: body.metadata.modified.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
const bookExists = await Book.findOne(query)
|
||||
|
||||
if (bookExists) {
|
||||
throw new HttpError(400, 'Version already exists')
|
||||
throw new HttpError(400, 'Ebook already exists')
|
||||
} else {
|
||||
const { title, isbn, author, publisher } = body
|
||||
// require at least 2 fields to be filled out
|
||||
if ([title, isbn, author, publisher].reduce((a, x) => a + (x ? 1 : 0), 0) >= 2) {
|
||||
result = await Book.create(body).fetch()
|
||||
const { publisher, title, author, identifier } = body.metadata
|
||||
// require at least 3 fields to be filled out
|
||||
if ([title, identifier, author, publisher].reduce((a, x) => a + (x ? 1 : 0), 0) >= 3) {
|
||||
result = await Book.create({
|
||||
...query,
|
||||
opds: body
|
||||
}).fetch()
|
||||
} else {
|
||||
throw new HttpError(400, 'Please fill out at least 2 fields (title, author, publisher, isbn)')
|
||||
throw new HttpError(400, 'Please fill out at least 3 opds metadata fields (title, author, publisher, identifier)')
|
||||
}
|
||||
}
|
||||
|
||||
if (req.file('opds')) {
|
||||
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
||||
if (err) {
|
||||
await Book.destroy({ id: result.id })
|
||||
throw new HttpError(500, err.message)
|
||||
}
|
||||
const fd = (uploaded[0] || {}).fd
|
||||
await Book.update({ id: result.id }, { storage: fd })
|
||||
sendUpdatesAsync(result.id)
|
||||
return res.json({
|
||||
...result
|
||||
})
|
||||
})
|
||||
} else {
|
||||
throw new HttpError(400, 'Missing OPDS file upload')
|
||||
}
|
||||
sendUpdatesAsync(result)
|
||||
return res.json(result)
|
||||
} catch (e) {
|
||||
if (e instanceof HttpError) return e.send(res)
|
||||
return res.status(500).json({
|
||||
|
@ -59,16 +59,16 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
|
||||
async function sendUpdatesAsync (id) {
|
||||
const book = await Book.findOne({ id })
|
||||
async function sendUpdatesAsync (book) {
|
||||
const id = book.id
|
||||
const targets = await TargetUrl.find()
|
||||
if (!book) return
|
||||
for (const i in targets) {
|
||||
try {
|
||||
const item = targets[i]
|
||||
const user = await User.findOne({ id: item.user })
|
||||
const { author: fAuthor, publisher: fPublisher, title: fTitle, isbn: fIsbn, url } = item
|
||||
const { author: bAuthor, publisher: bPublisher, title: bTitle, isbn: bIsbn } = book
|
||||
const { author: fAuthor, publisher: fPublisher, title: fTitle, identifier: fIsbn, url } = item
|
||||
const { author: bAuthor, publisher: bPublisher, title: bTitle, identifier: bIsbn, opds } = book
|
||||
sails.log('sending ' + book.id + ' info to ' + url)
|
||||
|
||||
if (uriRegex.test(url)) {
|
||||
|
@ -77,17 +77,7 @@ async function sendUpdatesAsync (id) {
|
|||
if (fTitle && !((bTitle || '').includes(fTitle))) continue
|
||||
if (fIsbn && !((bIsbn || '').includes(fIsbn))) continue
|
||||
|
||||
let content
|
||||
const skipperConfig = sails.config.skipperConfig
|
||||
const adapterConfig = { ...skipperConfig, adapter: undefined }
|
||||
const skipperAdapter = skipperConfig.adapter(adapterConfig)
|
||||
const opdsHelper = await sails.helpers.opds()
|
||||
try {
|
||||
if (!book.storage.length) throw new Error('missing book opds file')
|
||||
content = await asyncRead(skipperAdapter, opdsHelper, book.storage)
|
||||
} catch (e) {
|
||||
content = await opdsHelper.book2opds(book)
|
||||
}
|
||||
let content = opds
|
||||
const timestamp = Date.now()
|
||||
request.post({
|
||||
url: url,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const HttpError = require('../errors/HttpError')
|
||||
const { asyncRead } = require('../util')
|
||||
|
||||
module.exports = {
|
||||
navigation: async function (req, res) {
|
||||
|
@ -49,19 +48,7 @@ module.exports = {
|
|||
throw new HttpError(404, 'No books matching those parameters were found.')
|
||||
}
|
||||
|
||||
const skipperConfig = sails.config.skipperConfig
|
||||
const adapterConfig = { ...skipperConfig, adapter: undefined }
|
||||
const skipperAdapter = skipperConfig.adapter(adapterConfig)
|
||||
const opdsHelper = await sails.helpers.opds()
|
||||
|
||||
books = await Promise.all(books.map(book => {
|
||||
try {
|
||||
if (!book.storage.length) throw new Error('missing book opds file')
|
||||
return asyncRead(skipperAdapter, opdsHelper, book.storage)
|
||||
} catch (e) {
|
||||
return opdsHelper.book2opds(book)
|
||||
}
|
||||
}))
|
||||
books = books.map(b => b.opds)
|
||||
|
||||
return res.json({
|
||||
metadata: {
|
||||
|
@ -70,10 +57,10 @@ module.exports = {
|
|||
currentPage: page
|
||||
},
|
||||
links: [
|
||||
{ rel: 'self', href: `new?page=${page}`, type: 'application/opds+json' },
|
||||
{ rel: 'prev', href: `new?page=${page > 1 ? page - 1 : page}`, type: 'application/opds+json' },
|
||||
{ rel: 'next', href: `new?page=${page + 1}`, type: 'application/opds+json' },
|
||||
{ 'rel': 'search', 'href': 'all{?title,author,version,isbn}', 'type': 'application/opds+json', 'templated': true }
|
||||
{ rel: 'self', href: `all?page=${page}`, type: 'application/opds+json' },
|
||||
{ rel: 'prev', href: `all?page=${page > 1 ? page - 1 : page}`, type: 'application/opds+json' },
|
||||
{ rel: 'next', href: `all?page=${page + 1}`, type: 'application/opds+json' },
|
||||
{ rel: 'search', href: 'all{?title,author,version,isbn}', type: 'application/opds+json', templated: true }
|
||||
],
|
||||
publications: books
|
||||
})
|
||||
|
|
|
@ -20,10 +20,10 @@ module.exports = {
|
|||
title: { type: 'string', required: true },
|
||||
author: { type: 'string' },
|
||||
publisher: { type: 'string' },
|
||||
isbn: { type: 'string' },
|
||||
identifier: { type: 'string' },
|
||||
version: { type: 'string' },
|
||||
hostname: { type: 'string' },
|
||||
storage: { type: 'string' }
|
||||
opds: { type: 'json' }
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
'use strict'
|
||||
|
||||
import React from 'react'
|
||||
import IconButton from '../components/IconButton'
|
||||
import ConfirmIconButton from '../containers/ConfirmIconButton'
|
||||
import UnderlineInput from '../components/UnderlineInput'
|
||||
import './listitem.scss'
|
||||
import { changeUrlField, setUrl, removeUrl, setEditingUri } from '../actions'
|
||||
|
||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||
const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
|
||||
// const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
|
||||
|
||||
class UriListItem extends React.Component {
|
||||
constructor () {
|
||||
|
@ -81,11 +80,10 @@ class UriListItem extends React.Component {
|
|||
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||
<UnderlineInput
|
||||
className='uri flex'
|
||||
type='number'
|
||||
type='text'
|
||||
name={'isbn-' + this.props.item.id}
|
||||
placeholder='ISBN'
|
||||
placeholder='Identifier'
|
||||
value={'' + this.props.item.isbn}
|
||||
pattern={isbnRegex}
|
||||
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'isbn', e.target.value))}
|
||||
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||
</div>
|
||||
|
|
|
@ -85,12 +85,17 @@
|
|||
header {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
flex: none;
|
||||
|
||||
.logo {
|
||||
color: $black-1;
|
||||
text-decoration: none;
|
||||
font-size: 1.2em;
|
||||
padding: 0 20px;
|
||||
|
||||
@include break('small') {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
nav {
|
||||
a {
|
||||
|
@ -107,7 +112,7 @@
|
|||
line-height: 40px;
|
||||
height: 40px;
|
||||
margin: 5px 20px 5px 0;
|
||||
background: $accent-1;
|
||||
background: $accent-2;
|
||||
color: $text-light-1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
@ -115,12 +120,13 @@
|
|||
}
|
||||
}
|
||||
footer {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
line-height: 40px;
|
||||
background: $accent-1;
|
||||
color: white;
|
||||
padding: 0 20px;
|
||||
box-shadow: $shadow-3;
|
||||
flex: none;
|
||||
|
||||
a {
|
||||
color: $accent-3;
|
||||
|
@ -131,7 +137,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
main {
|
||||
.paper {
|
||||
background: white;
|
||||
width: 85%;
|
||||
max-width: 900px;
|
||||
|
@ -158,6 +164,7 @@
|
|||
background: $black-5;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
overflow-x: scroll;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
|
@ -181,5 +188,11 @@
|
|||
p {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
@include break('small') {
|
||||
width: 100%;
|
||||
padding: 30px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ module.exports.http = {
|
|||
publishLimit: publishLimiter,
|
||||
passportInit: require('passport').initialize(),
|
||||
passportSession: require('passport').session(),
|
||||
allowCrossDomain: allowCrossDomain
|
||||
allowCrossDomain: allowCrossDomain,
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
|
@ -83,10 +83,10 @@ module.exports.http = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
|
||||
// bodyParser: (function _configureBodyParser(){
|
||||
// var skipper = require('skipper');
|
||||
// var middlewareFn = skipper({ strict: true });
|
||||
// return middlewareFn;
|
||||
// })(),
|
||||
bodyParser: (function _configureBodyParser () {
|
||||
const skipper = require('skipper')
|
||||
const middlewareFn = skipper({ strict: true })
|
||||
return middlewareFn
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
|
36
docs/api.md
36
docs/api.md
|
@ -10,20 +10,31 @@ POST to /api/publish containing headers:
|
|||
roe-secret: <api secret>
|
||||
}
|
||||
|
||||
and body:
|
||||
and opds2 publication body with type `application/json`:
|
||||
|
||||
{
|
||||
title: The ebook's title,
|
||||
author: The author (optional),
|
||||
version: A version number (optional),
|
||||
isbn: The ISBN (optional),
|
||||
opds: file
|
||||
"metadata": {
|
||||
"@type": "http://schema.org/Book",
|
||||
"title": "Moby-Dick",
|
||||
"author": "Herman Melville",
|
||||
"identifier": "urn:isbn:978031600000X",
|
||||
"publisher": "Ebook Publisher.com",
|
||||
"language": "en",
|
||||
"modified": "2015-09-29T17:00:00Z"
|
||||
},
|
||||
"links": [
|
||||
{"rel": "self", "href": "http://example.org/manifest.json", "type": "application/webpub+json"}
|
||||
],
|
||||
"images": [
|
||||
{"href": "http://example.org/cover.jpg", "type": "image/jpeg", "height": 1400, "width": 800},
|
||||
{"href": "http://example.org/cover-small.jpg", "type": "image/jpeg", "height": 700, "width": 400},
|
||||
{"href": "http://example.org/cover.svg", "type": "image/svg+xml"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each tuple of `(title, author, version, isbn)` must be unique.
|
||||
|
||||
The `opds` parameter is an opds2 file containing an opds Publication sent along with the post body.
|
||||
@Type must be `http://schema.org/Book`.
|
||||
Each tuple of `(title, author, publisher, identifier, modified)` must be unique.
|
||||
|
||||
The server will respond with either:
|
||||
|
||||
|
@ -35,9 +46,10 @@ The server will respond with either:
|
|||
"id": number,
|
||||
"title": string,
|
||||
"author": string,
|
||||
"isbn": string,
|
||||
"publisher": string,
|
||||
"identifier": string,
|
||||
"version": string,
|
||||
"storage": string
|
||||
"opds": json
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -95,6 +107,7 @@ The server will respond with either:
|
|||
"@type": "http://schema.org/Book",
|
||||
"title": "Moby-Dick",
|
||||
"author": "Herman Melville",
|
||||
"publisher": "Ebook Publisher.com",
|
||||
"identifier": "urn:isbn:978031600000X",
|
||||
"language": "en",
|
||||
"modified": "2015-09-29T17:00:00Z"
|
||||
|
@ -152,6 +165,7 @@ HTTP Body:
|
|||
"@type": "http://schema.org/Book",
|
||||
"title": "Moby-Dick",
|
||||
"author": "Herman Melville",
|
||||
"publisher": "Ebook Publisher.com",
|
||||
"identifier": "urn:isbn:978031600000X",
|
||||
"language": "en",
|
||||
"modified": "2015-09-29T17:00:00Z"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
exports.up = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.json('opds')
|
||||
t.renameColumn('isbn', 'identifier')
|
||||
t.dropColumns('storage')
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
exports.down = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.dropColumns('opds')
|
||||
t.renameColumn('identifier', 'isbn')
|
||||
t.string('storage')
|
||||
})
|
||||
])
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
exports.up = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.dropColumns('source')
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
exports.down = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.string('source')
|
||||
})
|
||||
])
|
||||
}
|
|
@ -10,9 +10,9 @@
|
|||
<body class='home flex-container flex-vertical'>
|
||||
<%- partial('../shared/header.html') %>
|
||||
<main class="flex">
|
||||
<% if (content) { %>
|
||||
<section class="paper">
|
||||
<%- content %>
|
||||
<% } %>
|
||||
</section>
|
||||
</main>
|
||||
<%- partial('../shared/footer.html') %>
|
||||
</body>
|
||||
|
|
Loading…
Reference in New Issue