Improve performance of the export bookmark service.

Avoid temporary copies in memory of the Bookmark and Tag instances by returning an iterable from services.exporter.export_netscape_html.
This commit is contained in:
Fernando Costa Bertoldi 2024-11-20 13:40:00 -03:00
parent c3149409b0
commit ced4968522
4 changed files with 34 additions and 36 deletions

View file

@ -1,33 +1,30 @@
import html
from typing import List
import itertools
from typing import Iterable
from bookmarks.models import Bookmark
BookmarkDocument = List[str]
def export_netscape_html(bookmarks: Iterable[Bookmark]):
def _append_bookmarks():
for bookmark in bookmarks:
yield from append_bookmark(bookmark)
return itertools.chain(append_header(), append_list_start(), _append_bookmarks(), append_list_end())
def export_netscape_html(bookmarks: List[Bookmark]):
doc = []
append_header(doc)
append_list_start(doc)
[append_bookmark(doc, bookmark) for bookmark in bookmarks]
append_list_end(doc)
return "\n\r".join(doc)
def append_header():
yield "<!DOCTYPE NETSCAPE-Bookmark-file-1>"
yield '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">'
yield "<TITLE>Bookmarks</TITLE>"
yield "<H1>Bookmarks</H1>"
def append_header(doc: BookmarkDocument):
doc.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>")
doc.append('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">')
doc.append("<TITLE>Bookmarks</TITLE>")
doc.append("<H1>Bookmarks</H1>")
def append_list_start():
yield "<DL><p>"
def append_list_start(doc: BookmarkDocument):
doc.append("<DL><p>")
def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
def append_bookmark(bookmark: Bookmark):
url = bookmark.url
title = html.escape(bookmark.resolved_title or "")
desc = html.escape(bookmark.resolved_description or "")
@ -42,13 +39,12 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
added = int(bookmark.date_added.timestamp())
modified = int(bookmark.date_modified.timestamp())
doc.append(
f'<DT><A HREF="{url}" ADD_DATE="{added}" LAST_MODIFIED="{modified}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
)
yield f'<DT><A HREF="{url}" ADD_DATE="{added}" LAST_MODIFIED="{modified}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
if desc:
doc.append(f"<DD>{desc}")
yield f"<DD>{desc}"
def append_list_end(doc: BookmarkDocument):
doc.append("</DL><p>")
def append_list_end():
yield "</DL><p>"

View file

@ -74,7 +74,7 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
is_archived=True,
),
]
html = exporter.export_netscape_html(bookmarks)
html = "\r\n".join(exporter.export_netscape_html(bookmarks))
lines = [
'<DT><A HREF="https://example.com/1" ADD_DATE="1" LAST_MODIFIED="11" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>',
@ -89,7 +89,7 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
'<DT><A HREF="https://example.com/7" ADD_DATE="7" LAST_MODIFIED="77" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
'<DT><A HREF="https://example.com/8" ADD_DATE="8" LAST_MODIFIED="88" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
]
self.assertIn("\n\r".join(lines), html)
self.assertIn("\r\n".join(lines), html)
def test_escape_html(self):
bookmark = self.setup_bookmark(
@ -97,7 +97,7 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
description="The <style> HTML element contains style information for a document, or part of a document.",
notes="Interesting notes about the <style> HTML element.",
)
html = exporter.export_netscape_html([bookmark])
html = "\r\n".join(exporter.export_netscape_html([bookmark]))
self.assertIn("&lt;style&gt;: The Style Information element", html)
self.assertIn(
@ -111,4 +111,4 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
bookmark.title = ""
bookmark.description = ""
bookmark.save()
exporter.export_netscape_html([bookmark])
"\r\n".join(exporter.export_netscape_html([bookmark]))

View file

@ -7,7 +7,6 @@ from django.conf import settings as django_settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.db.models import prefetch_related_objects
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render
from django.urls import reverse
@ -233,14 +232,17 @@ def bookmark_import(request):
def bookmark_export(request):
# noinspection PyBroadException
try:
bookmarks = Bookmark.objects.filter(owner=request.user)
# Prefetch tags to prevent n+1 queries
prefetch_related_objects(bookmarks, "tags")
bookmarks = Bookmark.objects.filter(owner=request.user).prefetch_related("tags")
file_content = exporter.export_netscape_html(bookmarks)
def _newline_appended():
for line in file_content:
yield line
yield "\r\n"
response = HttpResponse(content_type="text/plain; charset=UTF-8")
response["Content-Disposition"] = 'attachment; filename="bookmarks.html"'
response.write(file_content)
response.writelines(_newline_appended())
return response
except:

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "linkding",
"version": "1.35.0",
"version": "1.36.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "linkding",
"version": "1.35.0",
"version": "1.36.0",
"license": "MIT",
"dependencies": {
"@hotwired/turbo": "^8.0.6",