mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 14:09:26 +02:00
Add REST endpoint for uploading snapshots from the Singlefile extension (#996)
* Extract asset logic * Allow disabling HTML snapshot when creating bookmark * Add endpoint for uploading singlefile snapshots * Add URL parameter to disable HTML snapshots * Allow using asset list in base Docker image * Expose app version through profile
This commit is contained in:
128
bookmarks/services/assets.py
Normal file
128
bookmarks/services/assets.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import gzip
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.utils import timezone, formats
|
||||
|
||||
from bookmarks.models import Bookmark, BookmarkAsset
|
||||
from bookmarks.services import singlefile
|
||||
|
||||
MAX_ASSET_FILENAME_LENGTH = 192
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_snapshot_asset(bookmark: Bookmark) -> BookmarkAsset:
|
||||
date_created = timezone.now()
|
||||
timestamp = formats.date_format(date_created, "SHORT_DATE_FORMAT")
|
||||
asset = BookmarkAsset(
|
||||
bookmark=bookmark,
|
||||
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
|
||||
date_created=date_created,
|
||||
content_type=BookmarkAsset.CONTENT_TYPE_HTML,
|
||||
display_name=f"HTML snapshot from {timestamp}",
|
||||
status=BookmarkAsset.STATUS_PENDING,
|
||||
)
|
||||
return asset
|
||||
|
||||
|
||||
def create_snapshot(asset: BookmarkAsset):
|
||||
try:
|
||||
# Create snapshot into temporary file
|
||||
temp_filename = _generate_asset_filename(asset, asset.bookmark.url, "tmp")
|
||||
temp_filepath = os.path.join(settings.LD_ASSET_FOLDER, temp_filename)
|
||||
singlefile.create_snapshot(asset.bookmark.url, temp_filepath)
|
||||
|
||||
# Store as gzip in asset folder
|
||||
filename = _generate_asset_filename(asset, asset.bookmark.url, "html.gz")
|
||||
filepath = os.path.join(settings.LD_ASSET_FOLDER, filename)
|
||||
with open(temp_filepath, "rb") as temp_file, gzip.open(
|
||||
filepath, "wb"
|
||||
) as gz_file:
|
||||
shutil.copyfileobj(temp_file, gz_file)
|
||||
|
||||
# Remove temporary file
|
||||
os.remove(temp_filepath)
|
||||
|
||||
asset.status = BookmarkAsset.STATUS_COMPLETE
|
||||
asset.file = filename
|
||||
asset.gzip = True
|
||||
asset.save()
|
||||
except Exception as error:
|
||||
asset.status = BookmarkAsset.STATUS_FAILURE
|
||||
asset.save()
|
||||
raise error
|
||||
|
||||
|
||||
def upload_snapshot(bookmark: Bookmark, html: bytes):
|
||||
asset = create_snapshot_asset(bookmark)
|
||||
filename = _generate_asset_filename(asset, asset.bookmark.url, "html.gz")
|
||||
filepath = os.path.join(settings.LD_ASSET_FOLDER, filename)
|
||||
|
||||
with gzip.open(filepath, "wb") as gz_file:
|
||||
gz_file.write(html)
|
||||
|
||||
# Only save the asset if the file was written successfully
|
||||
asset.status = BookmarkAsset.STATUS_COMPLETE
|
||||
asset.file = filename
|
||||
asset.gzip = True
|
||||
asset.save()
|
||||
|
||||
return asset
|
||||
|
||||
|
||||
def upload_asset(bookmark: Bookmark, upload_file: UploadedFile):
|
||||
try:
|
||||
asset = BookmarkAsset(
|
||||
bookmark=bookmark,
|
||||
asset_type=BookmarkAsset.TYPE_UPLOAD,
|
||||
date_created=timezone.now(),
|
||||
content_type=upload_file.content_type,
|
||||
display_name=upload_file.name,
|
||||
status=BookmarkAsset.STATUS_COMPLETE,
|
||||
gzip=False,
|
||||
)
|
||||
name, extension = os.path.splitext(upload_file.name)
|
||||
filename = _generate_asset_filename(asset, name, extension.lstrip("."))
|
||||
filepath = os.path.join(settings.LD_ASSET_FOLDER, filename)
|
||||
with open(filepath, "wb") as f:
|
||||
for chunk in upload_file.chunks():
|
||||
f.write(chunk)
|
||||
asset.file = filename
|
||||
asset.file_size = upload_file.size
|
||||
asset.save()
|
||||
logger.info(
|
||||
f"Successfully uploaded asset file. bookmark={bookmark} file={upload_file.name}"
|
||||
)
|
||||
return asset
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to upload asset file. bookmark={bookmark} file={upload_file.name}",
|
||||
exc_info=e,
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def _generate_asset_filename(
|
||||
asset: BookmarkAsset, filename: str, extension: str
|
||||
) -> str:
|
||||
def sanitize_char(char):
|
||||
if char.isalnum() or char in ("-", "_", "."):
|
||||
return char
|
||||
else:
|
||||
return "_"
|
||||
|
||||
formatted_datetime = asset.date_created.strftime("%Y-%m-%d_%H%M%S")
|
||||
sanitized_filename = "".join(sanitize_char(char) for char in filename)
|
||||
|
||||
# Calculate the length of fixed parts of the final filename
|
||||
non_filename_length = len(f"{asset.asset_type}_{formatted_datetime}_.{extension}")
|
||||
# Calculate the maximum length for the dynamic part of the filename
|
||||
max_filename_length = MAX_ASSET_FILENAME_LENGTH - non_filename_length
|
||||
# Truncate the filename if necessary
|
||||
sanitized_filename = sanitized_filename[:max_filename_length]
|
||||
|
||||
return f"{asset.asset_type}_{formatted_datetime}_{sanitized_filename}.{extension}"
|
Reference in New Issue
Block a user