Compare commits

...

11 Commits

Author SHA1 Message Date
Sascha Ißbrücker
e976fd054c Bump version 2021-03-28 12:14:11 +02:00
Sascha Ißbrücker
119d8f7efb Implement dark theme (#49) 2021-03-28 12:11:56 +02:00
Sascha Ißbrücker
3e5e825032 Update CHANGELOG.md 2021-03-20 12:41:47 +01:00
Sascha Ißbrücker
dc1f6f9c44 Update CHANGELOG.md 2021-03-20 12:39:15 +01:00
Sascha Ißbrücker
9e0114ea49 Bump version 2021-03-20 12:36:30 +01:00
Sascha Ißbrücker
84508e07cd Doc improvements (#97)
* Improve docs

* Improve docs
2021-03-20 11:58:20 +01:00
mattofr
496c5badbf Add backup document (#89)
* Added backup document

* Improve and reference backup docs

Co-authored-by: matto <matto@matto.nl>
Co-authored-by: Sascha Ißbrücker <sissbruecker@lyska.io>
2021-03-20 07:01:19 +01:00
dependabot[bot]
1c5d92dc73 Bump djangorestframework from 3.11.1 to 3.11.2 (#96)
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.11.1 to 3.11.2.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.11.1...3.11.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-20 05:58:58 +01:00
dependabot[bot]
b11444f98e Bump django from 2.2.13 to 2.2.18 (#94)
Bumps [django](https://github.com/django/django) from 2.2.13 to 2.2.18.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.13...2.2.18)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-20 05:58:35 +01:00
stranger-danger-zamu
ad070e7019 Multistage Dockerfile (#90)
* Multistage Dockerfile

* Fix final stage and improve image size + layer caching

Co-authored-by: Sascha Ißbrücker <sissbruecker@lyska.io>
2021-03-13 11:24:44 +01:00
Sascha Ißbrücker
6880c9ee56 Update CHANGELOG.md 2021-02-24 03:49:43 +01:00
39 changed files with 472 additions and 124 deletions

View File

@@ -5,13 +5,22 @@
/node_modules
/tmp
/docs
/static
/build
/.dockerignore
/.gitignore
/build-*.sh
/Dockerfile
/docker-compose.yml
/*.sh
/*.iml
/package*.json
/*.patch
/*.md
/*.js
# Whitelist files needed in build or prod image
!/rollup.config.js
!/bootstrap.sh
# Remove development settings
/siteroot/settings/dev.py

View File

@@ -1,5 +1,16 @@
# Changelog
## v1.4.1 (20/03/2021)
- Security patches
- Documentation improvements
---
## v1.4.0 (24/02/2021)
- [**enhancement**] Improve admin utilization [#76](https://github.com/sissbruecker/linkding/issues/76)
---
## v1.3.3 (18/02/2021)
- [**closed**] Missing "description" request body parameter in API causes 500 [#78](https://github.com/sissbruecker/linkding/issues/78)

View File

@@ -1,22 +1,52 @@
FROM python:3.7-slim-stretch
# Install packages required for uswgi
RUN apt-get update
RUN apt-get -y install build-essential
RUN apt-get -y install mime-support
# Install requirements and uwsgi server for running python web apps
FROM node:current-alpine AS node-build
WORKDIR /etc/linkding
COPY requirements.prod.txt ./requirements.txt
RUN pip install -U pip
RUN pip install -Ur requirements.txt
# Copy application
# install build dependencies
COPY package.json package-lock.json ./
RUN npm install -g npm && \
npm install
# compile JS components
COPY . .
RUN npm run build
FROM python:3.9-slim AS python-base
RUN apt-get update && apt-get -y install build-essential
WORKDIR /etc/linkding
FROM python-base AS python-build
# install build dependencies
COPY requirements.txt requirements.txt
RUN pip install -U pip && pip install -Ur requirements.txt
# run Django part of the build
COPY --from=node-build /etc/linkding .
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:3.9-slim as final
RUN apt-get update && apt-get -y install mime-support
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 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
# Run bootstrap logic
RUN ["chmod", "+x", "./bootstrap.sh"]
CMD ["./bootstrap.sh"]

View File

@@ -1,12 +1,30 @@
# linkding
*linkding* is a simple bookmark service that you can host yourself. It supports managing bookmarks, categorizing them with tags and has a search function. It provides a bookmarklet for quickly adding new bookmarks while browsing the web. It also supports import / export of bookmarks in the Netscape HTML format. And that's basically it 🙂.
*linkding* is a simple bookmark service that you can host yourself.
It's designed be to be minimal, fast and easy to set up using Docker.
The name comes from:
- *link* which is often used as a synonym for URLs and bookmarks in common language
- *Ding* which is german for *thing*
- ...so basically some thing for managing your links
**Feature Overview:**
- Tags for organizing bookmarks
- Search by text or tags
- Automatically provides titles and descriptions from linked websites
- Import and export bookmarks in Netscape HTML format
- Bookmark archive
- Extensions for [Firefox](https://addons.mozilla.org/de/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
- Bookmarklet that should work in most browsers
- Dark mode
- Easy to set up using Docker
- Uses SQLite as database
- Works without Javascript
- ...but has several UI enhancements when Javascript is enabled
- REST API for developing 3rd party apps
- Admin panel for user self-service and bulk operations
**Demo:** https://demo.linkding.link/ (configured with open registration)
**Screenshot:**
@@ -15,7 +33,7 @@ The name comes from:
## Installation
The easiest way to run linkding is to use [Docker](https://docs.docker.com/get-started/). The Docker image should be compatible with ARM platforms, so it can be run on a Raspberry Pi.
The easiest way to run linkding is to use [Docker](https://docs.docker.com/get-started/). The Docker image is compatible with ARM platforms, so it can be run on a Raspberry Pi.
There is also the option to set up the installation manually which I do not support, but can give some pointers on below.
@@ -49,7 +67,7 @@ docker-compose up -d
### User setup
Finally you need to create a user so that you can access the frontend. Replace the credentials in the following command and run it:
Finally you need to create a user so that you can access the application. Replace the credentials in the following command and run it:
**Docker**
```shell
@@ -67,10 +85,6 @@ The command will prompt you for a secure password. After the command has complet
If you can not or don't want to use Docker you can install the application manually on your server. To do so you can basically follow the steps from the *Development* section below while cross-referencing the `Dockerfile` and `bootstrap.sh` on how to make the application production-ready.
### Options
Check the [options document](Options.md) on how to configure your linkding installation.
### Hosting
The application runs in a web-server called [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) that is production-ready and that you can expose to the web. If you don't know how to configure your server to expose the application to the web there are several more steps involved. I can not support support the process here, but I can give some pointers on what to search for:
@@ -78,17 +92,17 @@ The application runs in a web-server called [uWSGI](https://uwsgi-docs.readthedo
- open the port that the application is running on in your servers firewall
- depending on your network configuration, forward the opened port in your network router, so that the application can be addressed from the internet using your public IP address and the opened port
### Backups
## Options
For backups you have two options: manually or automatic.
Check the [options document](docs/Options.md) on how to configure your linkding installation.
For manual backups you can export your bookmarks from the UI and store them on a backup device or online service.
## Backups
For automatic backups you want to backup the applications database. As described above, for production setups you should [mount](https://stackoverflow.com/questions/23439126/how-to-mount-a-host-directory-in-a-docker-container) the `/etc/linkding/data` directory from the Docker container to a directory on your host system. You can then use a backup tool of your choice to backup the contents of that directory.
Check the [backups document](docs/backup.md) for options on how to create backups.
## API
The application provides a REST API that can be used by 3rd party applications to manage bookmarks. Check the [API docs](API.md) for further information.
The application provides a REST API that can be used by 3rd party applications to manage bookmarks. Check the [API docs](docs/API.md) for further information.
## Troubleshooting
@@ -148,4 +162,4 @@ The frontend is now available under http://localhost:8000
## Community
- [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)
- [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)

View File

@@ -7,7 +7,7 @@ from django.utils.translation import ngettext, gettext
from rest_framework.authtoken.admin import TokenAdmin
from rest_framework.authtoken.models import Token
from bookmarks.models import Bookmark, Tag
from bookmarks.models import Bookmark, Tag, UserProfile
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
@@ -77,8 +77,24 @@ class AdminTag(admin.ModelAdmin):
), messages.SUCCESS)
class AdminUserProfileInline(admin.StackedInline):
model = UserProfile
can_delete = False
verbose_name_plural = 'Profile'
fk_name = 'user'
class AdminCustomUser(UserAdmin):
inlines = (AdminUserProfileInline,)
def get_inline_instances(self, request, obj=None):
if not obj:
return list()
return super(AdminCustomUser, self).get_inline_instances(request, obj)
linkding_admin_site = LinkdingAdminSite()
linkding_admin_site.register(Bookmark, AdminBookmark)
linkding_admin_site.register(Tag, AdminTag)
linkding_admin_site.register(User, UserAdmin)
linkding_admin_site.register(User, AdminCustomUser)
linkding_admin_site.register(Token, TokenAdmin)

View File

@@ -264,17 +264,4 @@
z-index: 2;
}
/* TODO: Should be read from theme */
.menu-item.selected > a {
background: #f1f1fc;
color: #5755d9;
}
.group-item, .group-item:hover {
color: #999999;
text-transform: uppercase;
background: none;
font-size: 0.6rem;
font-weight: bold;
}
</style>

View File

@@ -124,10 +124,4 @@
.menu.open {
display: block;
}
/* TODO: Should be read from theme */
.menu-item.selected > a {
background: #f1f1fc;
color: #5755d9;
}
</style>
</style>

View File

@@ -0,0 +1,43 @@
# Generated by Django 2.2.18 on 2021-03-26 22:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def forwards(apps, schema_editor):
User = apps.get_model('auth', 'User')
UserProfile = apps.get_model('bookmarks', 'UserProfile')
for user in User.objects.all():
try:
if user.profile:
continue
except UserProfile.DoesNotExist:
profile = UserProfile(user=user)
profile.save()
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('bookmarks', '0006_bookmark_is_archived'),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('theme',
models.CharField(choices=[('auto', 'Auto'), ('light', 'Light'), ('dark', 'Dark')], default='auto',
max_length=10)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile',
to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(forwards, reverse),
]

View File

@@ -2,7 +2,10 @@ from typing import List
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from bookmarks.utils import unique
from bookmarks.validators import BookmarkURLValidator
@@ -93,3 +96,33 @@ class BookmarkForm(forms.ModelForm):
class Meta:
model = Bookmark
fields = ['url', 'tag_string', 'title', 'description', 'auto_close', 'return_url']
class UserProfile(models.Model):
THEME_AUTO = 'auto'
THEME_LIGHT = 'light'
THEME_DARK = 'dark'
THEME_CHOICES = [
(THEME_AUTO, 'Auto'),
(THEME_LIGHT, 'Light'),
(THEME_DARK, 'Dark'),
]
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['theme']
@receiver(post_save, sender=get_user_model())
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=get_user_model())
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()

BIN
bookmarks/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -10,20 +10,22 @@ header {
.navbar-brand {
display: flex;
align-items: center;
.logo {
background-color: $primary-color;
color: $light-color;
padding: 14px;
width: 28px;
height: 28px;
}
h1 {
text-transform: uppercase;
display: inline-block;
margin: 0 0 0 8px;
}
}
.dropdown-toggle {
padding: 0;
}
}
@@ -39,9 +41,18 @@ h2 {
color: $gray-color-dark;
}
// Button color should not change for anchor elements
.btn:visited:not(.btn-primary) {
color: $primary-color;
// Fix up visited styles
a:visited {
color: $link-color;
}
a:visited:hover {
color: $link-color-dark;
}
.btn-link:visited:not(.btn-primary) {
color: $link-color;
}
.btn-link:visited:not(.btn-primary):hover {
color: $link-color-dark;
}
// Increase spacing between columns
@@ -57,4 +68,20 @@ h2 {
// Override border color for tab block
.tab-block {
border-bottom: solid 1px $border-color;
}
}
// Form auto-complete menu
.form-autocomplete .menu {
.menu-item.selected > a, .menu-item > a:hover {
background: $secondary-color;
color: $primary-color;
}
.group-item, .group-item:hover {
color: $gray-color;
text-transform: uppercase;
background: none;
font-size: 0.6rem;
font-weight: bold;
}
}

View File

@@ -39,7 +39,7 @@ ul.bookmark-list {
.description {
color: $gray-color-dark;
a {
a, a:visited:hover {
color: $alternative-color;
}
}
@@ -71,7 +71,7 @@ ul.bookmark-list {
.tag-cloud {
a {
a, a:visited:hover {
color: $alternative-color;
}

View File

@@ -0,0 +1,27 @@
/* Dark theme overrides */
/* Buttons */
.btn.btn-primary {
background: $dt-primary-button-color;
border-color: darken($dt-primary-button-color, 5%);
&:hover, &:active, &:focus {
background: darken($dt-primary-button-color, 5%);
border-color: darken($dt-primary-button-color, 10%);
}
}
/* Focus ring*/
a:focus, .btn:focus {
box-shadow: 0 0 0 .1rem rgba($primary-color, .5);
}
/* Forms */
.has-error .form-input, .form-input.is-error, .has-error .form-select, .form-select.is-error {
background: darken($error-color, 40%);
}
/* Pagination */
.pagination .page-item.active a {
background: $dt-primary-button-color;
}

View File

@@ -1,27 +0,0 @@
// Font sizes
$html-font-size: 18px !default;
//$alternative-color: #c84e00;
//$alternative-color: #FF84E8;
//$alternative-color: #98C1D9;
//$alternative-color: #7B287D;
$alternative-color: #05a6a3;
$alternative-color-dark: darken($alternative-color, 5%);
// Import Spectre CSS lib
@import "../../node_modules/spectre.css/src/spectre";
@import "../../node_modules/spectre.css/src/autocomplete";
// Import Spectre icons
@import "../../node_modules/spectre.css/src/icons/icons-core";
@import "../../node_modules/spectre.css/src/icons/icons-navigation";
@import "../../node_modules/spectre.css/src/icons/icons-action";
@import "../../node_modules/spectre.css/src/icons/icons-object";
// Import style modules
@import "base";
@import "util";
@import "shared";
@import "bookmarks";
@import "settings";
@import "auth";

View File

@@ -0,0 +1,17 @@
// Import custom variables
@import "variables-dark";
// Import Spectre CSS lib
@import "../../node_modules/spectre.css/src/spectre";
@import "../../node_modules/spectre.css/src/autocomplete";
// Import style modules
@import "base";
@import "util";
@import "shared";
@import "bookmarks";
@import "settings";
@import "auth";
// Dark theme overrides
@import "dark";

View File

@@ -0,0 +1,14 @@
// Import custom variables
@import "variables-light";
// Import Spectre CSS lib
@import "../../node_modules/spectre.css/src/spectre";
@import "../../node_modules/spectre.css/src/autocomplete";
// Import style modules
@import "base";
@import "util";
@import "shared";
@import "bookmarks";
@import "settings";
@import "auth";

View File

@@ -0,0 +1,28 @@
$html-font-size: 18px !default;
$body-bg: #161822 !default;
$bg-color: lighten($body-bg, 5%) !default;
$bg-color-light: lighten($body-bg, 5%) !default;
$border-color: #4C4E53 !default;
$border-color-dark: $border-color !default;
$body-font-color: #b5bec8 !default;
$light-color: #fafafa !default;
$gray-color: #7f879b !default;
$gray-color-dark: lighten($gray-color, 20%) !default;
$primary-color: #a8b1ff !default;
$primary-color-dark: saturate($primary-color, 5%) !default;
$secondary-color: lighten($body-bg, 10%) !default;
$link-color: $primary-color !default;
$link-color-dark: darken($link-color, 5%) !default;
$link-color-light: $link-color !default;
$alternative-color: #59bdb9;
$alternative-color-dark: #73f1eb;
/* Dark theme specific */
$dt-primary-button-color: #5761cb !default;

View File

@@ -0,0 +1,4 @@
$html-font-size: 18px !default;
$alternative-color: #05a6a3;
$alternative-color-dark: darken($alternative-color, 5%);

View File

@@ -2,7 +2,7 @@
<p class="empty-title h5">You have no bookmarks yet</p>
<p class="empty-subtitle">
You can get started by <a href="{% url 'bookmarks:new' %}">adding</a> bookmarks,
<a href="{% url 'bookmarks:settings.data' %}">importing</a> your existing bookmarks or configuring the
<a href="{% url 'bookmarks:settings.integrations' %}">bookmarklet</a>.
<a href="{% url 'bookmarks:settings.general' %}">importing</a> your existing bookmarks or configuring the
<a href="{% url 'bookmarks:settings.integrations' %}">browser extension</a> or the <a href="{% url 'bookmarks:settings.integrations' %}">bookmarklet</a>.
</p>
</div>

View File

@@ -7,7 +7,7 @@
{{ form.return_url|attr:"type:hidden" }}
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
{{ form.url|add_class:"form-input"|attr:"autofocus" }}
{{ form.url|add_class:"form-input"|attr:"autofocus"|attr:"placeholder: " }}
{% if form.url.errors %}
<div class="form-input-hint">
{{ form.url.errors }}

View File

@@ -12,14 +12,24 @@
<meta name="author" content="Sascha Ißbrücker">
<title>linkding</title>
{# Include SASS styles, files are resolved from bookmarks/styles #}
<link href="{% sass_src 'index.scss' %}" rel="stylesheet" type="text/css"/>
{# Include specific theme variant based on user profile setting #}
{% if request.user.profile.theme == 'light' %}
<link href="{% sass_src 'theme-light.scss' %}" rel="stylesheet" type="text/css"/>
{% elif request.user.profile.theme == 'dark' %}
<link href="{% sass_src 'theme-dark.scss' %}" rel="stylesheet" type="text/css"/>
{% else %}
{# Use auto theme as fallback #}
<link href="{% sass_src 'theme-dark.scss' %}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: dark)"/>
<link href="{% sass_src 'theme-light.scss' %}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: light)"/>
{% endif %}
</head>
<body>
<header class="navbar container grid-lg">
<section class="navbar-section">
<a href="/" class="navbar-brand text-bold">
<i class="logo icon icon-link s-circle"></i>
<img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
<h1>linkding</h1>
</a>
</section>

View File

@@ -7,12 +7,16 @@
</div>
{# Menu drop-down for smaller devices #}
<div class="show-md">
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary mr-2">
<i class="icon icon-plus"></i>
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary">
<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="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">
<i class="icon icon-menu icon-2x"></i>
<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" />
</svg>
</a>
<!-- menu component -->
<ul class="menu">
@@ -46,4 +50,4 @@
mobileNavMenuTrigger.addEventListener('blur', function () {
document.removeEventListener('click', mobileNavMenuOutsideClickHandler);
})
</script>
</script>

View File

@@ -20,11 +20,11 @@
{% endif %}
<div class="form-group">
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
{{ form.username|add_class:'form-input' }}
{{ form.username|add_class:'form-input'|attr:"placeholder: " }}
</div>
<div class="form-group">
<label class="form-label" for="{{ form.password.id_for_label }}">Password</label>
{{ form.password|add_class:'form-input' }}
{{ form.password|add_class:'form-input'|attr:"placeholder: " }}
</div>
<br/>

View File

@@ -1,10 +1,26 @@
{% extends "bookmarks/layout.html" %}
{% load widget_tweaks %}
{% block content %}
<div class="settings-page">
{% include 'settings/nav.html' %}
{# Profile section #}
<section class="content-area">
<h2>Profile</h2>
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
{% csrf_token %}
<div class="form-group">
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
{{ form.theme|add_class:"form-select col-2 col-sm-12" }}
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary mt-2">
</div>
</form>
</section>
{# Import section #}
<section class="content-area">
<h2>Import</h2>

View File

@@ -7,8 +7,15 @@
{# Integrations section #}
<section class="content-area">
<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://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>
<h2>Bookmarklet</h2>
<p>The bookmarklet is a quick way to add new bookmarks without opening the linkding application
<p>The bookmarklet is an alternative, cross-browser way to quickly add new bookmarks without opening the linkding application
first. Here's how it works:</p>
<ul>
<li>Drag the bookmarklet below into your browsers bookmark bar / toolbar</li>

View File

@@ -1,11 +1,11 @@
{% url 'bookmarks:settings.index' as index_url %}
{% url 'bookmarks:settings.data' as data_url %}
{% url 'bookmarks:settings.general' as general_url %}
{% url 'bookmarks:settings.integrations' as integrations_url %}
{% url 'bookmarks:settings.api' as api_url %}
<ul class="tab tab-block">
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == data_url%}active{% endif %}">
<a href="{% url 'bookmarks:settings.data' %}">Data</a>
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
<a href="{% url 'bookmarks:settings.general' %}">General</a>
</li>
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
@@ -16,8 +16,11 @@
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features &#010; such as user management and bulk operations.">
<a href="{% url 'admin:index' %}" target="_blank">
<span>Admin</span>
<i class="icon icon-share ml-1" style="font-size: 12px"></i>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ml-1" style="width: 1.2em; height: 1.2em; vertical-align: -0.2em;">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</li>
</ul>
<br>
<br>

View File

@@ -0,0 +1,12 @@
from django.test import TestCase
from django.contrib.auth.models import User
from bookmarks.models import UserProfile
class UserProfileTestCase(TestCase):
def test_create_user_should_init_profile(self):
user = User.objects.create_user('testuser', 'test@example.com', 'password123')
profile = UserProfile.objects.all().filter(user_id=user.id).first()
self.assertIsNotNone(profile)

View File

@@ -19,8 +19,8 @@ urlpatterns = [
path('bookmarks/<int:bookmark_id>/archive', views.bookmarks.archive, name='archive'),
path('bookmarks/<int:bookmark_id>/unarchive', views.bookmarks.unarchive, name='unarchive'),
# Settings
path('settings', views.settings.data, name='settings.index'),
path('settings/data', views.settings.data, name='settings.data'),
path('settings', views.settings.general, name='settings.index'),
path('settings/general', views.settings.general, name='settings.general'),
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
path('settings/api', views.settings.api, name='settings.api'),
path('settings/import', views.settings.bookmark_import, name='settings.import'),

View File

@@ -7,6 +7,7 @@ from django.shortcuts import render
from django.urls import reverse
from rest_framework.authtoken.models import Token
from bookmarks.models import UserProfileForm
from bookmarks.queries import query_bookmarks
from bookmarks.services.exporter import export_netscape_html
from bookmarks.services.importer import import_netscape_html
@@ -15,10 +16,18 @@ logger = logging.getLogger(__name__)
@login_required
def data(request):
def general(request):
if request.method == 'POST':
form = UserProfileForm(request.POST, instance=request.user.profile)
if form.is_valid():
form.save()
else:
form = UserProfileForm(instance=request.user.profile)
import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success')
import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors')
return render(request, 'settings/data.html', {
return render(request, 'settings/general.html', {
'form': form,
'import_success_message': import_success_message,
'import_errors_message': import_errors_message,
})
@@ -61,7 +70,7 @@ def bookmark_import(request):
messages.error(request, 'An error occurred during bookmark import.', 'bookmark_import_errors')
pass
return HttpResponseRedirect(reverse('bookmarks:settings.data'))
return HttpResponseRedirect(reverse('bookmarks:settings.general'))
@login_required
@@ -77,7 +86,7 @@ def bookmark_export(request):
return response
except:
return render(request, 'settings/data.html', {
return render(request, 'settings/general.html', {
'export_error': 'An error occurred during bookmark export.'
})

View File

@@ -2,7 +2,6 @@
version=$(<version.txt)
./build-static.sh
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
-t sissbruecker/linkding:latest \
-t sissbruecker/linkding:$version \

View File

52
docs/backup.md Normal file
View File

@@ -0,0 +1,52 @@
# Backups
This page describes some options on how to create backups.
## What to backup
Linkding stores all data in a SQLite database, so all you need to backup are the contents of that database.
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.
Below, we describe several methods to create a backup of the database:
- Manual backup using the export function from the UI
- Create a copy of the SQLite databse with the SQLite backup function
- Create a plain textfile with the contents of the SQLite database with the SQLite dump function
Choose the method that fits you best.
## Exporting from the UI
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).
```shell
sqlite3 db.sqlite3 ".backup 'backup.sqlite3'"
```
After you have created the backup database `backup.sqlite` you have to move it to another system, for example with rsync.
## 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.
```shell
sqlite3 db.sqlite3 .dump > backup.sql
```
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -1,22 +1,18 @@
beautifulsoup4==4.7.1
certifi==2019.6.16
chardet==3.0.4
Django==2.2.13
django-appconf==1.0.3
django-compressor==2.3
confusable-homoglyphs==3.2.0
Django==2.2.18
django-generate-secret-key==1.0.2
django-picklefield==2.0
django-registration==3.0.1
django-sass-processor==0.7.3
django-widget-tweaks==1.4.5
djangorestframework==3.11.1
djangorestframework==3.11.2
idna==2.8
pyparsing==2.4.7
pytz==2019.1
rcssmin==1.0.6
requests==2.22.0
rjsmin==1.1.0
six==1.12.0
soupsieve==1.9.2
sqlparse==0.3.0
urllib3==1.25.3

View File

@@ -2,15 +2,16 @@ beautifulsoup4==4.7.1
certifi==2019.6.16
chardet==3.0.4
confusable-homoglyphs==3.2.0
Django==2.2.13
Django==2.2.18
django-appconf==1.0.3
django-compressor==2.3
django-debug-toolbar==3.2
django-generate-secret-key==1.0.2
django-picklefield==2.0
django-registration==3.0.1
django-sass-processor==0.7.3
django-widget-tweaks==1.4.5
djangorestframework==3.11.1
djangorestframework==3.11.2
idna==2.8
libsass==0.19.2
pyparsing==2.4.7

View File

@@ -11,6 +11,14 @@ DEBUG = True
# Turn on SASS compilation
SASS_PROCESSOR_ENABLED = True
# Enable debug toolbar
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
INTERNAL_IPS = [
'127.0.0.1',
]
# Enable debug logging
LOGGING = {
'version': 1,

View File

@@ -17,7 +17,7 @@ from django.contrib.auth import views as auth_views
from django.urls import path, include
from bookmarks.admin import linkding_admin_site
from .settings import ALLOW_REGISTRATION
from .settings import ALLOW_REGISTRATION, DEBUG
urlpatterns = [
path('admin/', linkding_admin_site.urls),
@@ -28,5 +28,9 @@ urlpatterns = [
path('', include('bookmarks.urls')),
]
if DEBUG:
import debug_toolbar
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
if ALLOW_REGISTRATION:
urlpatterns.append(path('', include('django_registration.backends.one_step.urls')))

View File

@@ -1 +1 @@
1.4.0
1.5.0