mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-13 21:49:26 +02:00
Archive snapshots of websites locally (#672)
* Add basic HTML snapshots * Implement asset list * Add snapshot creation tests * Add deletion tests * Show file size * Remove snapshots * Create new snapshots * Switch to single-file * CSS tweak * Remove auto refresh * Show delete link when there is no file yet * Add current date to display name * Add flag for snapshot support * Add option for disabling automatic snapshots * Make snapshots sharable * Document image variants * Update README.md * Add migrations * Fix tests
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from .assets import *
|
||||
from .bookmarks import *
|
||||
from .settings import *
|
||||
from .toasts import *
|
||||
|
43
bookmarks/views/assets.py
Normal file
43
bookmarks/views/assets.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import gzip
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
Http404,
|
||||
)
|
||||
|
||||
from bookmarks.models import BookmarkAsset
|
||||
|
||||
|
||||
def view(request, asset_id: int):
|
||||
try:
|
||||
asset = BookmarkAsset.objects.get(pk=asset_id)
|
||||
except BookmarkAsset.DoesNotExist:
|
||||
raise Http404("Asset does not exist")
|
||||
|
||||
bookmark = asset.bookmark
|
||||
is_owner = bookmark.owner == request.user
|
||||
is_shared = (
|
||||
request.user.is_authenticated
|
||||
and bookmark.shared
|
||||
and bookmark.owner.profile.enable_sharing
|
||||
)
|
||||
is_public_shared = bookmark.shared and bookmark.owner.profile.enable_public_sharing
|
||||
|
||||
if not is_owner and not is_shared and not is_public_shared:
|
||||
raise Http404("Bookmark does not exist")
|
||||
|
||||
filepath = os.path.join(settings.LD_ASSET_FOLDER, asset.file)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
raise Http404("Asset file does not exist")
|
||||
|
||||
if asset.gzip:
|
||||
with gzip.open(filepath, "rb") as f:
|
||||
content = f.read()
|
||||
else:
|
||||
with open(filepath, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
return HttpResponse(content, content_type=asset.content_type)
|
@@ -12,7 +12,13 @@ from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.models import Bookmark, BookmarkForm, BookmarkSearch, build_tag_string
|
||||
from bookmarks.models import (
|
||||
Bookmark,
|
||||
BookmarkAsset,
|
||||
BookmarkForm,
|
||||
BookmarkSearch,
|
||||
build_tag_string,
|
||||
)
|
||||
from bookmarks.services.bookmarks import (
|
||||
create_bookmark,
|
||||
update_bookmark,
|
||||
@@ -28,6 +34,7 @@ from bookmarks.services.bookmarks import (
|
||||
share_bookmarks,
|
||||
unshare_bookmarks,
|
||||
)
|
||||
from bookmarks.services import tasks
|
||||
from bookmarks.utils import get_safe_return_url
|
||||
from bookmarks.views.partials import contexts
|
||||
|
||||
@@ -120,31 +127,39 @@ def _details(request, bookmark_id: int, template: str):
|
||||
if not is_owner and not is_shared and not is_public_shared:
|
||||
raise Http404("Bookmark does not exist")
|
||||
|
||||
edit_return_url = get_safe_return_url(
|
||||
request.GET.get("return_url"), reverse("bookmarks:details", args=[bookmark_id])
|
||||
)
|
||||
delete_return_url = get_safe_return_url(
|
||||
request.GET.get("return_url"), reverse("bookmarks:index")
|
||||
)
|
||||
|
||||
# handles status actions form
|
||||
if request.method == "POST":
|
||||
if not is_owner:
|
||||
raise Http404("Bookmark does not exist")
|
||||
bookmark.is_archived = request.POST.get("is_archived") == "on"
|
||||
bookmark.unread = request.POST.get("unread") == "on"
|
||||
bookmark.shared = request.POST.get("shared") == "on"
|
||||
bookmark.save()
|
||||
|
||||
return HttpResponseRedirect(edit_return_url)
|
||||
return_url = get_safe_return_url(
|
||||
request.GET.get("return_url"),
|
||||
reverse("bookmarks:details", args=[bookmark.id]),
|
||||
)
|
||||
|
||||
if "remove_asset" in request.POST:
|
||||
asset_id = request.POST["remove_asset"]
|
||||
try:
|
||||
asset = bookmark.bookmarkasset_set.get(pk=asset_id)
|
||||
except BookmarkAsset.DoesNotExist:
|
||||
raise Http404("Asset does not exist")
|
||||
asset.delete()
|
||||
if "create_snapshot" in request.POST:
|
||||
tasks.create_html_snapshot(bookmark)
|
||||
else:
|
||||
bookmark.is_archived = request.POST.get("is_archived") == "on"
|
||||
bookmark.unread = request.POST.get("unread") == "on"
|
||||
bookmark.shared = request.POST.get("shared") == "on"
|
||||
bookmark.save()
|
||||
|
||||
return HttpResponseRedirect(return_url)
|
||||
|
||||
details_context = contexts.BookmarkDetailsContext(request, bookmark)
|
||||
|
||||
return render(
|
||||
request,
|
||||
template,
|
||||
{
|
||||
"bookmark": bookmark,
|
||||
"edit_return_url": edit_return_url,
|
||||
"delete_return_url": delete_return_url,
|
||||
"details": details_context,
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
|
||||
from bookmarks.models import Bookmark
|
||||
from bookmarks.views.partials import contexts
|
||||
|
||||
|
||||
@@ -56,3 +58,15 @@ def shared_tag_cloud(request):
|
||||
tag_cloud_context = contexts.SharedTagCloudContext(request)
|
||||
|
||||
return render(request, "bookmarks/tag_cloud.html", {"tag_cloud": tag_cloud_context})
|
||||
|
||||
|
||||
@login_required
|
||||
def details_form(request, bookmark_id: int):
|
||||
try:
|
||||
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
|
||||
except Bookmark.DoesNotExist:
|
||||
raise Http404("Bookmark does not exist")
|
||||
|
||||
details_context = contexts.BookmarkDetailsContext(request, bookmark)
|
||||
|
||||
return render(request, "bookmarks/details/form.html", {"details": details_context})
|
||||
|
@@ -6,11 +6,13 @@ from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks import utils
|
||||
from bookmarks.models import (
|
||||
Bookmark,
|
||||
BookmarkAsset,
|
||||
BookmarkSearch,
|
||||
User,
|
||||
UserProfile,
|
||||
@@ -274,3 +276,55 @@ class SharedTagCloudContext(TagCloudContext):
|
||||
return queries.query_shared_bookmark_tags(
|
||||
user, self.request.user_profile, self.search, public_only
|
||||
)
|
||||
|
||||
|
||||
class BookmarkAssetItem:
|
||||
def __init__(self, asset: BookmarkAsset):
|
||||
self.asset = asset
|
||||
|
||||
self.id = asset.id
|
||||
self.display_name = asset.display_name
|
||||
self.content_type = asset.content_type
|
||||
self.file = asset.file
|
||||
self.file_size = asset.file_size
|
||||
self.status = asset.status
|
||||
|
||||
icon_classes = []
|
||||
text_classes = []
|
||||
if asset.status == BookmarkAsset.STATUS_PENDING:
|
||||
icon_classes.append("text-gray")
|
||||
text_classes.append("text-gray")
|
||||
elif asset.status == BookmarkAsset.STATUS_FAILURE:
|
||||
icon_classes.append("text-error")
|
||||
text_classes.append("text-error")
|
||||
else:
|
||||
icon_classes.append("text-primary")
|
||||
|
||||
self.icon_classes = " ".join(icon_classes)
|
||||
self.text_classes = " ".join(text_classes)
|
||||
|
||||
|
||||
class BookmarkDetailsContext:
|
||||
def __init__(self, request: WSGIRequest, bookmark: Bookmark):
|
||||
user = request.user
|
||||
user_profile = request.user_profile
|
||||
|
||||
self.edit_return_url = utils.get_safe_return_url(
|
||||
request.GET.get("return_url"),
|
||||
reverse("bookmarks:details", args=[bookmark.id]),
|
||||
)
|
||||
self.delete_return_url = utils.get_safe_return_url(
|
||||
request.GET.get("return_url"), reverse("bookmarks:index")
|
||||
)
|
||||
|
||||
self.bookmark = bookmark
|
||||
self.profile = request.user_profile
|
||||
self.is_editable = bookmark.owner == user
|
||||
self.sharing_enabled = user_profile.enable_sharing
|
||||
self.show_link_icons = user_profile.enable_favicons and bookmark.favicon_file
|
||||
# For now hide files section if snapshots are not supported
|
||||
self.show_files = settings.LD_ENABLE_SNAPSHOTS
|
||||
|
||||
self.assets = [
|
||||
BookmarkAssetItem(asset) for asset in bookmark.bookmarkasset_set.all()
|
||||
]
|
||||
|
@@ -12,7 +12,7 @@ from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from bookmarks.models import Bookmark, BookmarkSearch, UserProfileForm, FeedToken
|
||||
from bookmarks.models import Bookmark, UserProfileForm, FeedToken
|
||||
from bookmarks.services import exporter, tasks
|
||||
from bookmarks.services import importer
|
||||
from bookmarks.utils import app_version
|
||||
@@ -24,6 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
def general(request):
|
||||
profile_form = None
|
||||
enable_refresh_favicons = django_settings.LD_ENABLE_REFRESH_FAVICONS
|
||||
has_snapshot_support = django_settings.LD_ENABLE_SNAPSHOTS
|
||||
update_profile_success_message = None
|
||||
refresh_favicons_success_message = None
|
||||
import_success_message = _find_message_with_tag(
|
||||
@@ -53,6 +54,7 @@ def general(request):
|
||||
{
|
||||
"form": profile_form,
|
||||
"enable_refresh_favicons": enable_refresh_favicons,
|
||||
"has_snapshot_support": has_snapshot_support,
|
||||
"update_profile_success_message": update_profile_success_message,
|
||||
"refresh_favicons_success_message": refresh_favicons_success_message,
|
||||
"import_success_message": import_success_message,
|
||||
|
Reference in New Issue
Block a user