Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
f80673ade2 | |||
87e5a16a06 | |||
ca0a10e7b7 | |||
a1a4d15e4e | |||
dcab64c78d | |||
d2adf23936 | |||
5b4a4cc75d |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.venv
|
||||
__pycache__/
|
||||
.vscode
|
||||
.vscode
|
||||
.vercel
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python
|
||||
FROM python:alpine
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
@ -6,8 +6,6 @@ COPY ./requirements /srv/requirements
|
||||
|
||||
RUN pip install -r requirements/prod.txt
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
COPY ./app /srv/app
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8081}
|
||||
|
12
README.md
12
README.md
@ -42,12 +42,22 @@ Simple docker deployment
|
||||
docker build . -t publite_backend
|
||||
|
||||
# run it with docker
|
||||
docker run -p <port>:80 publite_backend
|
||||
docker run -p <port>:8081 publite_backend
|
||||
```
|
||||
|
||||
Dokku deployment with image from Docker Hub
|
||||
|
||||
```bash
|
||||
dokku apps:create publitebackend
|
||||
|
||||
# increase file size limit to be able to upload bigger books
|
||||
dokku nginx:set publitebackend client_max_body_size 50m
|
||||
|
||||
dokku git:from-image publitebackend publite/backend:latest
|
||||
```
|
||||
|
||||
# TODO
|
||||
|
||||
- Separate epub and fb2 files to python modules
|
||||
- Rewrite own `.opf` file parsing to get rid of dependency on EbookLib
|
||||
- Add cli interfaces for epub and fb2 libs
|
||||
|
17
app/epub.py
17
app/epub.py
@ -8,7 +8,7 @@ from base64 import b64encode
|
||||
from functools import cache
|
||||
from tempfile import SpooledTemporaryFile
|
||||
|
||||
import aiofiles as aiof
|
||||
import aiofiles
|
||||
import ebooklib
|
||||
from ebooklib import epub
|
||||
from fastapi import HTTPException
|
||||
@ -61,7 +61,7 @@ async def epub_to_tokens(
|
||||
|
||||
tokens = {}
|
||||
|
||||
async with aiof.tempfile.NamedTemporaryFile() as tmp:
|
||||
async with aiofiles.tempfile.NamedTemporaryFile() as tmp:
|
||||
await tmp.write(file.read())
|
||||
|
||||
# Reading book file
|
||||
@ -108,7 +108,6 @@ async def epub_to_tokens(
|
||||
|
||||
|
||||
def read_metadata(book: epub.EpubBook) -> dict[str, str]:
|
||||
|
||||
"""
|
||||
Reads metadata from xml to dict
|
||||
"""
|
||||
@ -121,7 +120,6 @@ def read_metadata(book: epub.EpubBook) -> dict[str, str]:
|
||||
|
||||
|
||||
def convert_list(titles_list: list[tuple[str, dict[str, str]]]) -> str:
|
||||
|
||||
"""
|
||||
Joins titles list to one string
|
||||
"""
|
||||
@ -134,7 +132,6 @@ def convert_list(titles_list: list[tuple[str, dict[str, str]]]) -> str:
|
||||
|
||||
|
||||
def set_cover(tokens: DocumentTokens) -> None:
|
||||
|
||||
"""
|
||||
Converts cover file name to base64 image stored in `tokens`
|
||||
"""
|
||||
@ -145,7 +142,6 @@ def set_cover(tokens: DocumentTokens) -> None:
|
||||
|
||||
|
||||
def epub_tokens2html(spine: list[tuple[str, str]], tokens: DocumentTokens) -> bytes:
|
||||
|
||||
"""
|
||||
Joins chapters in `spice` to one html string
|
||||
"""
|
||||
@ -157,11 +153,10 @@ def epub_tokens2html(spine: list[tuple[str, str]], tokens: DocumentTokens) -> by
|
||||
if file_path:
|
||||
res += process_xhtml(file_path, tokens)
|
||||
|
||||
return html.escape(html.unescape(res))
|
||||
return html.unescape(res)
|
||||
|
||||
|
||||
def process_xhtml(path: str, tokens: DocumentTokens) -> bytes:
|
||||
|
||||
"""
|
||||
Processes content of one xml body
|
||||
"""
|
||||
@ -179,7 +174,6 @@ def process_xhtml(path: str, tokens: DocumentTokens) -> bytes:
|
||||
|
||||
|
||||
def process_content(node: etree.Element, path: str, tokens: DocumentTokens) -> None:
|
||||
|
||||
"""
|
||||
Recursive function for xml element convertion to valid html
|
||||
"""
|
||||
@ -219,7 +213,6 @@ def process_content(node: etree.Element, path: str, tokens: DocumentTokens) -> N
|
||||
|
||||
|
||||
def process_a_element(node: etree.Element, path: str):
|
||||
|
||||
r"""
|
||||
Converts `filed` links to ids in \<a\> element
|
||||
"""
|
||||
@ -237,7 +230,6 @@ def process_a_element(node: etree.Element, path: str):
|
||||
|
||||
|
||||
def process_media_element(node: etree.Element, path: str, tokens: DocumentTokens):
|
||||
|
||||
"""
|
||||
Replaces file paths to base64 encoded media in `src` and `srcset` tags
|
||||
"""
|
||||
@ -256,7 +248,6 @@ def process_media_element(node: etree.Element, path: str, tokens: DocumentTokens
|
||||
|
||||
|
||||
def rel_to_abs_path(parent: str, rel: str):
|
||||
|
||||
"""
|
||||
Helper for relative path to media convertion to absolute
|
||||
"""
|
||||
@ -266,7 +257,6 @@ def rel_to_abs_path(parent: str, rel: str):
|
||||
|
||||
@cache
|
||||
def path_to_name(path: str) -> str:
|
||||
|
||||
"""
|
||||
Helper function for getting file name
|
||||
"""
|
||||
@ -275,7 +265,6 @@ def path_to_name(path: str) -> str:
|
||||
|
||||
|
||||
def children_to_html(root: etree.Element) -> bytes:
|
||||
|
||||
"""
|
||||
Converts all xml children of element to string and joins them
|
||||
"""
|
||||
|
15
app/fb2.py
15
app/fb2.py
@ -32,7 +32,7 @@ async def fb22html(file: SpooledTemporaryFile) -> HTMLBook:
|
||||
|
||||
return {
|
||||
**(tokens["metadata"]),
|
||||
"content": html.escape(html.unescape(html_content.decode())),
|
||||
"content": html.unescape(html_content.decode()),
|
||||
}
|
||||
|
||||
except Exception as err:
|
||||
@ -42,7 +42,6 @@ async def fb22html(file: SpooledTemporaryFile) -> HTMLBook:
|
||||
|
||||
|
||||
def fb22tokens(file: SpooledTemporaryFile) -> DocumentTokens:
|
||||
|
||||
r"""
|
||||
Parses fb2 file as xml document.
|
||||
It puts book metadata, its content and media to `tokens` dictionary and returns it.
|
||||
@ -78,7 +77,8 @@ def fb22tokens(file: SpooledTemporaryFile) -> DocumentTokens:
|
||||
metadata = {}
|
||||
metadata["title"] = book_info.find("./book-title", namespaces).text
|
||||
metadata["author"] = get_author(book_info.find("./author", namespaces))
|
||||
metadata["cover"] = get_cover(book_info.find("./coverpage", namespaces))
|
||||
metadata["cover"] = get_cover(
|
||||
book_info.find("./coverpage", namespaces))
|
||||
if "cover" not in metadata.keys():
|
||||
metadata.pop("cover")
|
||||
|
||||
@ -104,7 +104,6 @@ def fb22tokens(file: SpooledTemporaryFile) -> DocumentTokens:
|
||||
|
||||
|
||||
def get_author(author: Element) -> str:
|
||||
|
||||
"""
|
||||
Converts author xml structure to string
|
||||
"""
|
||||
@ -116,7 +115,7 @@ def get_author(author: Element) -> str:
|
||||
"last-name",
|
||||
):
|
||||
tag = author.find("./" + tag_name, namespaces)
|
||||
if tag is not None:
|
||||
if tag is not None and tag.text is not None:
|
||||
res.append(tag.text)
|
||||
if len(res) == 0:
|
||||
res = author.find("./nickname", namespaces).text
|
||||
@ -127,7 +126,6 @@ def get_author(author: Element) -> str:
|
||||
|
||||
|
||||
def get_cover(coverpage: Optional[Element]) -> Optional[str]:
|
||||
|
||||
"""
|
||||
Extracts cover image id if exists
|
||||
"""
|
||||
@ -148,7 +146,6 @@ def set_cover(tokens: DocumentTokens) -> None:
|
||||
|
||||
|
||||
def fb2body2html(tokens: DocumentTokens) -> str:
|
||||
|
||||
"""
|
||||
Convert fb2 xml to html, joins bodies into one string
|
||||
"""
|
||||
@ -163,7 +160,6 @@ def fb2body2html(tokens: DocumentTokens) -> str:
|
||||
|
||||
|
||||
def process_section(body: Element, tokens: DocumentTokens) -> str:
|
||||
|
||||
"""
|
||||
Processes individual sections, recursively goes throw sections tree
|
||||
"""
|
||||
@ -191,7 +187,6 @@ def process_section(body: Element, tokens: DocumentTokens) -> str:
|
||||
|
||||
|
||||
def children_to_html(root: Element) -> str:
|
||||
|
||||
"""
|
||||
Converts xml tag children to string
|
||||
"""
|
||||
@ -205,7 +200,6 @@ def children_to_html(root: Element) -> str:
|
||||
|
||||
|
||||
def process_image(element: Element, tokens: DocumentTokens) -> None:
|
||||
|
||||
r"""
|
||||
Converts fb2 \<image /\> to html \<img /\>. Replaces xlink:href with src="\<base64_image_data\>"
|
||||
"""
|
||||
@ -236,7 +230,6 @@ tag_with_class = {
|
||||
|
||||
|
||||
def process_content(root: Element, tokens: DocumentTokens) -> None:
|
||||
|
||||
"""
|
||||
Converts fb2 xml tag names to html equivalents and my own styled elements.
|
||||
Resolves binary data dependencies
|
||||
|
16
app/main.py
16
app/main.py
@ -3,12 +3,17 @@
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel # pylint: disable=no-name-in-module
|
||||
|
||||
from .epub import epub2html
|
||||
from .fb2 import fb22html
|
||||
from .utils import HashedHTMLBook, add_hash
|
||||
|
||||
origins = (
|
||||
"*"
|
||||
)
|
||||
|
||||
|
||||
class DebugInfo(BaseModel): # pylint: disable=too-few-public-methods
|
||||
"""Main handler return types"""
|
||||
@ -18,6 +23,14 @@ class DebugInfo(BaseModel): # pylint: disable=too-few-public-methods
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
start_time = datetime.now()
|
||||
|
||||
|
||||
@ -47,7 +60,8 @@ async def create_upload_file(file: UploadFile = File(...)):
|
||||
elif file.filename.endswith(".epub"):
|
||||
content = await epub2html(file.file)
|
||||
else:
|
||||
raise HTTPException(status_code=415, detail="Error! Unsupported file type")
|
||||
raise HTTPException(
|
||||
status_code=415, detail="Error! Unsupported file type")
|
||||
|
||||
h_content = add_hash(content)
|
||||
|
||||
|
@ -17,7 +17,7 @@ class HTMLBook(BaseModel): # pylint: disable=too-few-public-methods
|
||||
|
||||
title: str
|
||||
author: str
|
||||
cover: Optional[str]
|
||||
cover: Optional[str] = None
|
||||
content: str
|
||||
|
||||
|
||||
|
5
vercel.json
Normal file
5
vercel.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/api/main"}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user