From a8aa2c8457dc6b33a1ebd2173f9da2fb053f0d3f Mon Sep 17 00:00:00 2001
From: Dan Granville <dan@blipcreative.com>
Date: Fri, 11 Aug 2023 14:23:05 +0100
Subject: [PATCH] txt-preview: improvements (#180)

* enable horizontal scrolling
* add option to truncate .txt file preview after PREVIEWER_TXT_MAX_BYTES
* avoid possible invalid encoding errors

Co-authored-by: Guillaume Viger <fenekku@fenekku.com>
---
 .../scss/invenio_previewer/txt.scss            |  4 ++++
 invenio_previewer/config.py                    |  3 +++
 invenio_previewer/extensions/txt.py            |  8 +++++---
 .../semantic-ui/invenio_previewer/txt.html     | 11 +++++------
 invenio_previewer/webpack.py                   |  1 +
 tests/test_macros.py                           | 18 ++++++++++++++++++
 6 files changed, 36 insertions(+), 9 deletions(-)
 create mode 100644 invenio_previewer/assets/semantic-ui/scss/invenio_previewer/txt.scss

diff --git a/invenio_previewer/assets/semantic-ui/scss/invenio_previewer/txt.scss b/invenio_previewer/assets/semantic-ui/scss/invenio_previewer/txt.scss
new file mode 100644
index 0000000..61111bc
--- /dev/null
+++ b/invenio_previewer/assets/semantic-ui/scss/invenio_previewer/txt.scss
@@ -0,0 +1,4 @@
+#text-previewer {
+  padding: 0.5em;
+}
+
diff --git a/invenio_previewer/config.py b/invenio_previewer/config.py
index d88c397..b962a91 100644
--- a/invenio_previewer/config.py
+++ b/invenio_previewer/config.py
@@ -26,6 +26,9 @@ PREVIEWER_MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024
 PREVIEWER_MAX_IMAGE_SIZE_BYTES = 0.5 * 1024 * 1024
 """Maximum file size in bytes for image files."""
 
+PREVIEWER_TXT_MAX_BYTES = 1 * 1024 * 1024
+"""Maximum number of .txt file bytes to preview before truncated."""
+
 PREVIEWER_ZIP_MAX_FILES = 1000
 """Max number of files showed in the ZIP previewer."""
 
diff --git a/invenio_previewer/extensions/txt.py b/invenio_previewer/extensions/txt.py
index 7739ad5..2df9744 100644
--- a/invenio_previewer/extensions/txt.py
+++ b/invenio_previewer/extensions/txt.py
@@ -8,19 +8,20 @@
 
 """Text rendering."""
 
-from flask import render_template
+from flask import current_app, render_template
 
 from ..proxies import current_previewer
 from ..utils import detect_encoding
 
 previewable_extensions = ["txt"]
+max_bytes = current_app.config.get('PREVIEWER_TXT_MAX_BYTES', -1)
 
 
 def render(file):
     """Render HTML from txt file content."""
     with file.open() as fp:
         encoding = detect_encoding(fp, default="utf-8")
-        return fp.read().decode(encoding)
+        return fp.read(max_bytes).decode(encoding, errors="ignore")
 
 
 def can_preview(file):
@@ -37,5 +38,6 @@ def preview(file):
         file=file,
         content=render(file),
         js_bundles=current_previewer.js_bundles,
-        css_bundles=current_previewer.css_bundles,
+        css_bundles=['txt_css.css'],
+        truncated=max_bytes < file.size,
     )
diff --git a/invenio_previewer/templates/semantic-ui/invenio_previewer/txt.html b/invenio_previewer/templates/semantic-ui/invenio_previewer/txt.html
index 7587a59..e398363 100644
--- a/invenio_previewer/templates/semantic-ui/invenio_previewer/txt.html
+++ b/invenio_previewer/templates/semantic-ui/invenio_previewer/txt.html
@@ -10,11 +10,10 @@
 {%- extends config.PREVIEWER_ABSTRACT_TEMPLATE %}
 
 {% block panel %}
-<div class="ui container">
-    <div class="ui grid column">
-        <div class="column">
-            <pre>{{ content }}</pre>
-        </div>
-    </div>
+<div id="text-previewer">
+    <pre>{{ content }}</pre>
+  {% if truncated %}
+    <hr /><div class="banner">{{ _('Preview of large file truncated') }}</div>
+  {% endif %}
 </div>
 {% endblock %}
diff --git a/invenio_previewer/webpack.py b/invenio_previewer/webpack.py
index a46b68a..6600551 100644
--- a/invenio_previewer/webpack.py
+++ b/invenio_previewer/webpack.py
@@ -75,6 +75,7 @@ previewer = WebpackThemeBundle(
                 'bottom_css': './scss/invenio_previewer/bottom.scss',
                 'simple_image_css':
                     './scss/invenio_previewer/simple_image.scss',
+                'txt_css': './scss/invenio_previewer/txt.scss',
             },
             dependencies={
                 'd3': '^3.5.17',
diff --git a/tests/test_macros.py b/tests/test_macros.py
index 6ff5548..c265b2d 100644
--- a/tests/test_macros.py
+++ b/tests/test_macros.py
@@ -232,6 +232,24 @@ def test_simple_image_extension(app, webassets, record):
         assert 'class="previewer-simple-image"' in res.get_data(as_text=True)
 
 
+def test_txt_extension(app, webassets, record):
+    """Text .txt file viewer."""
+    create_file(record, 'test1.txt', BytesIO(b'test content foobar'))
+
+    with app.test_client() as client:
+        res = client.get(preview_url(record['control_number'], 'test1.txt'))
+        assert "<pre>test content foobar</pre>" in res.get_data(as_text=True)
+
+    max_file_size = app.config.get(
+        'PREVIEWER_TXT_MAX_BYTES', 1 * 1024 * 1024)
+    too_large_string = '1' * (max_file_size + 1)
+    create_file(record, 'test2.txt', BytesIO(b(too_large_string)))
+
+    with app.test_client() as client:
+        res = client.get(preview_url(record['control_number'], 'test1.txt'))
+        assert "file truncated" in res.get_data(as_text=True)
+
+
 def test_view_macro_file_list(app):
     """Test file list macro."""
     with app.test_request_context():
-- 
GitLab