2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-04 18:40:55 +00:00

Add option to "asset" tag to control error raising (#11591)

This commit is contained in:
Oliver
2026-03-22 17:15:14 +11:00
committed by GitHub
parent 0feba9fbfb
commit 1e0a0aa79d
2 changed files with 40 additions and 12 deletions

View File

@@ -188,20 +188,20 @@ def static_file_exists(path: Path | str) -> bool:
def get_static_file_contents( def get_static_file_contents(
path: Path | str, raise_error: bool = True path: Path | str, raise_error: bool = False
) -> bytes | None: ) -> bytes | None:
"""Return the contents of a static file. """Return the contents of a static file.
Arguments: Arguments:
path: The path to the static file, relative to the static storage root path: The path to the static file, relative to the static storage root
raise_error: If True, raise an error if the file cannot be found (default = True) raise_error: If True, raise an error if the file cannot be found (default = False)
Returns: Returns:
The contents of the static file, or None if the file cannot be found The contents of the static file, or None if the file cannot be found
""" """
if not path: if not path:
if raise_error: if raise_error:
raise ValueError('No media file specified') raise ValueError('No static file specified')
else: else:
return None return None
@@ -217,12 +217,14 @@ def get_static_file_contents(
return file_data return file_data
def get_media_file_contents(path: Path | str, raise_error: bool = True) -> bytes | None: def get_media_file_contents(
path: Path | str, raise_error: bool = False
) -> bytes | None:
"""Return the fully qualified file path to an uploaded media file. """Return the fully qualified file path to an uploaded media file.
Arguments: Arguments:
path: The path to the media file, relative to the media storage root path: The path to the media file, relative to the media storage root
raise_error: If True, raise an error if the file cannot be found (default = True) raise_error: If True, raise an error if the file cannot be found (default = False)
Returns: Returns:
The contents of the media file, or None if the file cannot be found The contents of the media file, or None if the file cannot be found
@@ -255,15 +257,24 @@ def get_media_file_contents(path: Path | str, raise_error: bool = True) -> bytes
@register.simple_tag() @register.simple_tag()
def asset(filename): def asset(filename: str, raise_error: bool = False) -> str | None:
"""Return fully-qualified path for an upload report asset file. """Return fully-qualified path for an upload report asset file.
Arguments: Arguments:
filename: Asset filename (relative to the 'assets' media directory) filename: Asset filename (relative to the 'assets' media directory)
raise_error: If True, raise an error if the file cannot be found (default = False)
Raises: Raises:
FileNotFoundError: If file does not exist FileNotFoundError: If file does not exist
ValueError: If an invalid filename is provided (e.g. empty string)
ValidationError: If the filename is invalid (e.g. path traversal attempt)
""" """
if not filename:
if raise_error:
raise ValueError('No asset file specified')
else:
return None
if type(filename) is SafeString: if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness' # Prepend an empty string to enforce 'stringiness'
filename = '' + filename filename = '' + filename
@@ -274,7 +285,10 @@ def asset(filename):
full_path = Path('report', 'assets', filename) full_path = Path('report', 'assets', filename)
if not media_file_exists(full_path): if not media_file_exists(full_path):
raise FileNotFoundError(_('Asset file not found') + f": '{filename}'") if raise_error:
raise FileNotFoundError(_('Asset file not found') + f": '{filename}'")
else:
return None
# In debug mode, return a web URL to the asset file (rather than a local file path) # In debug mode, return a web URL to the asset file (rather than a local file path)
if get_global_setting('REPORT_DEBUG_MODE', cache=False): if get_global_setting('REPORT_DEBUG_MODE', cache=False):
@@ -294,6 +308,7 @@ def uploaded_image(
width: Optional[int] = None, width: Optional[int] = None,
height: Optional[int] = None, height: Optional[int] = None,
rotate: Optional[float] = None, rotate: Optional[float] = None,
raise_error: bool = False,
**kwargs, **kwargs,
) -> str: ) -> str:
"""Return raw image data from an 'uploaded' image. """Return raw image data from an 'uploaded' image.
@@ -306,12 +321,14 @@ def uploaded_image(
width: Optional width of the image width: Optional width of the image
height: Optional height of the image height: Optional height of the image
rotate: Optional rotation to apply to the image rotate: Optional rotation to apply to the image
raise_error: If True, raise an error if the file cannot be found (default = False)
Returns: Returns:
Binary image data to be rendered directly in a <img> tag Binary image data to be rendered directly in a <img> tag
Raises: Raises:
FileNotFoundError: If the file does not exist FileNotFoundError: If the file does not exist
ValueError: If an invalid filename is provided (e.g. empty string)
""" """
if type(filename) is SafeString: if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness' # Prepend an empty string to enforce 'stringiness'
@@ -330,7 +347,7 @@ def uploaded_image(
raise FileNotFoundError(_('Image file not found') + f": '{filename}'") raise FileNotFoundError(_('Image file not found') + f": '{filename}'")
if exists: if exists:
img_data = get_media_file_contents(filename, raise_error=False) img_data = get_media_file_contents(filename, raise_error=raise_error)
# Check if the image data is valid # Check if the image data is valid
if ( if (
@@ -344,7 +361,9 @@ def uploaded_image(
else: else:
# Load the backup image from the static files directory # Load the backup image from the static files directory
replacement_file_path = Path('img', replacement_file) replacement_file_path = Path('img', replacement_file)
img_data = get_static_file_contents(replacement_file_path) img_data = get_static_file_contents(
replacement_file_path, raise_error=raise_error
)
if debug_mode: if debug_mode:
# In debug mode, return a web path (rather than an encoded image blob) # In debug mode, return a web path (rather than an encoded image blob)

View File

@@ -61,7 +61,15 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
self.debug_mode(b) self.debug_mode(b)
with self.assertRaises(FileNotFoundError): with self.assertRaises(FileNotFoundError):
report_tags.asset('bad_file.txt') report_tags.asset('bad_file.txt', raise_error=True)
# Test for missing file, no error
self.assertIsNone(report_tags.asset('missing.txt'))
self.assertIsNone(report_tags.asset(''))
with self.assertRaises(ValueError):
report_tags.asset('', raise_error=True)
# Create an asset file # Create an asset file
asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets') asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets')
@@ -80,6 +88,7 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
self.debug_mode(False) self.debug_mode(False)
asset = report_tags.asset('test.txt') asset = report_tags.asset('test.txt')
self.assertEqual(asset, f'file://{settings.MEDIA_ROOT}/report/assets/test.txt')
# Test for attempted path traversal # Test for attempted path traversal
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@@ -92,10 +101,10 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
self.assertFalse(report_tags.static_file_exists(fn)) self.assertFalse(report_tags.static_file_exists(fn))
with self.assertRaises(FileNotFoundError): with self.assertRaises(FileNotFoundError):
report_tags.get_media_file_contents('dummy_file.txt') report_tags.get_media_file_contents('dummy_file.txt', raise_error=True)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
report_tags.get_static_file_contents(None) report_tags.get_static_file_contents(None, raise_error=True)
# Try again, without throwing an error # Try again, without throwing an error
self.assertIsNone( self.assertIsNone(