Allow user files to be included in snippets.

master
Pacman Ghost 5 years ago
parent fbdcd70710
commit 360ca1941c
  1. 54
      vasl_templates/webapp/files.py
  2. 3
      vasl_templates/webapp/static/snippets.js
  3. 1
      vasl_templates/webapp/tests/fixtures/user-files/amp=& ; plus=+.txt
  4. 1
      vasl_templates/webapp/tests/fixtures/user-files/hello.txt
  5. BIN
      vasl_templates/webapp/tests/fixtures/user-files/subdir/placeholder.png
  6. 13
      vasl_templates/webapp/tests/remote.py
  7. 198
      vasl_templates/webapp/tests/test_files.py
  8. 3
      vasl_templates/webapp/tests/utils.py
  9. 25
      vasl_templates/webapp/vo_notes.py

@ -2,8 +2,11 @@
import os
import io
import urllib.request
import urllib.parse
import mimetypes
from flask import send_file, jsonify, redirect, url_for, abort
from flask import send_file, send_from_directory, jsonify, redirect, url_for, abort
from vasl_templates.webapp import app
from vasl_templates.webapp.file_server.vasl_mod import VaslMod
@ -19,20 +22,45 @@ class FileServer:
"""Serve static files."""
def __init__( self, base_dir ):
self.base_dir = os.path.abspath( base_dir )
if FileServer.is_remote_path( base_dir ):
self.base_dir = base_dir
else:
self.base_dir = os.path.abspath( base_dir )
def get_file( self, fname ):
def serve_file( self, path ):
"""Serve a file."""
if not fname:
return None
fname = os.path.join( self.base_dir, fname )
fname = os.path.abspath( fname )
if not os.path.isfile( fname ):
return None
prefix = os.path.commonpath( [ self.base_dir, fname ] )
if prefix != self.base_dir:
return None # nb: files must be sub-ordinate to the configured directory
return fname
if FileServer.is_remote_path( self.base_dir ):
url = "{}/{}".format( self.base_dir, path )
# NOTE: We download the target file and serve it ourself (instead of just redirecting)
# since VASSAL can't handle SSL :-/
resp = urllib.request.urlopen( url )
buf = io.BytesIO()
buf.write( resp.read() )
buf.seek( 0 )
mime_type = mimetypes.guess_type( url )[0]
if not mime_type:
# FUDGE! send_file() requires a MIME type, so we take a guess and hope the browser
# can figure it out if we're wrong :-/
mime_type = "image/png"
return send_file( buf, mimetype=mime_type )
else:
path = path.replace( "\\", "/" ) # nb: for Windows :-/
return send_from_directory( self.base_dir, path )
@staticmethod
def is_remote_path( path ):
"""Check if a path is referring to a remote server."""
return path.startswith( ("http://","https://") )
# ---------------------------------------------------------------------
@app.route( "/user/<path:path>" )
def get_user_file( path ):
"""Get a static file."""
dname = app.config.get( "USER_FILES_DIR" )
if not dname:
abort( 404 )
return FileServer( dname ).serve_file( path )
# ---------------------------------------------------------------------

@ -283,6 +283,9 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
return "[error: can't process template'" ;
}
// fixup any user file URL's
snippet = snippet.replace( "{{USER_FILES}}", APP_URL_BASE + "/user" ) ;
return snippet ;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -185,3 +185,16 @@ class ControlTests:
webapp_vo_notes._cached_vo_notes = None #pylint: disable=protected-access
webapp_vo_notes._vo_notes_file_server = None #pylint: disable=protected-access
return self
def _set_user_files_dir( self, dtype=None ):
"""Set the user files directory."""
if dtype == "test":
dname = os.path.join( os.path.split(__file__)[0], "fixtures/user-files" )
elif dtype and dtype.startswith( ("http://","https://") ):
dname = dtype
else:
assert dtype is None
dname = None
_logger.info( "Setting user files: %s", dname )
app.config["USER_FILES_DIR"] = dname
return self

@ -1,26 +1,200 @@
""" Test serving files. """
import os
import re
import urllib.request
import pytest
import werkzeug.exceptions
from vasl_templates.webapp.files import FileServer
from vasl_templates.webapp.tests.utils import init_webapp, find_child, wait_for_clipboard
# ---------------------------------------------------------------------
def test_file_server():
"""Test serving files."""
def test_local_file_server( webapp ):
"""Test serving files from the local file system."""
# initialize
base_dir = os.path.normpath( os.path.join( os.path.split(__file__)[0], "fixtures/file-server" ) )
file_server = FileServer( base_dir )
# do the tests
assert file_server.get_file( "1.txt" ) == os.path.join( base_dir, "1.txt" )
assert file_server.get_file( "/1.txt" ) is None
assert file_server.get_file( "unknown.txt" ) is None
assert file_server.get_file( "subdir/2.txt" ) == os.path.normpath( os.path.join( base_dir, "subdir/2.txt" ) )
assert file_server.get_file( "/subdir/2.txt" ) is None
# try access a file outside the configured directory
fname = "../new-default-scenario.json"
assert os.path.isfile( os.path.join( base_dir, fname ) )
assert file_server.get_file( fname ) is None
with webapp.test_request_context():
assert _get_response_data( file_server.serve_file( "1.txt" ) ).strip() == b"file 1"
with pytest.raises( werkzeug.exceptions.NotFound ):
_get_response_data( file_server.serve_file( "/1.txt" ) )
with pytest.raises( werkzeug.exceptions.NotFound ):
_get_response_data( file_server.serve_file( "unknown.txt" ) )
assert _get_response_data( file_server.serve_file( "subdir/2.txt" ) ).strip() == b"file 2"
with pytest.raises( werkzeug.exceptions.NotFound ):
_get_response_data( file_server.serve_file( "/subdir/2.txt" ) )
# try to get a file outside the configured directory
fname = "../new-default-scenario.json"
assert os.path.isfile( os.path.join( base_dir, fname ) )
with pytest.raises( werkzeug.exceptions.NotFound ):
_get_response_data( file_server.serve_file( fname) )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def test_remote_file_server( webapp ):
"""Test serving files from a remote file system."""
# initialize
base_url = "{}/static/images".format( _get_base_url( webapp ) )
file_server = FileServer( base_url )
base_dir = os.path.join( os.path.split(__file__)[0], "../static/images" )
def do_test( fname ):
"""Get the specified user file from the remote server and check the response."""
buf = _get_response_data( file_server.serve_file( fname ) )
with open( os.path.join( base_dir, fname ), "rb" ) as fp:
assert buf == fp.read()
# do the tests
with webapp.test_request_context():
do_test( "hint.gif" )
do_test( "flags/german.png" )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _get_base_url( webapp ):
"""Get the webapp base URL."""
url = webapp.url_for( "get_user_file", path="unused" )
mo = re.search( r"^http://.+?:\d+", url )
return mo.group()
def _get_response_data( resp ):
"""Get the data from a Flask response."""
resp.direct_passthrough = False
return resp.get_data()
# ---------------------------------------------------------------------
def test_local_user_files( webapp, webdriver ):
"""Test serving user files from the local file system."""
def do_test( enable_user_files ): #pylint: disable=missing-docstring
# initialize
init_webapp( webapp, webdriver,
reset = lambda ct:
ct.set_user_files_dir( dtype = "test" if enable_user_files else None )
)
# try getting a user file
try:
url = webapp.url_for( "get_user_file", path="hello.txt" )
resp = urllib.request.urlopen( url )
assert enable_user_files # nb: we should only get here if user files are enabled
assert resp.code == 200
assert resp.read().strip() == b"Yo, wassup!"
assert resp.headers[ "Content-Type" ].startswith( "text/plain" )
except urllib.error.HTTPError as ex:
assert not enable_user_files # nb: we should only get here if user files are disabled
assert ex.code == 404
# try getting a non-existent file (nb: should always fail, whether user files are enabled/disabled)
with pytest.raises( urllib.error.HTTPError ) as exc_info:
url = webapp.url_for( "get_user_file", path="unknown" )
resp = urllib.request.urlopen( url )
assert exc_info.value.code == 404
# try getting a file in a sub-directory
try:
url = webapp.url_for( "get_user_file", path="subdir/placeholder.png" )
resp = urllib.request.urlopen( url )
assert enable_user_files # nb: we should only get here if user files are enabled
assert resp.code == 200
assert resp.read().startswith( b"\x89PNG\r\n" )
assert resp.headers[ "Content-Type" ] == "image/png"
except urllib.error.HTTPError as ex:
assert not enable_user_files # nb: we should only get here if user files are disabled
assert ex.code == 404
# try getting a file outside the configured directory (nb: should always fail)
fname = os.path.join( os.path.split(__file__)[0], "fixtures/vasl-pieces.txt" )
assert os.path.isfile( fname )
with pytest.raises( urllib.error.HTTPError ) as exc_info:
url = webapp.url_for( "get_user_file", path="../vasl-pieces.txt" )
resp = urllib.request.urlopen( url )
assert exc_info.value.code == 404
# try getting a file with special characters in its name
try:
url = webapp.url_for( "get_user_file", path="amp=& ; plus=+.txt" )
resp = urllib.request.urlopen( url )
assert enable_user_files # nb: we should only get here if user files are enabled
assert resp.code == 200
assert resp.read().strip() == b"special chars"
assert resp.headers[ "Content-Type" ].startswith( "text/plain" )
except urllib.error.HTTPError as ex:
assert not enable_user_files # nb: we should only get here if user files are disabled
assert ex.code == 404
# do the tests with user files enabled/disabled
do_test( True )
do_test( False )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def test_remote_user_files( webapp, webdriver ):
"""Test serving user files from a remote server."""
def do_test( enable_user_files ): #pylint: disable=missing-docstring
# initialize
base_url = "{}/static/images".format( _get_base_url( webapp ) )
init_webapp( webapp, webdriver,
reset = lambda ct:
ct.set_user_files_dir( dtype = base_url if enable_user_files else None )
)
# try getting a user file
try:
url = webapp.url_for( "get_user_file", path="menu.png" )
resp = urllib.request.urlopen( url )
assert enable_user_files # nb: we should only get here if user files are enabled
assert resp.code == 200
assert resp.read().startswith( b"\x89PNG\r\n" )
assert resp.headers[ "Content-Type" ] == "image/png"
except urllib.error.HTTPError as ex:
assert not enable_user_files # nb: we should only get here if user files are disabled
assert ex.code == 404
# do the tests with user files enabled/disabled
do_test( True )
do_test( False )
# ---------------------------------------------------------------------
def test_user_file_snippets( webapp, webdriver ):
"""Test user files in snippets."""
def do_test( enable_user_files ): #pylint: disable=missing-docstring
# initialize
init_webapp( webapp, webdriver,
reset = lambda ct: ct.set_user_files_dir( dtype = "test" if enable_user_files else None )
)
# set the victory conditions
elem = find_child( "textarea[name='VICTORY_CONDITIONS']" )
elem.send_keys( "my image: {{USER_FILES}}/subdir/placeholder.png" )
btn = find_child( "button.generate[data-id='victory_conditions']" )
btn.click()
def get_user_file_url( clipboard ): #pylint: disable=missing-docstring
# nb: the test template wraps {{VICTORY_CONDITIONS}} in square brackets :-/
mo = re.search( r"http://.+?/([^]]+)", clipboard )
return "/" + mo.group(1)
wait_for_clipboard( 2, "/user/subdir/placeholder.png", transform=get_user_file_url )
# do the tests with user files enabled/disabled
# NOTE: The user file URL will be inserted into the snippet even if user files are disabled,
# but the URL will 404 when somebody tries to resolve it.
do_test( True )
do_test( False )

@ -57,7 +57,8 @@ def init_webapp( webapp, webdriver, **options ):
.set_default_template_pack( dname=None ) \
.set_vasl_mod( vmod=None ) \
.set_vassal_engine( vengine=None ) \
.set_vo_notes_dir( dtype=None )
.set_vo_notes_dir( dtype=None ) \
.set_user_files_dir( dtype=None )
if "reset" in options:
options.pop( "reset" )( control_tests )

@ -124,28 +124,29 @@ def get_vo_note( vo_type, nat, key ):
abort( 404 )
vo_notes = _do_get_vo_notes( vo_type )
fname = vo_notes.get( nat, {} ).get( key )
fname = _vo_notes_file_server.get_file( fname )
if not fname:
abort( 404 )
resp = _vo_notes_file_server.serve_file( fname )
# check if we should resize the file
scaling = request.args.get( "scaling" ) # nb: allow individual notes to set their scaling
if not scaling:
scaling = app.config.get( "CHAPTER_H_NOTE_SCALING", 100 )
if scaling != 100:
if scaling == 100:
# nope - just return the file as it is
return resp
else:
# yup - make it so
with open( fname, "rb" ) as fp:
img = Image.open( fp )
width = int( img.size[0] * float(scaling) / 100 )
height = int( img.size[1] * float(scaling) / 100 )
img = img.resize( (width,height), Image.ANTIALIAS )
buf = io.BytesIO()
resp.direct_passthrough = False
buf.write( resp.get_data() )
buf.seek( 0 )
img = Image.open( buf )
width = int( img.size[0] * float(scaling) / 100 )
height = int( img.size[1] * float(scaling) / 100 )
img = img.resize( (width,height), Image.ANTIALIAS )
buf = io.BytesIO()
img.save( buf, format="PNG" )
buf.seek( 0 )
return send_file( buf, mimetype="image/png" )
else:
# nope - just return the file as it is
return send_file( fname )
# ---------------------------------------------------------------------

Loading…
Cancel
Save