diff --git a/.gitignore b/.gitignore index 918ee2a..8f15ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -222,5 +222,8 @@ typings/ .next ### Custom -polls -tmp + +# Collected static files for deployment +/static +# Build output, etc. +/tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2789f2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.7-slim-stretch + +# Install packages required for uswgi +RUN apt-get update +RUN apt-get -y install build-essential + +# Install requirements and uwsgi server for running python web apps +WORKDIR /etc/linkdings +COPY requirements.prod.txt ./requirements.txt +RUN pip install -U pip +RUN pip install -Ur requirements.txt +RUN pip install uwsgi + +# Copy application +COPY bookmarks ./bookmarks +COPY siteroot ./siteroot +COPY static ./static +COPY manage.py . +COPY uwsgi.ini . +COPY bootstrap.sh . +RUN ["chmod", "+x", "./bootstrap.sh"] + +EXPOSE 9090 + +# Start uwsgi server +CMD ["./bootstrap.sh"] diff --git a/bookmarks/admin.py b/bookmarks/admin.py index 743e7f5..7f4e65f 100644 --- a/bookmarks/admin.py +++ b/bookmarks/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin # Register your models here. -from .models import Bookmark +from bookmarks.models import Bookmark admin.site.register(Bookmark) diff --git a/bookmarks/management/commands/ensure_superuser.py b/bookmarks/management/commands/ensure_superuser.py new file mode 100644 index 0000000..06dbd2d --- /dev/null +++ b/bookmarks/management/commands/ensure_superuser.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + + +class Command(BaseCommand): + help = "Creates an admin user non-interactively if it doesn't exist" + + def add_arguments(self, parser): + parser.add_argument('--username', help="Admin's username") + parser.add_argument('--email', help="Admin's email") + parser.add_argument('--password', help="Admin's password") + + def handle(self, *args, **options): + User = get_user_model() + if not User.objects.filter(username=options['username']).exists(): + User.objects.create_superuser(username=options['username'], + email=options['email'], + password=options['password']) diff --git a/bookmarks/services/bookmarks.py b/bookmarks/services/bookmarks.py index f440b90..0453dfc 100644 --- a/bookmarks/services/bookmarks.py +++ b/bookmarks/services/bookmarks.py @@ -2,8 +2,8 @@ from django.contrib.auth.models import User from django.utils import timezone from bookmarks.models import Bookmark, BookmarkForm, parse_tag_string -from services.tags import get_or_create_tags -from services.website_loader import load_website_metadata +from bookmarks.services.tags import get_or_create_tags +from bookmarks.services.website_loader import load_website_metadata def create_bookmark(form: BookmarkForm, current_user: User): diff --git a/bookmarks/services/importer.py b/bookmarks/services/importer.py index a6809b4..f0343c2 100644 --- a/bookmarks/services/importer.py +++ b/bookmarks/services/importer.py @@ -5,7 +5,7 @@ from bs4 import BeautifulSoup from django.contrib.auth.models import User from bookmarks.models import Bookmark, parse_tag_string -from services.tags import get_or_create_tags +from bookmarks.services.tags import get_or_create_tags def import_netscape_html(html: str, user: User): diff --git a/bookmarks/urls.py b/bookmarks/urls.py index 99fc1ad..47c16aa 100644 --- a/bookmarks/urls.py +++ b/bookmarks/urls.py @@ -2,8 +2,7 @@ from django.conf.urls import url from django.urls import path from django.views.generic import RedirectView -from . import views -from .views import api +from bookmarks import views app_name = 'bookmarks' urlpatterns = [ @@ -15,5 +14,5 @@ urlpatterns = [ path('bookmarks//edit', views.edit, name='edit'), path('bookmarks//remove', views.remove, name='remove'), # API - path('api/website_metadata', api.website_metadata, name='api.website_metadata'), + path('api/website_metadata', views.api.website_metadata, name='api.website_metadata'), ] diff --git a/bookmarks/views/__init__.py b/bookmarks/views/__init__.py index 37f2e1d..5eba78a 100644 --- a/bookmarks/views/__init__.py +++ b/bookmarks/views/__init__.py @@ -1 +1,2 @@ from .bookmarks import * +from .api import * diff --git a/bookmarks/views/api.py b/bookmarks/views/api.py index 556d972..b4dd8fe 100644 --- a/bookmarks/views/api.py +++ b/bookmarks/views/api.py @@ -1,7 +1,7 @@ from django.contrib.auth.decorators import login_required from django.http import JsonResponse -from services.website_loader import load_website_metadata +from bookmarks.services.website_loader import load_website_metadata @login_required diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..450a108 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Bootstrap script that gets executed in new Docker containers + +# Set host name in settings if it was passed as environment variable +if [[ -v HOST_NAME ]] +then + printf "ALLOWED_HOSTS=['%s']" $HOST_NAME > ./siteroot/settings_custom.py +fi + +# Run database migration +python manage.py migrate + +# Start uwsgi server +uwsgi uwsgi.ini diff --git a/build-docker.sh b/build-docker.sh new file mode 100755 index 0000000..0813d47 --- /dev/null +++ b/build-docker.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +./build-static.sh +#docker build -t sissbruecker/linkding . +docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t sissbruecker/linkding:latest --push . diff --git a/build-static.sh b/build-static.sh new file mode 100755 index 0000000..01e4b1a --- /dev/null +++ b/build-static.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +rm -rf static +python manage.py compilescss +python manage.py collectstatic --ignore=*.scss +python manage.py compilescss --delete-files diff --git a/requirements.prod.txt b/requirements.prod.txt new file mode 100644 index 0000000..b01b55b --- /dev/null +++ b/requirements.prod.txt @@ -0,0 +1,18 @@ +beautifulsoup4==4.7.1 +certifi==2019.6.16 +chardet==3.0.4 +Django==2.2.2 +django-appconf==1.0.3 +django-compressor==2.3 +django-picklefield==2.0 +django-sass-processor==0.7.3 +django-widget-tweaks==1.4.5 +idna==2.8 +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 diff --git a/siteroot/settings.py b/siteroot/settings.py index 5a63570..ac558d8 100644 --- a/siteroot/settings.py +++ b/siteroot/settings.py @@ -1,5 +1,5 @@ """ -Django settings for siteroot project. +Django settings for linkding webapp. Generated by 'django-admin startproject' using Django 2.2.2. @@ -120,8 +120,12 @@ USE_TZ = True STATIC_URL = '/static/' +# Collect static files in static folder +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + # Location where generated CSS files are saved -SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR, 'bookmarks', 'static', 'build') +SASS_PROCESSOR_ENABLED = True +SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR, 'tmp', 'build', 'styles') # Add SASS preprocessor finder to resolve generated CSS STATICFILES_FINDERS = [ diff --git a/siteroot/settings_custom.py b/siteroot/settings_custom.py new file mode 100644 index 0000000..ec6596d --- /dev/null +++ b/siteroot/settings_custom.py @@ -0,0 +1 @@ +# Placeholder, can be overridden in Docker container with a custom settings like ALLOWED_HOSTS diff --git a/siteroot/settings_prod.py b/siteroot/settings_prod.py new file mode 100644 index 0000000..36bb43f --- /dev/null +++ b/siteroot/settings_prod.py @@ -0,0 +1,18 @@ +""" +Production settings for linkding webapp +""" + +# Start from development settings +# noinspection PyUnresolvedReferences +from .settings import * + +# Turn of debug mode +DEBUG = False +# Turn off SASS compilation +SASS_PROCESSOR_ENABLED = False + +ALLOWED_HOSTS = ['*'] + +# Import custom settings +# noinspection PyUnresolvedReferences +from .settings_custom import * diff --git a/uwsgi.ini b/uwsgi.ini new file mode 100644 index 0000000..f7710e5 --- /dev/null +++ b/uwsgi.ini @@ -0,0 +1,12 @@ +[uwsgi] +#socket = 127.0.0.1:3031 +http = :9090 +chdir = /etc/linkdings +module = siteroot.wsgi:application +env = DJANGO_SETTINGS_MODULE=siteroot.settings_prod +static-map = /static=static +processes = 4 +threads = 2 +pidfile = /tmp/project-master.pid +vacuum=True +stats = 127.0.0.1:9191