openmedialibrary/oml/item/handlers.py
2024-06-10 16:26:27 +01:00

275 lines
9.7 KiB
Python

# -*- coding: utf-8 -*-
from datetime import datetime
import mimetypes
import os
from urllib.request import quote
import zipfile
import base64
import ox
from .models import Item, File
from user.models import List
from .scan import add_file
import db
import settings
import tornado.web
import tornado.gen
from concurrent.futures import ThreadPoolExecutor
from tornado.concurrent import run_on_executor
from oxtornado import json_dumps, json_response
from media import get_id
import state
import logging
logger = logging.getLogger(__name__)
MAX_WORKERS = 4
class OptionalBasicAuthMixin(object):
class SendChallenge(Exception):
pass
def prepare(self):
if settings.preferences.get('authentication'):
try:
self.authenticate_user()
except self.SendChallenge:
self.send_auth_challenge()
def send_auth_challenge(self):
realm = "Open Media Library"
hdr = 'Basic realm="%s"' % realm
self.set_status(401)
self.set_header('www-authenticate', hdr)
self.finish()
return False
def authenticate_user(self):
auth_header = self.request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Basic '):
raise self.SendChallenge()
auth_data = auth_header.split(None, 1)[-1]
auth_data = base64.b64decode(auth_data).decode('ascii')
username, password = auth_data.split(':', 1)
auth = settings.preferences.get('authentication')
if auth.get('username') == username and auth.get('password') == password:
self._current_user = username
else:
raise self.SendChallenge()
class OMLHandler(OptionalBasicAuthMixin, tornado.web.RequestHandler):
def initialize(self):
pass
class EpubHandler(OMLHandler):
def get(self, id, filename):
with db.session():
item = Item.get(id)
path = item.get_path()
if not item or item.info['extension'] != 'epub' or not path:
self.set_status(404)
self.write('')
else:
z = zipfile.ZipFile(path)
if filename == '':
self.write('<br>\n'.join([f.filename for f in z.filelist]))
elif filename not in [f.filename for f in z.filelist]:
self.set_status(404)
self.write('')
else:
content_type = {
'xpgt': 'application/vnd.adobe-page-template+xml'
}.get(filename.split('.')[0], mimetypes.guess_type(filename)[0]) or 'text/plain'
self.set_header('Content-Type', content_type)
self.write(z.read(filename))
class CropHandler(OMLHandler):
def get(self, id, page, left, top, right, bottom):
from media.pdf import crop
with db.session():
item = Item.get(id)
path = item.get_path()
data = crop(path, page, left, top, right, bottom)
if path and data:
self.set_header('Content-Type', 'image/jpeg')
self.set_header('Content-Length', str(len(data)))
self.write(data)
return
self.set_status(404)
return
def serve_static(handler, path, mimetype, include_body=True, disposition=None):
handler.set_header('Content-Type', mimetype)
size = os.stat(path).st_size
handler.set_header('Accept-Ranges', 'bytes')
if disposition:
handler.set_header('Content-Disposition', "attachment; filename*=UTF-8''%s" % quote(disposition.encode('utf-8')))
if include_body:
if 'Range' in handler.request.headers:
handler.set_status(206)
r = handler.request.headers.get('Range').split('=')[-1].split('-')
start = int(r[0])
end = int(r[1]) if r[1] else (size - 1)
length = end - start + 1
handler.set_header('Content-Length', str(length))
handler.set_header('Content-Range', 'bytes %s-%s/%s' % (start, end, size))
with open(path, 'rb') as fd:
fd.seek(start)
handler.write(fd.read(length))
else:
handler.set_header('Content-Length', str(size))
with open(path, 'rb') as fd:
handler.write(fd.read())
else:
handler.set_header('Content-Length', str(size))
return
class FileHandler(OMLHandler):
def initialize(self, attachment=False):
self._attachment = attachment
def head(self, id):
self.get(id, include_body=False)
def get(self, id, include_body=True):
with db.session():
item = Item.get(id)
path = item.get_path() if item else None
if not item or not path:
self.set_status(404)
return
mimetype = {
'cbr': 'application/x-cbr',
'cbz': 'application/x-cbz',
'epub': 'application/epub+zip',
'pdf': 'application/pdf',
'txt': 'text/plain',
}.get(path.split('.')[-1], None)
if mimetype == 'text/plain':
try:
open(path, 'rb').read().decode('utf-8')
mimetype = 'text/plain; charset=utf-8'
except:
mimetype = 'text/plain; charset=latin-1'
if self._attachment:
disposition = os.path.basename(path)
else:
disposition = None
return serve_static(self, path, mimetype, include_body, disposition=disposition)
class ReaderHandler(OMLHandler):
def get(self, id):
with db.session():
item = Item.get(id)
if not item:
self.set_status(404)
return
if item.info['extension'] in ('cbr', 'cbz'):
html = 'html/cbr.html'
elif item.info['extension'] == 'epub':
html = 'html/epub.html'
elif item.info['extension'] == 'pdf':
html = 'html/pdf.html'
elif item.info['extension'] == 'txt':
html = 'html/txt.html'
else:
self.set_status(404)
return
item.accessed = datetime.utcnow()
item.timesaccessed = (item.timesaccessed or 0) + 1
item.update_sort()
item.save()
path = os.path.join(settings.static_path, html)
return serve_static(self, path, 'text/html')
class UploadHandler(OMLHandler):
executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
def initialize(self, context=None):
self._context = context
def get(self):
self.write('use POST')
@run_on_executor
def save_files(self, request):
listname = request.arguments.get('list', None)
if listname:
listname = listname[0]
if isinstance(listname, bytes):
listname = listname.decode('utf-8')
with self._context():
prefs = settings.preferences
ids = []
for upload in request.files.get('files', []):
filename = upload.filename
id = get_id(data=upload.body)
ids.append(id)
file = File.get(id)
if not file or not os.path.exists(file.fullpath()):
logger.debug('add %s to library', id)
prefix_books = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books' + os.sep)
prefix_imported = os.path.join(prefix_books, '.import' + os.sep)
ox.makedirs(prefix_imported)
import_name = os.path.join(prefix_imported, filename)
n = 1
while os.path.exists(import_name):
n += 1
name, extension = filename.rsplit('.', 1)
if extension == 'kepub':
extension = 'epub'
import_name = os.path.join(prefix_imported, '%s [%d].%s' % (name, n, extension))
with open(import_name, 'wb') as fd:
fd.write(upload.body)
file = add_file(id, import_name, prefix_books)
file.move()
else:
user = state.user()
if not file.item:
item = Item.get_or_create(id=file.sha1, info=file.info)
file.item_id = item.id
state.db.session.add(file)
state.db.session.commit()
else:
item = file.item
if user not in item.users:
logger.debug('add %s to local user', id)
item.add_user(user)
add_record('additem', item.id, file.info)
add_record('edititem', item.id, item.meta)
item.update()
if listname and ids:
list_ = List.get(settings.USER_ID, listname)
if list_:
list_.add_items(ids)
response = json_response({'ids': ids})
return response
@tornado.gen.coroutine
def post(self):
if 'origin' in self.request.headers and self.request.host not in self.request.headers['origin']:
logger.debug('reject cross site attempt to access api %s', self.request)
self.set_status(403)
self.write('')
return
response = yield self.save_files(self.request)
if 'status' not in response:
response = json_response(response)
response = json_dumps(response)
self.set_header('Content-Type', 'application/json')
self.write(response)