Compare commits

...

9 Commits

Author SHA1 Message Date
Sascha Ißbrücker
cfe4ff113d Bump version 2025-02-22 19:28:47 +01:00
Sascha Ißbrücker
757dc56277 Bump base images 2025-02-19 16:14:34 +01:00
Sascha Ißbrücker
dfbb367857 Fix auth proxy logout (#994) 2025-02-19 07:27:04 +01:00
Sascha Ißbrücker
2276832465 Return web archive fallback URL from REST API (#993) 2025-02-19 06:44:21 +01:00
Chris M
9d61bdce52 Add note about OIDC and LD_SUPERUSER_NAME combination (#992)
* docs: add note about OIDC and LD_SUPERUSER_NAME combination

Resolves #988

* tweak text

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
2025-02-18 22:45:26 +01:00
Sascha Ißbrücker
1274a9ae0a Try limit uwsgi memory usage by configuring file descriptor limit (#990) 2025-02-15 08:49:58 +01:00
Sascha Ißbrücker
5e7172d17e Remove preview image when bookmark is deleted (#989) 2025-02-15 08:26:58 +01:00
Sascha Ißbrücker
78608135d9 Update CHANGELOG.md 2025-02-09 10:47:02 +01:00
Sascha Ißbrücker
51acd1da3f add build script 2025-02-08 18:20:15 +01:00
13 changed files with 195 additions and 26 deletions

26
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: build
on: workflow_dispatch
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
file: ./docker/default.Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: false
tags: sissbruecker/linkding:test
target: linkding

View File

@@ -1,5 +1,23 @@
# Changelog
## v1.38.0 (09/02/2025)
### What's Changed
* Fix nav menu closing on mousedown in Safari by @sissbruecker in https://github.com/sissbruecker/linkding/pull/965
* Allow customizing username when creating user through OIDC by @kyuuk in https://github.com/sissbruecker/linkding/pull/971
* Improve accessibility of modal dialogs by @sissbruecker in https://github.com/sissbruecker/linkding/pull/974
* Add option to collapse side panel by @sissbruecker in https://github.com/sissbruecker/linkding/pull/975
* Convert tag modal into drawer by @sissbruecker in https://github.com/sissbruecker/linkding/pull/977
* Add RSS link to shared bookmarks page by @sissbruecker in https://github.com/sissbruecker/linkding/pull/984
* Add Additional iOS Shortcut to community section by @joshdick in https://github.com/sissbruecker/linkding/pull/968
### New Contributors
* @kyuuk made their first contribution in https://github.com/sissbruecker/linkding/pull/971
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.37.0...v1.38.0
---
## v1.37.0 (26/01/2025)
### What's Changed

View File

@@ -10,6 +10,7 @@ from bookmarks.services.bookmarks import (
enhance_with_website_metadata,
)
from bookmarks.services.tags import get_or_create_tag
from bookmarks.services.wayback import generate_fallback_webarchive_url
class TagListField(serializers.ListField):
@@ -59,9 +60,10 @@ class BookmarkSerializer(serializers.ModelSerializer):
# Custom tag_names field to allow passing a list of tag names to create/update
tag_names = TagListField(required=False)
# Custom fields to return URLs for favicon and preview image
# Custom fields to generate URLs for favicon, preview image, and web archive snapshot
favicon_url = serializers.SerializerMethodField()
preview_image_url = serializers.SerializerMethodField()
web_archive_snapshot_url = serializers.SerializerMethodField()
# Add dummy website title and description fields for backwards compatibility but keep them empty
website_title = serializers.SerializerMethodField()
website_description = serializers.SerializerMethodField()
@@ -82,6 +84,12 @@ class BookmarkSerializer(serializers.ModelSerializer):
preview_image_url = request.build_absolute_uri(preview_image_file_path)
return preview_image_url
def get_web_archive_snapshot_url(self, obj: Bookmark):
if obj.web_archive_snapshot_url:
return obj.web_archive_snapshot_url
return generate_fallback_webarchive_url(obj.url, obj.date_added)
def get_website_title(self, obj: Bookmark):
return None

View File

@@ -93,6 +93,19 @@ class Bookmark(models.Model):
return self.resolved_title + " (" + self.url[:30] + "...)"
@receiver(post_delete, sender=Bookmark)
def bookmark_deleted(sender, instance, **kwargs):
if instance.preview_image_file:
filepath = os.path.join(settings.LD_PREVIEW_FOLDER, instance.preview_image_file)
if os.path.isfile(filepath):
try:
os.remove(filepath)
except Exception as error:
logger.error(
f"Failed to delete preview image: {filepath}", exc_info=error
)
class BookmarkAsset(models.Model):
TYPE_SNAPSHOT = "snapshot"
TYPE_UPLOAD = "upload"

View File

@@ -28,7 +28,7 @@
</ul>
</div>
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a>
<form class="d-inline" action="{% url 'logout' %}" method="post">
<form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link">Logout</button>
</form>
@@ -72,7 +72,7 @@
<a href="{% url 'bookmarks:settings.index' %}" class="menu-link">Settings</a>
</li>
<li class="menu-item">
<form class="d-inline" action="{% url 'logout' %}" method="post">
<form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link menu-link">Logout</button>
</form>

View File

@@ -1,25 +1,25 @@
import os
import shutil
import tempfile
from django.conf import settings
from django.test import TestCase
from django.test import TestCase, override_settings
from bookmarks.tests.helpers import (
BookmarkFactoryMixin,
)
from bookmarks.services import bookmarks
from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkAssetsTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
self.override = override_settings(LD_ASSET_FOLDER=self.temp_dir)
self.override.enable()
def tearDown(self):
temp_files = [
f for f in os.listdir(settings.LD_ASSET_FOLDER) if f.startswith("temp")
]
for temp_file in temp_files:
os.remove(os.path.join(settings.LD_ASSET_FOLDER, temp_file))
self.override.disable()
shutil.rmtree(self.temp_dir)
def setup_asset_file(self, filename):
if not os.path.exists(settings.LD_ASSET_FOLDER):
os.makedirs(settings.LD_ASSET_FOLDER)
filepath = os.path.join(settings.LD_ASSET_FOLDER, filename)
with open(filepath, "w") as f:
f.write("test")

View File

@@ -0,0 +1,70 @@
import os
import shutil
import tempfile
from django.conf import settings
from django.test import TestCase, override_settings
from bookmarks.services import bookmarks
from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkPreviewsTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
self.override = override_settings(LD_PREVIEW_FOLDER=self.temp_dir)
self.override.enable()
def tearDown(self):
self.override.disable()
shutil.rmtree(self.temp_dir)
def setup_preview_file(self, filename):
filepath = os.path.join(settings.LD_PREVIEW_FOLDER, filename)
with open(filepath, "w") as f:
f.write("test")
def setup_bookmark_with_preview(self):
bookmark = self.setup_bookmark()
bookmark.preview_image_file = f"preview_{bookmark.id}.jpg"
bookmark.save()
self.setup_preview_file(bookmark.preview_image_file)
return bookmark
def assertPreviewImageExists(self, bookmark):
self.assertTrue(
os.path.exists(
os.path.join(settings.LD_PREVIEW_FOLDER, bookmark.preview_image_file)
)
)
def assertPreviewImageDoesNotExist(self, bookmark):
self.assertFalse(
os.path.exists(
os.path.join(settings.LD_PREVIEW_FOLDER, bookmark.preview_image_file)
)
)
def test_delete_bookmark_deletes_preview_image(self):
bookmark = self.setup_bookmark_with_preview()
self.assertPreviewImageExists(bookmark)
bookmark.delete()
self.assertPreviewImageDoesNotExist(bookmark)
def test_bulk_delete_bookmarks_deletes_preview_images(self):
bookmark1 = self.setup_bookmark_with_preview()
bookmark2 = self.setup_bookmark_with_preview()
bookmark3 = self.setup_bookmark_with_preview()
self.assertPreviewImageExists(bookmark1)
self.assertPreviewImageExists(bookmark2)
self.assertPreviewImageExists(bookmark3)
bookmarks.delete_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertPreviewImageDoesNotExist(bookmark1)
self.assertPreviewImageDoesNotExist(bookmark2)
self.assertPreviewImageDoesNotExist(bookmark3)

View File

@@ -1,15 +1,18 @@
import datetime
import urllib.parse
from collections import OrderedDict
from unittest.mock import patch
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from bookmarks.models import Bookmark, BookmarkSearch, UserProfile
from bookmarks.services import website_loader
from bookmarks.services.wayback import generate_fallback_webarchive_url
from bookmarks.services.website_loader import WebsiteMetadata
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
@@ -33,7 +36,10 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
expectation["title"] = bookmark.title
expectation["description"] = bookmark.description
expectation["notes"] = bookmark.notes
expectation["web_archive_snapshot_url"] = bookmark.web_archive_snapshot_url
expectation["web_archive_snapshot_url"] = (
bookmark.web_archive_snapshot_url
or generate_fallback_webarchive_url(bookmark.url, bookmark.date_added)
)
expectation["favicon_url"] = (
f"http://testserver/static/{bookmark.favicon_file}"
if bookmark.favicon_file
@@ -590,6 +596,23 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual([response.data], [bookmark])
def test_get_bookmark_returns_fallback_webarchive_url(self):
self.authenticate()
bookmark = self.setup_bookmark(
web_archive_snapshot_url="",
url="https://example.com/",
added=timezone.datetime(
2023, 8, 11, 21, 45, 11, tzinfo=datetime.timezone.utc
),
)
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertEqual(
response.data["web_archive_snapshot_url"],
"https://web.archive.org/web/20230811214511/https://example.com/",
)
def test_update_bookmark(self):
self.authenticate()
bookmark = self.setup_bookmark()

View File

@@ -10,7 +10,7 @@ COPY bookmarks/styles ./bookmarks/styles
RUN npm run build
FROM python:3.12.8-alpine3.21 AS build-deps
FROM python:3.12.9-alpine3.21 AS build-deps
# Add required packages
# alpine-sdk linux-headers pkgconfig: build Python packages from source
# libpq-dev: build Postgres client from source
@@ -49,7 +49,7 @@ RUN wget https://www.sqlite.org/${SQLITE_RELEASE_YEAR}/sqlite-amalgamation-${SQL
gcc -fPIC -shared icu.c `pkg-config --libs --cflags icu-uc icu-io` -o libicu.so
FROM python:3.12.8-alpine3.21 AS linkding
FROM python:3.12.9-alpine3.21 AS linkding
LABEL org.opencontainers.image.source="https://github.com/sissbruecker/linkding"
# install runtime dependencies
RUN apk update && apk add bash curl icu libpq mailcap libssl3
@@ -73,6 +73,8 @@ ENV PATH=/opt/venv/bin:$PATH
RUN mkdir data && \
python manage.py collectstatic
# Limit file descriptors used by uwsgi, see https://github.com/sissbruecker/linkding/issues/453
ENV UWSGI_MAX_FD=4096
# Expose uwsgi server at port 9090
EXPOSE 9090
# Allow running containers as an an arbitrary user in the root group, to support deployment scenarios like OpenShift, Podman

View File

@@ -10,7 +10,7 @@ COPY bookmarks/styles ./bookmarks/styles
RUN npm run build
FROM python:3.12.8-slim-bookworm AS build-deps
FROM python:3.12.9-slim-bookworm AS build-deps
# Add required packages
# build-essential pkg-config: build Python packages from source
# libpq-dev: build Postgres client from source
@@ -51,7 +51,7 @@ RUN wget https://www.sqlite.org/${SQLITE_RELEASE_YEAR}/sqlite-amalgamation-${SQL
gcc -fPIC -shared icu.c `pkg-config --libs --cflags icu-uc icu-io` -o libicu.so
FROM python:3.12.8-slim-bookworm AS linkding
FROM python:3.12.9-slim-bookworm AS linkding
LABEL org.opencontainers.image.source="https://github.com/sissbruecker/linkding"
# install runtime dependencies
RUN apt-get update && apt-get -y install mime-support libpq-dev libicu-dev libssl3 curl
@@ -71,6 +71,8 @@ ENV PATH=/opt/venv/bin:$PATH
RUN mkdir data && \
python manage.py collectstatic
# Limit file descriptors used by uwsgi, see https://github.com/sissbruecker/linkding/issues/453
ENV UWSGI_MAX_FD=4096
# Expose uwsgi server at port 9090
EXPOSE 9090
# Allow running containers as an an arbitrary user in the root group, to support deployment scenarios like OpenShift, Podman

View File

@@ -19,7 +19,7 @@ For multiple options, use one `-e` argument per option.
### Docker-compose
For docker-compose options are configured using an `.env` file.
For docker-compose options are configured using an `.env` file.
Follow the docker-compose setup in the README and copy `.env.sample` to `.env`. Then modify the options in `.env`.
## List of options
@@ -106,10 +106,10 @@ Values: `True`, `False` | Default = `False`
Enables support for OpenID Connect (OIDC) authentication, allowing to use single sign-on (SSO) with OIDC providers.
When enabled, this shows a button on the login page that allows users to authenticate using an OIDC provider.
Users are associated by the email address provided from the OIDC provider, which is by default also used as username in linkding. You can configure a custom claim to be used as username with `OIDC_USERNAME_CLAIM`.
If there is no user with that email address as username, a new user is created automatically.
If there is no user with that email address as username, a new user is created automatically.
This requires configuring a number of options, which of those you need depends on which OIDC provider you use and how it is configured.
In general, you should find the required information in the UI of your OIDC provider, or its documentation.
In general, you should find the required information in the UI of your OIDC provider, or its documentation.
The options are adopted from the [mozilla-django-oidc](https://mozilla-django-oidc.readthedocs.io/en/stable/) library, which is used by linkding for OIDC support.
Please check their documentation for more information on the options.
@@ -127,6 +127,13 @@ The following options can be configured:
- `OIDC_RP_SCOPES` - Scopes asked for on the authorization flow. Default is `oidc email profile`.
- `OIDC_USERNAME_CLAIM` - A custom claim to used as username for new accounts, for example `preferred_username`. If the configured claim does not exist or is empty, the email claim is used as fallback. Default is `email`.
#### `OIDC` and `LD_SUPERUSER_NAME`
As noted above, OIDC matches users by email address, but `LD_SUPERUSER_NAME` will only set the username.
Instead of setting `LD_SUPERUSER_NAME` it is recommended that you use the method described in [User setup](/installation#user-setup) to configure a superuser with both username and email address.
This way when OIDC searches for a matching user it will find the superuser account you created.
Note that you should create the superuser **before** logging in with OIDC for the first time.
<details>
<summary>Authelia Example</summary>
@@ -200,7 +207,7 @@ All the other database variables below are only required for configured Postgres
Values: `String` | Default = `linkding`
The name of the database.
The name of the database.
### `LD_DB_USER`
@@ -260,7 +267,7 @@ Alternative favicon providers:
Values: `Float` | Default = 60.0
When creating HTML archive snapshots, control the timeout for how long to wait for the snapshot to complete, in `seconds`.
Defaults to 60 seconds; on lower-powered hardware you may need to increase this value.
Defaults to 60 seconds; on lower-powered hardware you may need to increase this value.
### `LD_SINGLEFILE_OPTIONS`

View File

@@ -1,6 +1,6 @@
{
"name": "linkding",
"version": "1.38.0",
"version": "1.38.1",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -1 +1 @@
1.38.0
1.38.1