mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-09-10 02:59:44 +02:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
639629ddfe | ||
![]() |
2b342c0d56 | ||
![]() |
3ffec72d3e | ||
![]() |
edd958fff6 | ||
![]() |
2d22d6871e | ||
![]() |
5e8f5b2c58 | ||
![]() |
d5a83722de | ||
![]() |
5d8fdebb7c | ||
![]() |
f7bd6ccb31 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.27.1 (07/04/2024)
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
* Fix HTML snapshot errors related to single-file-cli by @sissbruecker in https://github.com/sissbruecker/linkding/pull/683
|
||||||
|
* Replace django-background-tasks with huey by @sissbruecker in https://github.com/sissbruecker/linkding/pull/657
|
||||||
|
* Add Authelia OIDC example to docs by @hugo-vrijswijk in https://github.com/sissbruecker/linkding/pull/675
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.27.0...v1.27.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.27.0 (01/04/2024)
|
## v1.27.0 (01/04/2024)
|
||||||
|
|
||||||
### What's Changed
|
### What's Changed
|
||||||
|
@@ -150,17 +150,27 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-autocomplete-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: var(--control-size);
|
||||||
|
min-height: var(--control-size);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-autocomplete-input input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-autocomplete.small .form-autocomplete-input {
|
.form-autocomplete.small .form-autocomplete-input {
|
||||||
height: var(--control-size-sm);
|
height: var(--control-size-sm);
|
||||||
min-height: var(--control-size-sm);
|
min-height: var(--control-size-sm);
|
||||||
padding: 0.05rem 0.3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-autocomplete.small .form-autocomplete-input input {
|
.form-autocomplete.small .form-autocomplete-input input {
|
||||||
width: 100%;
|
padding: 0.05rem 0.3rem;
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,3 +24,8 @@ class Command(BaseCommand):
|
|||||||
source_db.close()
|
source_db.close()
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"Backup created at {destination}"))
|
self.stdout.write(self.style.SUCCESS(f"Backup created at {destination}"))
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
"This backup method is deprecated and may be removed in the future. Please use the full_backup command instead, which creates backup zip file with all contents of the data folder."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
62
bookmarks/management/commands/full_backup.py
Normal file
62
bookmarks/management/commands/full_backup.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Creates a backup of the linkding data folder"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("backup_file", type=str, help="Backup zip file destination")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
backup_file = options["backup_file"]
|
||||||
|
with zipfile.ZipFile(backup_file, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||||
|
# Backup the database
|
||||||
|
self.stdout.write("Create database backup...")
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
backup_db_file = os.path.join(temp_dir, "db.sqlite3")
|
||||||
|
self.backup_database(backup_db_file)
|
||||||
|
zip_file.write(backup_db_file, "db.sqlite3")
|
||||||
|
|
||||||
|
# Backup the assets folder
|
||||||
|
if not os.path.exists(os.path.join("data", "assets")):
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING("No assets folder found. Skipping...")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write("Backup bookmark assets...")
|
||||||
|
assets_folder = os.path.join("data", "assets")
|
||||||
|
for root, _, files in os.walk(assets_folder):
|
||||||
|
for file in files:
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
zip_file.write(file_path, os.path.join("assets", file))
|
||||||
|
|
||||||
|
# Backup the favicons folder
|
||||||
|
if not os.path.exists(os.path.join("data", "favicons")):
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING("No favicons folder found. Skipping...")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write("Backup bookmark favicons...")
|
||||||
|
favicons_folder = os.path.join("data", "favicons")
|
||||||
|
for root, _, files in os.walk(favicons_folder):
|
||||||
|
for file in files:
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
zip_file.write(file_path, os.path.join("favicons", file))
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Backup created at {backup_file}"))
|
||||||
|
|
||||||
|
def backup_database(self, backup_db_file):
|
||||||
|
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(backup_db_file)
|
||||||
|
with backup_db:
|
||||||
|
source_db.backup(backup_db, pages=50, progress=progress)
|
||||||
|
backup_db.close()
|
||||||
|
source_db.close()
|
@@ -1,6 +1,7 @@
|
|||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -17,14 +18,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def create_snapshot(url: str, filepath: str):
|
def create_snapshot(url: str, filepath: str):
|
||||||
singlefile_path = settings.LD_SINGLEFILE_PATH
|
singlefile_path = settings.LD_SINGLEFILE_PATH
|
||||||
# singlefile_options = settings.LD_SINGLEFILE_OPTIONS
|
# parse string to list of arguments
|
||||||
|
singlefile_options = shlex.split(settings.LD_SINGLEFILE_OPTIONS)
|
||||||
temp_filepath = filepath + ".tmp"
|
temp_filepath = filepath + ".tmp"
|
||||||
|
# concat lists
|
||||||
args = [singlefile_path, url, temp_filepath]
|
args = [singlefile_path] + singlefile_options + [url, temp_filepath]
|
||||||
try:
|
try:
|
||||||
# Use start_new_session=True to create a new process group
|
# Use start_new_session=True to create a new process group
|
||||||
process = subprocess.Popen(args, start_new_session=True)
|
process = subprocess.Popen(args, start_new_session=True)
|
||||||
process.wait(timeout=60)
|
process.wait(timeout=settings.LD_SINGLEFILE_TIMEOUT_SEC)
|
||||||
|
|
||||||
# check if the file was created
|
# check if the file was created
|
||||||
if not os.path.exists(temp_filepath):
|
if not os.path.exists(temp_filepath):
|
||||||
|
@@ -239,6 +239,9 @@ def create_html_snapshot(bookmark: Bookmark):
|
|||||||
asset.save()
|
asset.save()
|
||||||
|
|
||||||
|
|
||||||
|
MAX_SNAPSHOT_FILENAME_LENGTH = 192
|
||||||
|
|
||||||
|
|
||||||
def _generate_snapshot_filename(asset: BookmarkAsset) -> str:
|
def _generate_snapshot_filename(asset: BookmarkAsset) -> str:
|
||||||
def sanitize_char(char):
|
def sanitize_char(char):
|
||||||
if char.isalnum() or char in ("-", "_", "."):
|
if char.isalnum() or char in ("-", "_", "."):
|
||||||
@@ -249,6 +252,13 @@ def _generate_snapshot_filename(asset: BookmarkAsset) -> str:
|
|||||||
formatted_datetime = asset.date_created.strftime("%Y-%m-%d_%H%M%S")
|
formatted_datetime = asset.date_created.strftime("%Y-%m-%d_%H%M%S")
|
||||||
sanitized_url = "".join(sanitize_char(char) for char in asset.bookmark.url)
|
sanitized_url = "".join(sanitize_char(char) for char in asset.bookmark.url)
|
||||||
|
|
||||||
|
# Calculate the length of the non-URL parts of the filename
|
||||||
|
non_url_length = len(f"{asset.asset_type}{formatted_datetime}__.html.gz")
|
||||||
|
# Calculate the maximum length for the URL part
|
||||||
|
max_url_length = MAX_SNAPSHOT_FILENAME_LENGTH - non_url_length
|
||||||
|
# Truncate the URL if necessary
|
||||||
|
sanitized_url = sanitized_url[:max_url_length]
|
||||||
|
|
||||||
return f"{asset.asset_type}_{formatted_datetime}_{sanitized_url}.html.gz"
|
return f"{asset.asset_type}_{formatted_datetime}_{sanitized_url}.html.gz"
|
||||||
|
|
||||||
|
|
||||||
|
@@ -556,6 +556,21 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(asset.file, expected_filename)
|
self.assertEqual(asset.file, expected_filename)
|
||||||
self.assertTrue(asset.gzip)
|
self.assertTrue(asset.gzip)
|
||||||
|
|
||||||
|
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
||||||
|
def test_create_html_snapshot_truncate_filename(self):
|
||||||
|
# Create a bookmark with a very long URL
|
||||||
|
long_url = "http://" + "a" * 300 + ".com"
|
||||||
|
bookmark = self.setup_bookmark(url=long_url)
|
||||||
|
|
||||||
|
tasks.create_html_snapshot(bookmark)
|
||||||
|
BookmarkAsset.objects.get(bookmark=bookmark)
|
||||||
|
|
||||||
|
# Run periodic task to process the snapshot
|
||||||
|
tasks._schedule_html_snapshots_task()
|
||||||
|
|
||||||
|
asset = BookmarkAsset.objects.get(bookmark=bookmark)
|
||||||
|
self.assertEqual(len(asset.file), 192)
|
||||||
|
|
||||||
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
||||||
def test_create_html_snapshot_should_handle_error(self):
|
def test_create_html_snapshot_should_handle_error(self):
|
||||||
bookmark = self.setup_bookmark(url="https://example.com")
|
bookmark = self.setup_bookmark(url="https://example.com")
|
||||||
|
@@ -49,3 +49,15 @@ class OidcSupportTest(TestCase):
|
|||||||
base_settings.AUTHENTICATION_BACKENDS,
|
base_settings.AUTHENTICATION_BACKENDS,
|
||||||
)
|
)
|
||||||
del os.environ["LD_ENABLE_OIDC"] # Remove the temporary environment variable
|
del os.environ["LD_ENABLE_OIDC"] # Remove the temporary environment variable
|
||||||
|
|
||||||
|
def test_default_settings(self):
|
||||||
|
os.environ["LD_ENABLE_OIDC"] = "True"
|
||||||
|
base_settings = importlib.import_module("siteroot.settings.base")
|
||||||
|
importlib.reload(base_settings)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
True,
|
||||||
|
base_settings.OIDC_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
|
||||||
|
del os.environ["LD_ENABLE_OIDC"]
|
||||||
|
@@ -3,7 +3,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from bookmarks.services import singlefile
|
from bookmarks.services import singlefile
|
||||||
|
|
||||||
@@ -50,3 +50,62 @@ class SingleFileServiceTestCase(TestCase):
|
|||||||
with mock.patch("subprocess.Popen"):
|
with mock.patch("subprocess.Popen"):
|
||||||
with self.assertRaises(singlefile.SingeFileError):
|
with self.assertRaises(singlefile.SingeFileError):
|
||||||
singlefile.create_snapshot("http://example.com", self.html_filepath)
|
singlefile.create_snapshot("http://example.com", self.html_filepath)
|
||||||
|
|
||||||
|
def test_create_snapshot_empty_options(self):
|
||||||
|
mock_process = mock.Mock()
|
||||||
|
mock_process.wait.return_value = 0
|
||||||
|
self.create_test_file()
|
||||||
|
|
||||||
|
with mock.patch("subprocess.Popen") as mock_popen:
|
||||||
|
singlefile.create_snapshot("http://example.com", self.html_filepath)
|
||||||
|
|
||||||
|
expected_args = [
|
||||||
|
"single-file",
|
||||||
|
"http://example.com",
|
||||||
|
self.html_filepath + ".tmp",
|
||||||
|
]
|
||||||
|
mock_popen.assert_called_with(expected_args, start_new_session=True)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
LD_SINGLEFILE_OPTIONS='--some-option "some value" --another-option "another value" --third-option="third value"'
|
||||||
|
)
|
||||||
|
def test_create_snapshot_custom_options(self):
|
||||||
|
mock_process = mock.Mock()
|
||||||
|
mock_process.wait.return_value = 0
|
||||||
|
self.create_test_file()
|
||||||
|
|
||||||
|
with mock.patch("subprocess.Popen") as mock_popen:
|
||||||
|
singlefile.create_snapshot("http://example.com", self.html_filepath)
|
||||||
|
|
||||||
|
expected_args = [
|
||||||
|
"single-file",
|
||||||
|
"--some-option",
|
||||||
|
"some value",
|
||||||
|
"--another-option",
|
||||||
|
"another value",
|
||||||
|
"--third-option=third value",
|
||||||
|
"http://example.com",
|
||||||
|
self.html_filepath + ".tmp",
|
||||||
|
]
|
||||||
|
mock_popen.assert_called_with(expected_args, start_new_session=True)
|
||||||
|
|
||||||
|
def test_create_snapshot_default_timeout_setting(self):
|
||||||
|
mock_process = mock.Mock()
|
||||||
|
mock_process.wait.return_value = 0
|
||||||
|
self.create_test_file()
|
||||||
|
|
||||||
|
with mock.patch("subprocess.Popen", return_value=mock_process):
|
||||||
|
singlefile.create_snapshot("http://example.com", self.html_filepath)
|
||||||
|
|
||||||
|
mock_process.wait.assert_called_with(timeout=60)
|
||||||
|
|
||||||
|
@override_settings(LD_SINGLEFILE_TIMEOUT_SEC=120)
|
||||||
|
def test_create_snapshot_custom_timeout_setting(self):
|
||||||
|
mock_process = mock.Mock()
|
||||||
|
mock_process.wait.return_value = 0
|
||||||
|
self.create_test_file()
|
||||||
|
|
||||||
|
with mock.patch("subprocess.Popen", return_value=mock_process):
|
||||||
|
singlefile.create_snapshot("http://example.com", self.html_filepath)
|
||||||
|
|
||||||
|
mock_process.wait.assert_called_with(timeout=120)
|
||||||
|
@@ -118,6 +118,7 @@ The following options can be configured:
|
|||||||
- `OIDC_RP_CLIENT_SECRET` - The client secret of the application.
|
- `OIDC_RP_CLIENT_SECRET` - The client secret of the application.
|
||||||
- `OIDC_RP_SIGN_ALGO` - The algorithm the OIDC provider uses to sign ID tokens. Default is `RS256`.
|
- `OIDC_RP_SIGN_ALGO` - The algorithm the OIDC provider uses to sign ID tokens. Default is `RS256`.
|
||||||
- `OIDC_USE_PKCE` - Whether to use PKCE for the OIDC flow. Default is `True`.
|
- `OIDC_USE_PKCE` - Whether to use PKCE for the OIDC flow. Default is `True`.
|
||||||
|
- `OIDC_VERIFY_SSL` - Whether to verify the SSL certificate of the OIDC provider. Set to `False` if using self-signed certificates or custom certificate authority. Default is `True`.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|
||||||
@@ -245,3 +246,20 @@ See the default URL for how to insert the placeholder to the favicon provider UR
|
|||||||
|
|
||||||
Alternative favicon providers:
|
Alternative favicon providers:
|
||||||
- DuckDuckGo: `https://icons.duckduckgo.com/ip3/{domain}.ico`
|
- DuckDuckGo: `https://icons.duckduckgo.com/ip3/{domain}.ico`
|
||||||
|
|
||||||
|
|
||||||
|
### `LD_SINGLEFILE_TIMEOUT_SEC`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### `LD_SINGLEFILE_OPTIONS`
|
||||||
|
|
||||||
|
Values: `String` | Default = None
|
||||||
|
|
||||||
|
When creating HTML archive snapshots, pass additional options to the `single-file` application that is used to create snapshots.
|
||||||
|
See `single-file --help` for complete list of arguments, or browse source: https://github.com/gildas-lormeau/single-file-cli/blob/master/options.js
|
||||||
|
|
||||||
|
Example: `LD_SINGLEFILE_OPTIONS=--user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0"`
|
||||||
|
@@ -4,24 +4,56 @@ Linkding stores all data in the application's data folder.
|
|||||||
The full path to that folder in the Docker container is `/etc/linkding/data`.
|
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.
|
As described in the installation docs, you should mount the `/etc/linkding/data` folder to a folder on your host system.
|
||||||
|
|
||||||
The data folder contains the following contents:
|
The data folder contains the following contents that are relevant for backups:
|
||||||
- `db.sqlite3` - the SQLite database
|
- `db.sqlite3` - the SQLite database
|
||||||
|
- `assets` - folder that contains HTML snapshots of bookmarks
|
||||||
- `favicons` - folder that contains downloaded favicons
|
- `favicons` - folder that contains downloaded favicons
|
||||||
|
|
||||||
The following sections explain how to back up the individual contents.
|
The following sections explain how to back up the individual contents.
|
||||||
|
|
||||||
## Database
|
## Full backup
|
||||||
|
|
||||||
This section describes several methods on how to back up the contents of the SQLite database.
|
linkding provides a CLI command to create a full backup of the data folder. This creates a zip file that contains backups of the database, assets, and favicons.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This method assumes that you are using the default SQLite database.
|
||||||
|
> If you are using a different database, such as Postgres, you'll have to back up the database and other contents of the data folder manually.
|
||||||
|
|
||||||
|
To create a full backup, execute the following command:
|
||||||
|
```shell
|
||||||
|
docker exec -it linkding python manage.py full_backup /etc/linkding/data/backup.zip
|
||||||
|
```
|
||||||
|
This creates a `backup.zip` file in the Docker container under `/etc/linkding/data`.
|
||||||
|
|
||||||
|
To copy the backup file to your host system, execute the following command:
|
||||||
|
```shell
|
||||||
|
docker cp linkding:/etc/linkding/data/backup.zip backup.zip
|
||||||
|
```
|
||||||
|
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 a backup:
|
||||||
|
- Extract the zip file in a folder of your new installation.
|
||||||
|
- Rename the extracted folder to `data`.
|
||||||
|
- When starting the Docker container, mount that folder to `/etc/linkding/data` as explained in the README.
|
||||||
|
- Then start the Docker container.
|
||||||
|
|
||||||
|
## Alternative backup methods
|
||||||
|
|
||||||
|
If you can't use the full backup method, this section describes alternatives how to back up the individual contents of the data folder.
|
||||||
|
|
||||||
|
### SQLite database backup
|
||||||
|
|
||||||
|
linkding includes a CLI command for creating a backup copy of the database.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> While the SQLite database is just a single file, it is not recommended to just copy that file.
|
> 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).
|
> 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.
|
> Use one of the backup methods described below.
|
||||||
|
|
||||||
### Using the backup command
|
> [!WARNING]
|
||||||
|
> This method is deprecated and may be removed in the future.
|
||||||
linkding includes a CLI command for creating a backup copy of the database.
|
> Please use the full backup method described above.
|
||||||
|
|
||||||
To create a backup, execute the following command:
|
To create a backup, execute the following command:
|
||||||
```shell
|
```shell
|
||||||
@@ -38,12 +70,12 @@ 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.
|
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
|
### SQLite database SQL dump
|
||||||
|
|
||||||
Requires [SQLite](https://www.sqlite.org/index.html) to be installed on your host system.
|
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.
|
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:
|
To create a backup, execute the following command in the data folder on your host system:
|
||||||
```shell
|
```shell
|
||||||
sqlite3 db.sqlite3 .dump > backup.sql
|
sqlite3 db.sqlite3 .dump > backup.sql
|
||||||
```
|
```
|
||||||
@@ -56,8 +88,8 @@ Using git, you can commit the changes, followed by a git push to a remote reposi
|
|||||||
This is the least technical option to back up bookmarks, but has several limitations:
|
This is the least technical option to back up bookmarks, but has several limitations:
|
||||||
- It does not export user profiles.
|
- It does not export user profiles.
|
||||||
- It only exports your own bookmarks, not those of other users.
|
- 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 URLs of snapshots on the Internet Archive Wayback machine.
|
||||||
|
- It does not export HTML snapshots of bookmarks. Even if you backup and restore the assets folder, the bookmarks will not be linked to the snapshots anymore.
|
||||||
- It does not export favicons.
|
- It does not export favicons.
|
||||||
|
|
||||||
Only use this method if you are fine with the above limitations.
|
Only use this method if you are fine with the above limitations.
|
||||||
@@ -70,7 +102,16 @@ 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.
|
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.
|
Then click on the *Import* button to import the bookmarks.
|
||||||
|
|
||||||
## Favicons
|
### Assets
|
||||||
|
|
||||||
|
If you are using the HTML snapshots feature, you should also do backups of the `assets` folder.
|
||||||
|
It contains the HTML snapshots files of your bookmarks which are referenced from the database.
|
||||||
|
|
||||||
|
To back up the assets, then you have to copy the `assets` folder to your backup location.
|
||||||
|
|
||||||
|
To restore the assets, copy the `assets` folder back to the data folder of your new installation.
|
||||||
|
|
||||||
|
### Favicons
|
||||||
|
|
||||||
Doing a backup of the icons is optional, as they can be downloaded again.
|
Doing a backup of the icons is optional, as they can be downloaded again.
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkding",
|
"name": "linkding",
|
||||||
"version": "1.27.1",
|
"version": "1.28.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@@ -212,6 +212,7 @@ if LD_ENABLE_OIDC:
|
|||||||
OIDC_RP_CLIENT_SECRET = os.getenv("OIDC_RP_CLIENT_SECRET")
|
OIDC_RP_CLIENT_SECRET = os.getenv("OIDC_RP_CLIENT_SECRET")
|
||||||
OIDC_RP_SIGN_ALGO = os.getenv("OIDC_RP_SIGN_ALGO", "RS256")
|
OIDC_RP_SIGN_ALGO = os.getenv("OIDC_RP_SIGN_ALGO", "RS256")
|
||||||
OIDC_USE_PKCE = os.getenv("OIDC_USE_PKCE", True) in (True, "True", "1")
|
OIDC_USE_PKCE = os.getenv("OIDC_USE_PKCE", True) in (True, "True", "1")
|
||||||
|
OIDC_VERIFY_SSL = os.getenv("OIDC_VERIFY_SSL", True) in (True, "True", "1")
|
||||||
|
|
||||||
# Enable authentication proxy support if configured
|
# Enable authentication proxy support if configured
|
||||||
LD_ENABLE_AUTH_PROXY = os.getenv("LD_ENABLE_AUTH_PROXY", False) in (True, "True", "1")
|
LD_ENABLE_AUTH_PROXY = os.getenv("LD_ENABLE_AUTH_PROXY", False) in (True, "True", "1")
|
||||||
@@ -294,6 +295,7 @@ LD_ENABLE_SNAPSHOTS = os.getenv("LD_ENABLE_SNAPSHOTS", False) in (
|
|||||||
)
|
)
|
||||||
LD_SINGLEFILE_PATH = os.getenv("LD_SINGLEFILE_PATH", "single-file")
|
LD_SINGLEFILE_PATH = os.getenv("LD_SINGLEFILE_PATH", "single-file")
|
||||||
LD_SINGLEFILE_OPTIONS = os.getenv("LD_SINGLEFILE_OPTIONS", "")
|
LD_SINGLEFILE_OPTIONS = os.getenv("LD_SINGLEFILE_OPTIONS", "")
|
||||||
|
LD_SINGLEFILE_TIMEOUT_SEC = float(os.getenv("LD_SINGLEFILE_TIMEOUT_SEC", 60))
|
||||||
|
|
||||||
# Monolith isn't used at the moment, as the local snapshot implementation
|
# Monolith isn't used at the moment, as the local snapshot implementation
|
||||||
# switched to single-file after the prototype. Keeping this around in case
|
# switched to single-file after the prototype. Keeping this around in case
|
||||||
|
@@ -1 +1 @@
|
|||||||
1.27.1
|
1.28.0
|
||||||
|
Reference in New Issue
Block a user