Compare commits

...

30 Commits

Author SHA1 Message Date
Sascha Ißbrücker
e83d519cab Bump version 2023-12-08 22:01:24 +01:00
Sascha Ißbrücker
6355d8dff1 Properly encode search query param (#587) 2023-12-08 21:53:54 +01:00
Sascha Ißbrücker
227cfdb063 Update CHANGELOG.md 2023-11-24 10:23:44 +01:00
Sascha Ißbrücker
2d4da099c7 Bump version 2023-11-24 09:33:36 +01:00
Sascha Ißbrücker
a9512b2333 Include archived bookmarks in export (#579) 2023-11-24 09:21:23 +01:00
Oleksandr Perepadia
47e944e6c5 Update README.md (#574)
* Update README.md

Correct Firefox addon links to direct to the English language page

* Update firefox addon links to not presume any language
2023-11-14 17:08:17 +01:00
Sascha Ißbrücker
6c7ce91d53 Add backup CLI command (#571) 2023-11-05 19:27:48 +01:00
Sascha Ißbrücker
87020de917 Add Alpine based Docker image (experimental) (#570)
* use alpine as base image

* try fix missing mime types

* Extract separate Dockerfile

* Restore playwright dev dependency

* Add info to README.md
2023-11-05 14:18:26 +01:00
Sascha Ißbrücker
a130daa0f0 Update backup docs 2023-11-05 13:53:08 +01:00
Sascha Ißbrücker
d7c68c2818 Update CHANGELOG.md 2023-11-04 12:26:30 +01:00
Sascha Ißbrücker
1daad2c86c Bump version 2023-11-04 10:17:01 +01:00
dependabot[bot]
251def2583 Bump django from 4.1.10 to 4.1.13 (#567)
Bumps [django](https://github.com/django/django) from 4.1.10 to 4.1.13.
- [Commits](https://github.com/django/django/compare/4.1.10...4.1.13)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-04 09:57:08 +01:00
Vitor Marçal
560769f068 Fix RSS feed not handling None values (#569)
Previously, the 'sanitize' function would throw an error when 'text' was None. This commit fixes the issue by adding a check to handle the case where 'text' is None, returning an empty string instead.

Closes #568
2023-11-04 09:56:06 +01:00
Sascha Ißbrücker
dc9799cc53 Update CHANGELOG.md 2023-10-27 21:18:08 +02:00
Sascha Ißbrücker
41c1b9ab84 Bump version 2023-10-27 20:06:25 +02:00
dependabot[bot]
2396c8fe99 Bump urllib3 from 1.26.17 to 1.26.18 (#560)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-27 20:00:14 +02:00
Sascha Ißbrücker
de328c78e2 Sanitize RSS feed to remove control characters (#565) 2023-10-27 19:59:06 +02:00
Strubbl
314e4a9b74 Add feed2linkding to community section (#544)
* Update README.md

add feed2linkding

* Update README.md

sort feed2linkding in the list
2023-10-14 00:25:44 +02:00
Sascha Ißbrücker
ff400a79ec Disable editing of search preferences in user admin (#555) 2023-10-14 00:05:27 +02:00
Sascha Ißbrücker
f4fcb96b5e Update README.md 2023-10-11 18:16:47 +02:00
Sascha Ißbrücker
daab772971 update ios shortcut how-to 2023-10-07 18:02:07 +02:00
Sascha Ißbrücker
64c81ea565 rename ios shortcut 2023-10-07 17:58:01 +02:00
Sascha Ißbrücker
1dd19e8fa2 add ios shortcut 2023-10-07 17:36:54 +02:00
andrewdolphin
dd3699cdeb Add iOS shortcut to community section (#550)
* Update README.md

* Update README.md

* Update README.md

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@googlemail.com>
2023-10-07 16:41:12 +02:00
dependabot[bot]
f9c9d17873 Bump urllib3 from 1.26.11 to 1.26.17 (#542)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.11 to 1.26.17.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.11...1.26.17)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-07 10:30:53 +02:00
Sascha Ißbrücker
5c9f03a715 Fix search options not opening on iOS (#549)
* Fix search options not opening on iOS

* cleanup
2023-10-07 10:24:09 +02:00
Sascha Ißbrücker
7600fe87f9 Bump version 2023-10-06 23:35:17 +02:00
Sascha Ißbrücker
f756e28daf Fix memory leak with SQLite (#548) 2023-10-06 23:29:29 +02:00
Sascha Ißbrücker
1e10d7eb4a Bump docker node version 2023-10-03 18:08:23 +02:00
Sascha Ißbrücker
ccf8e03571 Update CHANGELOG.md 2023-10-01 22:19:39 +02:00
37 changed files with 595 additions and 143 deletions

View File

@@ -1,34 +1,22 @@
# Remove project files, data, tmp files, build files
/.env
/.idea
/data
/node_modules
/tmp
/docs
/static
/scripts
/build
/out
/.git
/.devcontainer
# Ignore everything
*
/.dockerignore
/.gitignore
/.gitattributes
/Dockerfile
/docker-compose.yml
/*.sh
/*.iml
/*.patch
/*.md
/*.js
/*.log
/*.pid
# Include files required for build or at runtime
!/bookmarks
!/siteroot
# Whitelist files needed in build or prod image
!/rollup.config.js
!/bootstrap.sh
!/background-tasks-wrapper.sh
!/bootstrap.sh
!/LICENSE.txt
!/manage.py
!/package.json
!/package-lock.json
!/requirements.prod.txt
!/requirements.txt
!/rollup.config.js
!/supervisord.conf
!/uwsgi.ini
!/version.txt
# Remove development settings
# Remove dev settings
/siteroot/settings/dev.py

View File

@@ -1,5 +1,80 @@
# Changelog
## v1.23.0 (24/11/2023)
### What's Changed
* Add Alpine based Docker image (experimental) by @sissbruecker in https://github.com/sissbruecker/linkding/pull/570
* Add backup CLI command by @sissbruecker in https://github.com/sissbruecker/linkding/pull/571
* Update browser extension links by @OPerepadia in https://github.com/sissbruecker/linkding/pull/574
* Include archived bookmarks in export by @sissbruecker in https://github.com/sissbruecker/linkding/pull/579
### New Contributors
* @OPerepadia made their first contribution in https://github.com/sissbruecker/linkding/pull/574
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.3...v1.23.0
---
## v1.22.3 (04/11/2023)
### What's Changed
* Fix RSS feed not handling None values by @vitormarcal in https://github.com/sissbruecker/linkding/pull/569
* Bump django from 4.1.10 to 4.1.13 by @dependabot in https://github.com/sissbruecker/linkding/pull/567
### New Contributors
* @vitormarcal made their first contribution in https://github.com/sissbruecker/linkding/pull/569
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.2...v1.22.3
---
## v1.22.2 (27/10/2023)
### What's Changed
* Fix search options not opening on iOS by @sissbruecker in https://github.com/sissbruecker/linkding/pull/549
* Bump urllib3 from 1.26.11 to 1.26.17 by @dependabot in https://github.com/sissbruecker/linkding/pull/542
* Add iOS shortcut to community section by @andrewdolphin in https://github.com/sissbruecker/linkding/pull/550
* Disable editing of search preferences in user admin by @sissbruecker in https://github.com/sissbruecker/linkding/pull/555
* Add feed2linkding to community section by @Strubbl in https://github.com/sissbruecker/linkding/pull/544
* Sanitize RSS feed to remove control characters by @sissbruecker in https://github.com/sissbruecker/linkding/pull/565
* Bump urllib3 from 1.26.17 to 1.26.18 by @dependabot in https://github.com/sissbruecker/linkding/pull/560
### New Contributors
* @andrewdolphin made their first contribution in https://github.com/sissbruecker/linkding/pull/550
* @Strubbl made their first contribution in https://github.com/sissbruecker/linkding/pull/544
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.1...v1.22.2
---
## v1.22.1 (06/10/2023)
### What's Changed
* Fix memory leak with SQLite by @sissbruecker in https://github.com/sissbruecker/linkding/pull/548
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.0...v1.22.1
---
## v1.22.0 (01/10/2023)
### What's Changed
* Fix case-insensitive search for unicode characters in SQLite by @sissbruecker in https://github.com/sissbruecker/linkding/pull/520
* Add sort option to bookmark list by @sissbruecker in https://github.com/sissbruecker/linkding/pull/522
* Add button to show tags on smaller screens by @sissbruecker in https://github.com/sissbruecker/linkding/pull/529
* Make code blocks in notes scrollable by @sissbruecker in https://github.com/sissbruecker/linkding/pull/530
* Add filter for shared state by @sissbruecker in https://github.com/sissbruecker/linkding/pull/531
* Add support for exporting/importing bookmark notes by @sissbruecker in https://github.com/sissbruecker/linkding/pull/532
* Add filter for unread state by @sissbruecker in https://github.com/sissbruecker/linkding/pull/535
* Allow saving search preferences by @sissbruecker in https://github.com/sissbruecker/linkding/pull/540
* Add user profile endpoint by @sissbruecker in https://github.com/sissbruecker/linkding/pull/541
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.21.0...v1.22.0
---
## v1.21.1 (26/09/2023)
### What's Changed

View File

@@ -17,7 +17,7 @@
- [Documentation](#documentation)
- [Browser Extension](#browser-extension)
- [Community](#community)
- [Acknowledgements](#acknowledgements)
- [Acknowledgements + Donations](#acknowledgements--donations)
- [Development](#development)
## Introduction
@@ -40,7 +40,7 @@ The name comes from:
- Automatically provides titles, descriptions and icons of bookmarked websites
- Automatically creates snapshots of bookmarked websites on [the Internet Archive Wayback Machine](https://archive.org/web/)
- Import and export bookmarks in Netscape HTML format
- Extensions for [Firefox](https://addons.mozilla.org/de/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe), as well as a bookmarklet
- Extensions for [Firefox](https://addons.mozilla.org/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe), as well as a bookmarklet
- Light and dark themes
- REST API for developing 3rd party apps
- Admin panel for user self-service and raw data access
@@ -58,9 +58,27 @@ The name comes from:
linkding is designed to be run with container solutions like [Docker](https://docs.docker.com/get-started/).
The Docker image is compatible with ARM platforms, so it can be run on a Raspberry Pi.
By default, linkding uses SQLite as a database.
linkding uses an SQLite database by default.
Alternatively linkding supports PostgreSQL, see the [database options](docs/Options.md#LD_DB_ENGINE) for more information.
<details>
<summary>🧪 Alpine-based image</summary>
The default Docker image (`latest` tag) is based on a slim variant of Debian Linux.
Alternatively, there is an image based on Alpine Linux (`latest-alpine` tag) which has a smaller size, resulting in a smaller download and less disk space required.
The Alpine image is currently about 45 MB in compressed size, compared to about 130 MB for the Debian image.
To use it, replace the `latest` tag with `latest-alpine`, either in the CLI command below when using Docker, or in the `docker-compose.yml` file when using docker-compose.
> [!WARNING]
> The image is currently considered experimental in order to gather feedback and iron out any issues.
> Only use it if you are comfortable running experimental software or want to help out with testing.
> While there should be no issues with creating new installations, there might be issues when migrating existing installations.
> If you plan to migrate your existing installation, make sure to create proper [backups](https://github.com/sissbruecker/linkding/blob/master/docs/backup.md) first.
</details>
### Using Docker
To install linkding using Docker you can just run the [latest image](https://hub.docker.com/repository/docker/sissbruecker/linkding) from Docker Hub:
@@ -160,10 +178,10 @@ Instead of configuring header forwarding in your proxy, you can also configure t
### Managed Hosting Options
Self-hosting web applications still requires a lot of technical know-how, and commitment to maintenance, with regard to keeping everything up-to-date and secure. This can be a huge entry barrier for people who are interested in self-hosting, but lack the technical knowledge to do so. This section is intended to provide alternatives in form of managed hosting solutions. Note that these options are usually commercial offerings, that require paying a fee for the convenience of being managed by another party. The technical knowledge required to make use of individual options is going to vary, and no guarantees can be made that every option is accessible for everyone. That being said, I hope this section helps in making the application accessible to a wider audience.
Self-hosting web applications still requires a lot of technical know-how and commitment to maintenance, in order to keep everything up-to-date and secure. This section is intended to provide simple alternatives in form of managed hosting solutions.
- [linkding on fly.io](https://github.com/fspoettel/linkding-on-fly) - Guide for hosting a linkding installation on [fly.io](https://fly.io). By [fspoettel](https://github.com/fspoettel)
- [PikaPods.com](https://www.pikapods.com/) - Managed hosting for linkding, EU and US regions available. [1-click setup link](https://www.pikapods.com/pods?run=linkding)
- [PikaPods.com](https://www.pikapods.com/) - Managed hosting for linkding, EU and US regions available. [1-click setup link](https://www.pikapods.com/pods?run=linkding) ([Disclosure](#pikapods))
## Documentation
@@ -180,7 +198,7 @@ Self-hosting web applications still requires a lot of technical know-how, and co
## Browser Extension
linkding comes with an official browser extension that allows to quickly add bookmarks, and search bookmarks through the browser's address bar. You can get the extension here:
- [Mozilla Addon Store](https://addons.mozilla.org/de/firefox/addon/linkding-extension/)
- [Mozilla Addon Store](https://addons.mozilla.org/firefox/addon/linkding-extension/)
- [Chrome Web Store](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
The extension is open-source as well, and can be found [here](https://github.com/sissbruecker/linkding-extension).
@@ -190,7 +208,9 @@ The extension is open-source as well, and can be found [here](https://github.com
This section lists community projects around using linkding, in alphabetical order. If you have a project that you want to share with the linkding community, feel free to submit a PR to add your project to this section.
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
- [feed2linkding](https://codeberg.org/strubbl/feed2linkding) A commandline utility to add all web feed item links to linkding via API call. By [Strubbl](https://github.com/Strubbl)
- [Helm Chart](https://charts.pascaliske.dev/charts/linkding/) Helm Chart for deploying linkding inside a Kubernetes cluster. By [pascaliske](https://github.com/pascaliske)
- [iOS Shortcut using API and Tagging](https://gist.github.com/andrewdolphin/a7dff49505e588d940bec55132fab8ad) An iOS shortcut using the Linkding API (no extra logins required) that pulls previously used tags and allows tagging at the time of link creation.
- [Linka!](https://github.com/cmsax/linka) Web app (also a PWA) for quickly searching & opening bookmarks in linkding, support multi keywords, exclude mode and other advance options. By [cmsax](https://github.com/cmsax)
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
@@ -199,9 +219,21 @@ This section lists community projects around using linkding, in alphabetical ord
- [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
- [Postman collection](https://gist.github.com/gingerbeardman/f0b42502f3bc9344e92ce63afd4360d3) a group of saved request templates for API testing. By [gingerbeardman](https://github.com/gingerbeardman)
## Acknowledgements
## Acknowledgements + Donations
JetBrains provides an open-source license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) for the development of linkding.
### PikaPods
[PikaPods](https://www.pikapods.com/) has a revenue sharing agreement with this project, sharing some of their revenue from hosting linkding instances. I do not intend to profit from this project financially, so I am in turn donating that revenue. Big thanks to PikaPods for making this possible.
See the table below for a list of donations.
| Source | Description | Amount | Donated to |
|---------------------------------------|---------------------------------------------|---------|---------------------------------------------------------------------|
| [PikaPods](https://www.pikapods.com/) | Linkding hosting June 2022 - September 2023 | $163.50 | [Internet Archive](/docs/donations/2023-10-11-internet-archive.png) |
### JetBrains
JetBrains has previously provided an open-source license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) for the development of linkding.
## Development

View File

@@ -122,7 +122,7 @@ class AdminUserProfileInline(admin.StackedInline):
can_delete = False
verbose_name_plural = 'Profile'
fk_name = 'user'
readonly_fields = ('search_preferences', )
class AdminCustomUser(UserAdmin):
inlines = (AdminUserProfileInline,)

View File

@@ -1,11 +1,12 @@
import unicodedata
from dataclasses import dataclass
from django.contrib.syndication.views import Feed
from django.db.models import QuerySet
from django.urls import reverse
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken
from bookmarks import queries
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken
@dataclass
@@ -14,6 +15,14 @@ class FeedContext:
query_set: QuerySet[Bookmark]
def sanitize(text: str):
if not text:
return ''
# remove control characters
valid_chars = ['\n', '\r', '\t']
return ''.join(ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != 'C')
class BaseBookmarksFeed(Feed):
def get_object(self, request, feed_key: str):
feed_token = FeedToken.objects.get(key__exact=feed_key)
@@ -22,10 +31,10 @@ class BaseBookmarksFeed(Feed):
return FeedContext(feed_token, query_set)
def item_title(self, item: Bookmark):
return item.resolved_title
return sanitize(item.resolved_title)
def item_description(self, item: Bookmark):
return item.resolved_description
return sanitize(item.resolved_description)
def item_link(self, item: Bookmark):
return item.url

View File

@@ -0,0 +1,36 @@
import { registerBehavior } from "./index";
class DropdownBehavior {
constructor(element) {
this.element = element;
this.opened = false;
this.onOutsideClick = this.onOutsideClick.bind(this);
const toggle = element.querySelector(".dropdown-toggle");
toggle.addEventListener("click", () => {
if (this.opened) {
this.close();
} else {
this.open();
}
});
}
open() {
this.element.classList.add("active");
document.addEventListener("click", this.onOutsideClick);
}
close() {
this.element.classList.remove("active");
document.removeEventListener("click", this.onOutsideClick);
}
onOutsideClick(event) {
if (!this.element.contains(event.target)) {
this.close();
}
}
}
registerBehavior("ld-dropdown", DropdownBehavior);

View File

@@ -4,6 +4,7 @@ import { ApiClient } from "./api";
import "./behaviors/bookmark-page";
import "./behaviors/bulk-edit";
import "./behaviors/confirm-button";
import "./behaviors/dropdown";
import "./behaviors/modal";
import "./behaviors/global-shortcuts";
import "./behaviors/tag-autocomplete";

View File

@@ -0,0 +1,26 @@
import sqlite3
import os
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Creates a backup of the linkding database"
def add_arguments(self, parser):
parser.add_argument('destination', type=str, help='Backup file destination')
def handle(self, *args, **options):
destination = options['destination']
def progress(status, remaining, total):
self.stdout.write(f'Copied {total-remaining} of {total} pages...')
source_db = sqlite3.connect(os.path.join('data', 'db.sqlite3'))
backup_db = sqlite3.connect(destination)
with backup_db:
source_db.backup(backup_db, pages=50, progress=progress)
backup_db.close()
source_db.close()
self.stdout.write(self.style.SUCCESS(f'Backup created at {destination}'))

View File

@@ -33,7 +33,10 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
desc = html.escape(bookmark.resolved_description or '')
if bookmark.notes:
desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]'
tags = ','.join(bookmark.tag_names)
tag_names = bookmark.tag_names
if bookmark.is_archived:
tag_names.append('linkding:archived')
tags = ','.join(tag_names)
toread = '1' if bookmark.unread else '0'
private = '0' if bookmark.shared else '1'
added = int(bookmark.date_added.timestamp())

View File

@@ -5,7 +5,7 @@ from typing import List
from django.contrib.auth.models import User
from django.utils import timezone
from bookmarks.models import Bookmark, Tag, parse_tag_string
from bookmarks.models import Bookmark, Tag
from bookmarks.services import tasks
from bookmarks.services.parser import parse, NetscapeBookmark
from bookmarks.utils import parse_timestamp
@@ -93,8 +93,7 @@ def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User)
tags_to_create = []
for netscape_bookmark in netscape_bookmarks:
tag_names = parse_tag_string(netscape_bookmark.tag_string)
for tag_name in tag_names:
for tag_name in netscape_bookmark.tag_names:
tag = tag_cache.get(tag_name)
if not tag:
tag = Tag(name=tag_name, owner=user)
@@ -194,8 +193,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
continue
# Get tag models by string, schedule inserts for bookmark -> tag associations
tag_names = parse_tag_string(netscape_bookmark.tag_string)
tags = tag_cache.get_all(tag_names)
tags = tag_cache.get_all(netscape_bookmark.tag_names)
for tag in tags:
relationships.append(BookmarkToTagRelationShip(bookmark=bookmark, tag=tag))
@@ -219,3 +217,5 @@ def _copy_bookmark_data(netscape_bookmark: NetscapeBookmark, bookmark: Bookmark,
bookmark.notes = netscape_bookmark.notes
if options.map_private_flag and not netscape_bookmark.private:
bookmark.shared = True
if netscape_bookmark.archived:
bookmark.is_archived = True

View File

@@ -2,6 +2,8 @@ from dataclasses import dataclass
from html.parser import HTMLParser
from typing import Dict, List
from bookmarks.models import parse_tag_string
@dataclass
class NetscapeBookmark:
@@ -10,9 +12,10 @@ class NetscapeBookmark:
description: str
notes: str
date_added: str
tag_string: str
tag_names: List[str]
to_read: bool
private: bool
archived: bool
class BookmarkParser(HTMLParser):
@@ -56,16 +59,24 @@ class BookmarkParser(HTMLParser):
def handle_start_a(self, attrs: Dict[str, str]):
vars(self).update(attrs)
tag_names = parse_tag_string(self.tags)
archived = 'linkding:archived' in self.tags
try:
tag_names.remove('linkding:archived')
except ValueError:
pass
self.bookmark = NetscapeBookmark(
href=self.href,
title='',
description='',
notes='',
date_added=self.add_date,
tag_string=self.tags,
tag_names=tag_names,
to_read=self.toread == '1',
# Mark as private by default, also when attribute is not specified
private=self.private != '0',
archived=archived,
)
def handle_a_data(self, data):

View File

@@ -74,12 +74,6 @@
min-width: 250px;
}
&:focus-within {
.menu {
display: block;
}
}
.menu .actions {
margin-top: $unit-4;
display: flex;

View File

@@ -44,8 +44,8 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</a>
<div class="dropdown dropdown-right">
<a href="#" id="mobile-nav-menu-trigger" class="btn btn-link dropdown-toggle" tabindex="0">
<div ld-dropdown class="dropdown dropdown-right">
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
style="width: 24px; height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
@@ -80,22 +80,3 @@
</div>
</div>
{% endhtmlmin %}
<script>
// Hide mobile menu on outside click
// The Spectre CSS component relies on focus changes to show/hide the dropdown, however mobile browsers like
// Safari will not trigger a blur event when clicking on a non-focusable element, so we have to simulate the
// behaviour through Javascript
const mobileNavMenuTrigger = document.getElementById('mobile-nav-menu-trigger');
function mobileNavMenuOutsideClickHandler(clickEvent) {
if (mobileNavMenuTrigger.parentElement.contains(clickEvent.target)) return
mobileNavMenuTrigger.blur();
}
mobileNavMenuTrigger.addEventListener('focus', function () {
document.addEventListener('click', mobileNavMenuOutsideClickHandler);
})
mobileNavMenuTrigger.addEventListener('blur', function () {
document.removeEventListener('click', mobileNavMenuOutsideClickHandler);
})
</script>

View File

@@ -9,7 +9,7 @@
{{ hidden_field }}
{% endfor %}
</form>
<div class="search-options dropdown dropdown-right">
<div ld-dropdown class="search-options dropdown dropdown-right">
<button type="button" class="btn dropdown-toggle{% if search.has_modified_preferences %} badge{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
@@ -95,7 +95,7 @@
props: {
name: 'q',
placeholder: 'Search for words or #tags',
value: '{{ search.q|safe }}',
value: input.value,
tags: uniqueTags,
mode: '{{ mode }}',
linkTarget: '{{ request.user_profile.bookmark_link_target }}',

View File

@@ -9,7 +9,7 @@
<h2>Browser Extension</h2>
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The extension is available in the official extension stores for:</p>
<ul>
<li><a href="https://addons.mozilla.org/de/firefox/addon/linkding-extension/" target="_blank">Firefox</a></li>
<li><a href="https://addons.mozilla.org/firefox/addon/linkding-extension/" target="_blank">Firefox</a></li>
<li><a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe" target="_blank">Chrome</a></li>
</ul>
<p>The extension is <a href="https://github.com/sissbruecker/linkding-extension" target="_blank">open source</a> as well, which enables you to build and manually load it into any browser that supports Chrome extensions.</p>

View File

@@ -422,3 +422,31 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
self.assertEqual(actions_form.attrs['action'],
'/bookmarks/archived/action?q=%23foo&return_url=%2Fbookmarks%2Farchived%3Fq%3D%2523foo')
def test_encode_search_params(self):
bookmark = self.setup_bookmark(description='alert(\'xss\')', is_archived=True)
url = reverse('bookmarks:archived') + '?q=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
self.assertContains(response, bookmark.url)
url = reverse('bookmarks:archived') + '?sort=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:archived') + '?unread=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:archived') + '?shared=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:archived') + '?user=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:archived') + '?page=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')

View File

@@ -418,3 +418,31 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertEqual(actions_form.attrs['action'],
'/bookmarks/action?q=%23foo&return_url=%2Fbookmarks%3Fq%3D%2523foo')
def test_encode_search_params(self):
bookmark = self.setup_bookmark(description='alert(\'xss\')')
url = reverse('bookmarks:index') + '?q=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
self.assertContains(response, bookmark.url)
url = reverse('bookmarks:index') + '?sort=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:index') + '?unread=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:index') + '?shared=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:index') + '?user=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:index') + '?page=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')

View File

@@ -500,3 +500,35 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertEqual(actions_form.attrs['action'],
'/bookmarks/shared/action?q=%23foo&return_url=%2Fbookmarks%2Fshared%3Fq%3D%2523foo')
def test_encode_search_params(self):
self.authenticate()
user = self.get_or_create_test_user()
user.profile.enable_sharing = True
user.profile.save()
bookmark = self.setup_bookmark(description='alert(\'xss\')', shared=True)
url = reverse('bookmarks:shared') + '?q=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
self.assertContains(response, bookmark.url)
url = reverse('bookmarks:shared') + '?sort=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:shared') + '?unread=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:shared') + '?shared=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:shared') + '?user=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')
url = reverse('bookmarks:shared') + '?page=alert(%27xss%27)'
response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')')

View File

@@ -22,6 +22,9 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
description='Example description', notes='Example notes'),
self.setup_bookmark(url='https://example.com/6', title='Title 6', added=added, shared=True,
notes='Example notes'),
self.setup_bookmark(url='https://example.com/7', title='Title 7', added=added, is_archived=True),
self.setup_bookmark(url='https://example.com/8', title='Title 8', added=added,
tags=[self.setup_tag(name='tag4'), self.setup_tag(name='tag5')], is_archived=True),
]
html = exporter.export_netscape_html(bookmarks)
@@ -35,6 +38,8 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
'<DD>Example description[linkding-notes]Example notes[/linkding-notes]',
f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
'<DD>[linkding-notes]Example notes[/linkding-notes]',
f'<DT><A HREF="https://example.com/7" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
f'<DT><A HREF="https://example.com/8" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
]
self.assertIn('\n\r'.join(lines), html)

View File

@@ -7,6 +7,8 @@ from django.urls import reverse
from bookmarks.tests.helpers import BookmarkFactoryMixin
from bookmarks.models import FeedToken, User
from bookmarks.feeds import sanitize
def rfc2822_date(date):
@@ -104,6 +106,17 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.assertContains(response, '<item>', count=0)
def test_strip_control_characters(self):
self.setup_bookmark(title='test\n\r\t\0\x08title', description='test\n\r\t\0\x08description')
response = self.client.get(reverse('bookmarks:feeds.all', args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=1)
self.assertContains(response, f'<title>test\n\r\ttitle</title>', count=1)
self.assertContains(response, f'<description>test\n\r\tdescription</description>', count=1)
def test_sanitize_with_none_text(self):
self.assertEqual('', sanitize(None))
def test_unread_returns_404_for_unknown_feed_token(self):
response = self.client.get(reverse('bookmarks:feeds.unread', args=['foo']))

View File

@@ -295,6 +295,27 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
self.assertEqual(bookmark2.shared, False)
self.assertEqual(bookmark3.shared, True)
def test_archived_state(self):
test_html = self.render_html(tags_html='''
<DT><A HREF="https://example.com/1" ADD_DATE="1" TAGS="tag1,tag2,linkding:archived">Example title 1</A>
<DD>Example description 1</DD>
<DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1" TAGS="tag1,tag2">Example title 2</A>
<DD>Example description 2</DD>
<DT><A HREF="https://example.com/3" ADD_DATE="1" PRIVATE="0">Example title 3</A>
<DD>Example description 3</DD>
''')
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 3)
self.assertEqual(Bookmark.objects.all()[0].is_archived, True)
self.assertEqual(Bookmark.objects.all()[1].is_archived, False)
self.assertEqual(Bookmark.objects.all()[2].is_archived, False)
tags = Tag.objects.all()
self.assertEqual(len(tags), 2)
self.assertEqual(tags[0].name, 'tag1')
self.assertEqual(tags[1].name, 'tag2')
def test_notes(self):
# initial notes
test_html = self.render_html(tags_html='''

View File

@@ -2,6 +2,7 @@ from typing import List
from django.test import TestCase
from bookmarks.models import parse_tag_string
from bookmarks.services.parser import NetscapeBookmark
from bookmarks.services.parser import parse
from bookmarks.tests.helpers import ImportTestMixin, BookmarkHtmlTag
@@ -16,7 +17,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
self.assertEqual(bookmark.title, html_tag.title)
self.assertEqual(bookmark.date_added, html_tag.add_date)
self.assertEqual(bookmark.description, html_tag.description)
self.assertEqual(bookmark.tag_string, html_tag.tags)
self.assertEqual(bookmark.tag_names, parse_tag_string(html_tag.tags))
self.assertEqual(bookmark.to_read, html_tag.to_read)
self.assertEqual(bookmark.private, html_tag.private)

View File

@@ -3,6 +3,7 @@ from unittest.mock import patch
from django.test import TestCase
from django.urls import reverse
from bookmarks.models import Bookmark
from bookmarks.tests.helpers import BookmarkFactoryMixin
@@ -20,6 +21,9 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(tags=[self.setup_tag()])
self.setup_bookmark(tags=[self.setup_tag()])
self.setup_bookmark(tags=[self.setup_tag()])
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
response = self.client.get(
reverse('bookmarks:settings.export'),
@@ -30,6 +34,35 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(response['content-type'], 'text/plain; charset=UTF-8')
self.assertEqual(response['Content-Disposition'], 'attachment; filename="bookmarks.html"')
for bookmark in Bookmark.objects.all():
self.assertContains(response, bookmark.url)
def test_should_only_export_user_bookmarks(self):
other_user = self.setup_user()
owned_bookmarks = [
self.setup_bookmark(tags=[self.setup_tag()]),
self.setup_bookmark(tags=[self.setup_tag()]),
self.setup_bookmark(tags=[self.setup_tag()]),
]
non_owned_bookmarks = [
self.setup_bookmark(tags=[self.setup_tag()], user=other_user),
self.setup_bookmark(tags=[self.setup_tag()], user=other_user),
self.setup_bookmark(tags=[self.setup_tag()], user=other_user),
]
response = self.client.get(
reverse('bookmarks:settings.export'),
follow=True
)
text = response.content.decode('utf-8')
for bookmark in owned_bookmarks:
self.assertIn(bookmark.url, text)
for bookmark in non_owned_bookmarks:
self.assertNotIn(bookmark.url, text)
def test_should_check_authentication(self):
self.client.logout()
response = self.client.get(reverse('bookmarks:settings.export'), follow=True)

View File

@@ -12,8 +12,7 @@ from django.shortcuts import render
from django.urls import reverse
from rest_framework.authtoken.models import Token
from bookmarks.models import BookmarkSearch, UserProfileForm, FeedToken
from bookmarks.queries import query_bookmarks
from bookmarks.models import Bookmark, BookmarkSearch, UserProfileForm, FeedToken
from bookmarks.services import exporter, tasks
from bookmarks.services import importer
from bookmarks.utils import app_version
@@ -136,7 +135,7 @@ def bookmark_import(request):
def bookmark_export(request):
# noinspection PyBroadException
try:
bookmarks = list(query_bookmarks(request.user, request.user_profile, BookmarkSearch()))
bookmarks = Bookmark.objects.filter(owner=request.user)
# Prefetch tags to prevent n+1 queries
prefetch_related_objects(bookmarks, 'tags')
file_content = exporter.export_netscape_html(bookmarks)

89
docker/alpine.Dockerfile Normal file
View File

@@ -0,0 +1,89 @@
FROM node:18.18.0-alpine AS node-build
WORKDIR /etc/linkding
# install build dependencies
COPY rollup.config.js package.json package-lock.json ./
RUN npm install
# copy files needed for JS build
COPY bookmarks/frontend ./bookmarks/frontend
# run build
RUN npm run build
FROM python:3.10.13-alpine3.18 AS python-base
RUN apk update && apk add alpine-sdk linux-headers libpq-dev pkgconfig icu-dev sqlite-dev
WORKDIR /etc/linkding
FROM python-base AS python-build
# install build dependencies
COPY requirements.txt requirements.txt
# remove playwright from requirements as there is not always a distro and it's not needed for the build
RUN sed -i '/playwright/d' requirements.txt
RUN pip install -U pip && pip install -Ur requirements.txt
# copy files needed for Django build
COPY . .
COPY --from=node-build /etc/linkding .
# run Django part of the build
RUN python manage.py compilescss && \
python manage.py collectstatic --ignore=*.scss && \
python manage.py compilescss --delete-files
FROM python-base AS prod-deps
COPY requirements.prod.txt ./requirements.txt
RUN mkdir /opt/venv && \
python -m venv --upgrade-deps --copies /opt/venv && \
/opt/venv/bin/pip install --upgrade pip wheel && \
/opt/venv/bin/pip install -Ur requirements.txt
FROM python-base AS compile-icu
# Defines SQLite version
# Since this is only needed for downloading the header files this probably
# doesn't need to be up-to-date, assuming the SQLite APIs used by the ICU
# extension do not change
ARG SQLITE_RELEASE_YEAR=2023
ARG SQLITE_RELEASE=3430000
# Compile the ICU extension needed for case-insensitive search and ordering
# with SQLite. This does:
# - Download SQLite amalgamation for header files
# - Download ICU extension source file
# - Compile ICU extension
RUN wget https://www.sqlite.org/${SQLITE_RELEASE_YEAR}/sqlite-amalgamation-${SQLITE_RELEASE}.zip && \
unzip sqlite-amalgamation-${SQLITE_RELEASE}.zip && \
cp sqlite-amalgamation-${SQLITE_RELEASE}/sqlite3.h ./sqlite3.h && \
cp sqlite-amalgamation-${SQLITE_RELEASE}/sqlite3ext.h ./sqlite3ext.h && \
wget https://www.sqlite.org/src/raw/ext/icu/icu.c?name=91c021c7e3e8bbba286960810fa303295c622e323567b2e6def4ce58e4466e60 -O icu.c && \
gcc -fPIC -shared icu.c `pkg-config --libs --cflags icu-uc icu-io` -o libicu.so
FROM python:3.10.13-alpine3.18 AS final
# install runtime dependencies
RUN apk update && apk add bash curl icu libpq mailcap
# create www-data user and group
RUN set -x ; \
addgroup -g 82 -S www-data ; \
adduser -u 82 -D -S -G www-data www-data && exit 0 ; exit 1
WORKDIR /etc/linkding
# copy prod dependencies
COPY --from=prod-deps /opt/venv /opt/venv
# copy output from build stage
COPY --from=python-build /etc/linkding/static static/
# copy compiled icu extension
COPY --from=compile-icu /etc/linkding/libicu.so libicu.so
# copy application code
COPY . .
# Expose uwsgi server at port 9090
EXPOSE 9090
# Activate virtual env
ENV VIRTUAL_ENV /opt/venv
ENV PATH /opt/venv/bin:$PATH
# Allow running containers as an an arbitrary user in the root group, to support deployment scenarios like OpenShift, Podman
RUN chmod g+w . && \
chmod +x ./bootstrap.sh
HEALTHCHECK --interval=30s --retries=3 --timeout=1s \
CMD curl -f http://localhost:${LD_SERVER_PORT:-9090}/${LD_CONTEXT_PATH}health || exit 1
CMD ["./bootstrap.sh"]

View File

@@ -1,11 +1,11 @@
FROM node:18.13.0-alpine AS node-build
FROM node:18.18.0-alpine AS node-build
WORKDIR /etc/linkding
# install build dependencies
COPY package.json package-lock.json ./
RUN npm install -g npm && \
npm install
# compile JS components
COPY . .
COPY rollup.config.js package.json package-lock.json ./
RUN npm install
# copy files needed for JS build
COPY bookmarks/frontend ./bookmarks/frontend
# run build
RUN npm run build
@@ -17,9 +17,13 @@ WORKDIR /etc/linkding
FROM python-base AS python-build
# install build dependencies
COPY requirements.txt requirements.txt
# remove playwright from requirements as there is not always a distro and it's not needed for the build
RUN sed -i '/playwright/d' requirements.txt
RUN pip install -U pip && pip install -Ur requirements.txt
# run Django part of the build
# copy files needed for Django build
COPY . .
COPY --from=node-build /etc/linkding .
# run Django part of the build
RUN python manage.py compilescss && \
python manage.py collectstatic --ignore=*.scss && \
python manage.py compilescss --delete-files

Binary file not shown.

View File

@@ -1,52 +1,82 @@
# Backups
This page describes some options on how to create backups.
Linkding stores all data in the application's data folder.
The full path to that folder in the Docker container is `/etc/linkding/data`.
As described in the installation docs, you should mount the `/etc/linkding/data` folder to a folder on your host system.
## What to backup
The data folder contains the following contents:
- `db.sqlite3` - the SQLite database
- `favicons` - folder that contains downloaded favicons
Linkding stores all data in a SQLite database, so all you need to backup are the contents of that database.
The following sections explain how to back up the individual contents.
The location of the database file is `data/db.sqlite3` in the application folder.
If you are using Docker then the full path in the Docker container is `/etc/linkding/data/db.sqlite`.
As described in the installation docs, you should mount the `/etc/linkding/data` folder to a folder on your host system, from which you then can execute the backup.
## Database
Below, we describe several methods to create a backup of the database:
This section describes several methods on how to back up the contents of the SQLite database.
- Manual backup using the export function from the UI
- Create a copy of the SQLite database with the SQLite backup function
- Create a plain textfile with the contents of the SQLite database with the SQLite dump function
> [!WARNING]
> While the SQLite database is just a single file, it is not recommended to just copy that file.
> This method is not transaction safe and may result in a [corrupted database](https://www.sqlite.org/howtocorrupt.html).
> Use one of the backup methods described below.
Choose the method that fits you best.
### Using the backup command
## Exporting from the UI
linkding includes a CLI command for creating a backup copy of the database.
The least technical option is to use the bookmark export in the UI.
Go to the settings page and open the *Data* tab.
Then click on the *Download* button to download an HTML file containing all your bookmarks.
You can backup this file on a drive, or an online file host.
## Using the SQLite backup function
Requires [SQLite](https://www.sqlite.org/index.html) to be installed on your host system.
With this method you create a new SQLite database, which is a copy of your linkding database.
This method uses the backup command in the [Command Line Shell For SQLite](https://sqlite.org/cli.html).
To create a backup, execute the following command:
```shell
sqlite3 db.sqlite3 ".backup 'backup.sqlite3'"
docker exec -it linkding python manage.py backup backup.sqlite3
```
After you have created the backup database `backup.sqlite` you have to move it to another system, for example with rsync.
This creates a `backup.sqlite3` file in the Docker container.
## Using the SQLite dump function
To copy the backup file to your host system, execute the following command:
```shell
docker cp linkding:/etc/linkding/backup.sqlite3 backup.sqlite3
```
This copies the backup file from the Docker container to the current folder on your host system.
Now you can move that file to your backup location.
To restore the backup, just copy the backup file to the data folder of your new installation and rename it to `db.sqlite3`. Then start the Docker container.
### Using the SQLite dump function
Requires [SQLite](https://www.sqlite.org/index.html) to be installed on your host system.
With this method you create a plain text file with the SQL statements to recreate the SQLite database.
To create a backup, execute the following command in the data folder:
```shell
sqlite3 db.sqlite3 .dump > backup.sql
```
This creates a `backup.sql` which you can copy to your backup location.
As this is a plain text file you can also commit it to any revision management system, like git.
Using git, you can commit the changes, followed by a git push to a remote repository.
As this is a plain text file you can commit it to any revision management system, like git.
Using git you can commit the changes, followed by a git push to a remote repository.
### Exporting bookmarks from the UI
This is the least technical option to back up bookmarks, but has several limitations:
- It does not export user profiles.
- It only exports your own bookmarks, not those of other users.
- It does not export archived bookmarks.
- It does not export URLs of snapshots on the Internet Archive Wayback machine.
- It does not export favicons.
Only use this method if you are fine with the above limitations.
To export bookmarks from the UI, open the general settings.
In the Export section, click on the *Download* button to download an HTML file containing all your bookmarks.
Then move that file to your backup location.
To restore bookmarks, open the general settings on your new installation.
In the Import section, click on the *Choose file* button to select the HTML file you downloaded before.
Then click on the *Import* button to import the bookmarks.
## Favicons
Doing a backup of the icons is optional, as they can be downloaded again.
If you choose not to back up the icons, you can just restore the database and then click the _Refresh Favicons_ button in the general settings.
This will download all missing icons again.
If you want to back up the icons, then you have to copy the `favicons` folder to your backup location.
To restore the icons, copy the `favicons` folder back to the data folder of your new installation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -40,23 +40,22 @@ Try using share button on an app, a new item `Send to...` should appear on the s
This how-to explains how to make use of the app shortcuts iOS app to create a share action that can be used in Safari for adding bookmarks to your linkding instance.
**In the shortcuts app:**
- create new shortcut
- go to shortcut details, enable to option to show the shortcut in share menu
- from the available share input types only select "URL"
- add Safari action "Show Web Page At"
- for URL enter your linkding instance URL and specifically point to the new bookmark form, then add the shortcut input variable from the list of suggested variables after the URL parameter. Visually it should look something like this: `https://linkding.mydomain.com/bookmarks/new?url=[Shortcut input]`, where `[Shortcut input]` is a visual block that was inserted after selecting the shortcut input variable suggestion. This is basically a placeholder that will get replaced with the actual URL that you want to bookmark. See screenshot at the end for an example on how this looks.
- save, give the shortcut a nice name + glyph
To install the shortcut:
- Download the [Shortcut](https://raw.githubusercontent.com/sissbruecker/linkding/master/docs/Add%20To%20Linkding.shortcut) on your iOS device
- Tap the downloaded file, which brings up the Shortcuts app
- Confirm that you want to add the shortcut
- In the shortcut, change `https://linkding.mydomain.com` to the URL of your linkding instance
- Confirm / close the shortcut
Example of how the shortcut configuration should look:
To use the shortcut:
- Open Safari and navigate to the page you want to bookmark
- Tap the share button
- Scroll down and tap "Add To Linkding"
- This opens linkding in a Safari overlay where you can configure the bookmark
- When you're done, tap "Save"
- After the bookmark is saved you can close the overlay
![Screenshot](/docs/ios-app-shortcut-example.png?raw=true "Screenshot demonstrating how to insert the input placeholder into the URL")
**Using the share action from Safari:**
- browse to the website that you want to share
- click the share button
- your new app shortcut should now be available as share action
- select the app shortcut
- this should open a new Safari overlay showing the add bookmark form with the URL field prefilled
- after saving the bookmark you can close the overlay and continue browsing
At the bottom of the share sheet there is a button for configuring share actions. You can use this to move the "Add To Linkding" action to the top of the share sheet if you like.
> [!NOTE]
> You can also check the [Community section](https://github.com/sissbruecker/linkding#community) for other pre-made shortcuts that you can use.

View File

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

View File

@@ -6,7 +6,7 @@ certifi==2023.7.22
charset-normalizer==2.1.1
click==8.1.3
confusable-homoglyphs==3.2.0
Django==4.1.10
Django==4.1.13
django-generate-secret-key==1.0.2
django-registration==3.3
django-sass-processor==1.2.1
@@ -23,7 +23,7 @@ soupsieve==2.3.2.post1
sqlparse==0.4.4
supervisor==4.2.4
typing-extensions==3.10.0.0
urllib3==1.26.11
urllib3==1.26.18
uWSGI==2.0.22
waybackpy==3.0.6
webencodings==0.5.1

View File

@@ -7,7 +7,7 @@ charset-normalizer==2.1.1
click==8.1.3
confusable-homoglyphs==3.2.0
coverage==5.5
Django==4.1.10
Django==4.1.13
django-appconf==1.0.5
django-compressor==4.1
django-debug-toolbar==3.6.0
@@ -33,6 +33,6 @@ six==1.16.0
soupsieve==2.3.2.post1
sqlparse==0.4.4
typing-extensions==3.10.0.0
urllib3==1.26.11
urllib3==1.26.18
waybackpy==3.0.6
webencodings==0.5.1

View File

@@ -3,6 +3,13 @@
version=$(<version.txt)
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
-f docker/default.Dockerfile \
-t sissbruecker/linkding:latest \
-t sissbruecker/linkding:$version \
--push .
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
-f docker/alpine.Dockerfile \
-t sissbruecker/linkding:latest-alpine \
-t sissbruecker/linkding:$version-alpine \
--push .

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env bash
docker build -t sissbruecker/linkding:local .
variant="${1:-default}"
docker build -f "docker/$variant.Dockerfile" -t sissbruecker/linkding:local .
docker rm -f linkding-local || true

View File

@@ -225,6 +225,11 @@ else:
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'data', 'db.sqlite3'),
'OPTIONS': LD_DB_OPTIONS,
# Creating a connection loads the ICU extension into the SQLite
# connection, and also loads an ICU collation. The latter causes a
# memory leak, so try to counter that by making connections indefinitely
# persistent.
'CONN_MAX_AGE': None
}
DATABASES = {

View File

@@ -1 +1 @@
1.22.0
1.23.1