Skip to content

[ENG-7607][ENG-8357] Feature/unoserver integration #388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: feature/buff-worms
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
FROM python:3.13-slim

# ensure unoconv can locate the uno library
ENV PYTHONPATH=/usr/lib/python3/dist-packages

RUN usermod -d /home www-data \
&& chown www-data:www-data /home \
Expand Down
42 changes: 23 additions & 19 deletions mfr/extensions/unoconv/export.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
from http import HTTPStatus
from os.path import basename, splitext
from subprocess import run, CalledProcessError

from unoserver.client import UnoClient
from mfr.core.extension import BaseExporter
from mfr.core.exceptions import SubprocessError
from mfr.extensions.unoconv.settings import (PORT,
ADDRESS,
UNOCONV_BIN,
UNOCONV_TIMEOUT)
from mfr.extensions.unoconv.settings import (
UNOSERVER_HOST,
UNOSERVER_PORT,
UNOSERVER_TIMEOUT,
)


class UnoconvExporter(BaseExporter):
"""
Uses UnoClient (unoserver) to convert the source file into the requested format.
"""

def export(self):
client = UnoClient(
server=UNOSERVER_HOST,
port=int(UNOSERVER_PORT),
)
try:
run([
UNOCONV_BIN,
'-n',
'-c', f'socket,host={ADDRESS},port={PORT};urp;StarOffice.ComponentContext',
'-f', self.format,
'-o', self.output_file_path,
'-vvv',
self.source_file_path
], check=True, timeout=UNOCONV_TIMEOUT)
except CalledProcessError as err:
client.convert(
inpath=self.source_file_path,
outpath=self.output_file_path,
convert_to=self.format,
)
except Exception as err:
name, extension = splitext(basename(self.source_file_path))
raise SubprocessError(
'Unable to export the file in the requested format, please try again later.',
process='unoconv',
cmd=str(err.cmd),
returncode=err.returncode,
process='unoserver',
cmd=str(err),
returncode=None,
path=str(self.source_file_path),
code=HTTPStatus.BAD_REQUEST,
extension=extension or '',
exporter_class='unoconv',
exporter_class='unoserver',
)
3 changes: 1 addition & 2 deletions mfr/extensions/unoconv/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def __init__(self, metadata, file_path, url, assets_url, export_url):
export_url
)

# can't call file_required until renderer is built
self.renderer_metrics.add('file_required', self.file_required)
self.renderer_metrics.add('cache_result', self.cache_result)

Expand Down Expand Up @@ -63,7 +62,7 @@ def render(self):
try:
os.remove(self.export_file_path)
except FileNotFoundError:
pass
logger.warning(f"[render] Export file not found for cleanup: {self.export_file_path}")

return rendition

Expand Down
11 changes: 4 additions & 7 deletions mfr/extensions/unoconv/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

from mfr import settings

config = settings.child('UNOSERVER_EXTENSION_CONFIG')

config = settings.child('UNOCONV_EXTENSION_CONFIG')

UNOCONV_BIN = config.get('UNOCONV_BIN', '/usr/local/bin/unoconv')
UNOCONV_TIMEOUT = int(config.get('UNOCONV_TIMEOUT', 60))

ADDRESS = config.get('SERVER', os.environ.get('UNOCONV_PORT_2002_TCP_ADDR', '127.0.0.1'))
PORT = config.get('PORT', os.environ.get('UNOCONV_PORT_2002_TCP_PORT', '2002'))
UNOSERVER_TIMEOUT = int(config.get('TIMEOUT', 60))

UNOSERVER_HOST = config.get( 'HOST',os.environ.get('UNOSERVER_INTERFACE', '127.0.0.1'))
UNOSERVER_PORT = config.get('PORT',os.environ.get('UNOSERVER_PORT', '2003'))
DEFAULT_RENDER = {'renderer': '.pdf', 'format': 'pdf'}

RENDER_MAP = config.get_object('RENDER_MAP', {
Expand Down
2 changes: 1 addition & 1 deletion mfr/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def make_app(debug):
app = tornado.web.Application(
[
(r'/static/(.*)', tornado.web.StaticFileHandler, {'path': server_settings.STATIC_PATH}),
(r'/assets/(.*?)/(.*\..*)', ExtensionsStaticFileHandler),
(r'/assets/(?P<module>[^/]+)/(?P<path>.*)', ExtensionsStaticFileHandler),
(r'/export', ExportHandler),
(r'/exporters', ExportersHandler),
(r'/render', RenderHandler),
Expand Down
49 changes: 33 additions & 16 deletions mfr/server/handlers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import uuid
import asyncio
import logging
import pathlib

from importlib.metadata import entry_points

import tornado.web
Expand Down Expand Up @@ -296,30 +298,45 @@ class ExtensionsStaticFileHandler(tornado.web.StaticFileHandler, CorsMixin):

def initialize(self):
# Todo: the method args differ in comparison with StaticFileHandler
namespace = 'mfr.renderers'
module_path = 'mfr.extensions'

namespace = "mfr.renderers"
module_path_prefix = "mfr.extensions"
self.modules = {}

for ep in entry_points().select(group=namespace):
module_name = ep.value.split(":")[0] # replacement for ep.module_name
module = module_name.replace(module_path + ".", "").split(".")[0]
dist_location = ep.dist.locate_file("") # replacement for ep.dist.location
static_path = os.path.join(dist_location, 'mfr', 'extensions', module, 'static')
self.modules[module] = static_path

async def get(self, module_name, path):
# Todo: the method args differ in comparison with StaticFileHandler, maybe it is ok
fq_mod = ep.value.split(":")[0]
module = fq_mod.replace(f"{module_path_prefix}.", "").split(".")[0]
root = pathlib.Path(ep.dist.locate_file(""))
static_dir = root / "mfr" / "extensions" / module / "static"
if static_dir.is_dir():
self.modules[module] = static_dir.as_posix()
logger.debug(f" ↳ EP {module}: {static_dir}")

repo_root = pathlib.Path(__file__).resolve().parents[2]
ext_root = repo_root / "extensions"
if not ext_root.is_dir():
ext_root = repo_root.parent / "mfr" / "extensions"

for module_path in ext_root.iterdir():
static_dir = module_path / "static"
if static_dir.is_dir():
self.modules.setdefault(module_path.name, static_dir.as_posix())
logger.debug(f" ↳ FS {module_path.name}: {static_dir}")


async def get(self, module: str, path: str):
root = self.modules.get(module)
if not root:
self.set_status(404)
return

try:
super().initialize(self.modules[module_name])
super().initialize(root)
return await super().get(path)
except Exception:
# Todo: maybe it is needed to use logger and specific message for exception logging
except Exception as e:
self.set_status(404)

try:
super().initialize(settings.STATIC_PATH)
return await super().get(path)
except Exception:
# Todo: maybe it is needed to use logger and specific message for exception logging
except Exception as e:
self.set_status(404)
Loading
Loading