diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c6df9055 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +tmp/ +build/ +dist/ +*.egg-info/ +node_modules/ +umap/settings/local.py +Dockerfile +.git/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..883931d6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +FROM node:alpine AS vendors + +RUN apk add git + +WORKDIR /srv/umap + +COPY package.json . + +RUN npm install + +COPY . . + +RUN npm run vendors + +# This part installs deps needed at runtime. +FROM python:3.11-slim as common + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + tini \ + uwsgi \ + sqlite3 \ + libpq-dev \ + gdal-bin \ + && \ + apt-get autoremove -y && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# This part adds deps needed only at buildtime. +FROM common as build + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + binutils \ + libproj-dev \ + curl \ + git \ + gettext \ + python3-venv \ + libffi-dev \ + libtiff5-dev \ + libjpeg62-turbo-dev \ + zlib1g-dev \ + libfreetype6-dev \ + liblcms2-dev \ + libwebp-dev + +RUN python -m venv /venv + +WORKDIR /srv/umap + +COPY . /srv/umap + +RUN /venv/bin/pip install .[docker] + +FROM common + +COPY --from=build /srv/umap/docker/ /srv/umap/docker/ +COPY --from=build /venv/ /venv/ +COPY --from=vendors /srv/umap/umap/static/umap/vendors /srv/umap/umap/static/umap/vendors + +WORKDIR /srv/umap + +RUN mkdir -p /srv/umap/uploads + +ENV PYTHONUNBUFFERED=1 \ + PORT=8000 + +EXPOSE 8000 + +ENTRYPOINT ["/usr/bin/tini", "--"] + +CMD ["/srv/umap/docker/entrypoint.sh"] diff --git a/Makefile b/Makefile index 92deb39d..16166160 100644 --- a/Makefile +++ b/Makefile @@ -19,30 +19,7 @@ messages: cd umap && umap makemessages -l en node node_modules/leaflet-i18n/bin/i18n.js --dir_path=umap/static/umap/js/ --dir_path=umap/static/umap/vendors/measurable/ --locale_dir_path=umap/static/umap/locale/ --locale_codes=en --mode=json --clean --default_values vendors: - mkdir -p umap/static/umap/vendors/leaflet/ && cp -r node_modules/leaflet/dist/** umap/static/umap/vendors/leaflet/ - mkdir -p umap/static/umap/vendors/editable/ && cp -r node_modules/leaflet-editable/src/*.js umap/static/umap/vendors/editable/ - mkdir -p umap/static/umap/vendors/editable/ && cp -r node_modules/leaflet.path.drag/src/*.js umap/static/umap/vendors/editable/ - mkdir -p umap/static/umap/vendors/hash/ && cp -r node_modules/leaflet-hash/*.js umap/static/umap/vendors/hash/ - mkdir -p umap/static/umap/vendors/i18n/ && cp -r node_modules/leaflet-i18n/*.js umap/static/umap/vendors/i18n/ - mkdir -p umap/static/umap/vendors/editinosm/ && cp -r node_modules/leaflet-editinosm/{Leaflet.EditInOSM.*,edit-in-osm.png} umap/static/umap/vendors/editinosm/ - mkdir -p umap/static/umap/vendors/minimap/ && cp -r node_modules/leaflet-minimap/src/** umap/static/umap/vendors/minimap/ - mkdir -p umap/static/umap/vendors/loading/ && cp -r node_modules/leaflet-loading/src/** umap/static/umap/vendors/loading/ - mkdir -p umap/static/umap/vendors/markercluster/ && cp -r node_modules/leaflet.markercluster/dist/** umap/static/umap/vendors/markercluster/ - mkdir -p umap/static/umap/vendors/contextmenu/ && cp -r node_modules/leaflet-contextmenu/dist/** umap/static/umap/vendors/contextmenu/ - mkdir -p umap/static/umap/vendors/heat/ && cp -r node_modules/leaflet.heat/dist/** umap/static/umap/vendors/heat/ - mkdir -p umap/static/umap/vendors/fullscreen/ && cp -r node_modules/leaflet-fullscreen/dist/** umap/static/umap/vendors/fullscreen/ - mkdir -p umap/static/umap/vendors/toolbar/ && cp -r node_modules/leaflet-toolbar/dist/** umap/static/umap/vendors/toolbar/ - mkdir -p umap/static/umap/vendors/formbuilder/ && cp -r node_modules/leaflet-formbuilder/*.js umap/static/umap/vendors/formbuilder/ - mkdir -p umap/static/umap/vendors/measurable/ && cp -r node_modules/leaflet-measurable/Leaflet.Measurable.* umap/static/umap/vendors/measurable/ - mkdir -p umap/static/umap/vendors/photon/ && cp -r node_modules/leaflet.photon/*.js umap/static/umap/vendors/photon/ - mkdir -p umap/static/umap/vendors/csv2geojson/ && cp -r node_modules/csv2geojson/*.js umap/static/umap/vendors/csv2geojson/ - mkdir -p umap/static/umap/vendors/togeojson/ && cp -r node_modules/togeojson/*.js umap/static/umap/vendors/togeojson/ - mkdir -p umap/static/umap/vendors/osmtogeojson/ && cp -r node_modules/osmtogeojson/osmtogeojson.js umap/static/umap/vendors/osmtogeojson/ - mkdir -p umap/static/umap/vendors/georsstogeojson/ && cp -r node_modules/georsstogeojson/GeoRSSToGeoJSON.js umap/static/umap/vendors/georsstogeojson/ - mkdir -p umap/static/umap/vendors/togpx/ && cp -r node_modules/togpx/togpx.js umap/static/umap/vendors/togpx/ - mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js umap/static/umap/vendors/tokml - mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/{dist/L.Control.Locate.css,src/L.Control.Locate.js} umap/static/umap/vendors/locatecontrol/ - mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.js umap/static/umap/vendors/dompurify/ + npm run vendors installjs: npm install testjsfx: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..6e8f4201 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3' +services: + db: + image: postgis/postgis:14-3.3-alpine + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - db:/var/lib/postgresql/data + + app: + image: umap:1.3.0 + ports: + - "8001:8000" + environment: + - DATABASE_URL=postgis://postgres@db/postgres + - SECRET_KEY=some-long-and-weirdly-unrandom-secret-key + - SITE_URL=https://umap.local/ + - UMAP_ALLOW_ANONYMOUS=True + - DEBUG=1 + volumes: + - data:/srv/umap/uploads + +volumes: + data: + db: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..bf6c5165 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -eo pipefail + +source /venv/bin/activate + +# default variables +: "${SLEEP:=1}" +: "${TRIES:=60}" + +function wait_for_database {( + echo "Waiting for database to respond..." + tries=0 + while true; do + [[ $tries -lt $TRIES ]] || return + (echo "from django.db import connection; connection.connect()" | umap shell) >/dev/null + [[ $? -eq 0 ]] && return + sleep $SLEEP + tries=$((tries + 1)) + done +)} + +# first wait for the database +wait_for_database +# then migrate the database +umap migrate +# then collect static files +umap collectstatic --noinput +# compress static files +umap compress +# run uWSGI +exec uwsgi --ini docker/uwsgi.ini diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini new file mode 100644 index 00000000..2239f863 --- /dev/null +++ b/docker/uwsgi.ini @@ -0,0 +1,11 @@ +[uwsgi] +http = :$(PORT) +home = /venv +module = umap.wsgi:application +master = True +vacuum = True +max-requests = 5000 +processes = 4 +enable-threads = true +static-map = /static=/srv/umap/static +static-map = /uploads=/srv/umap/uploads diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 00000000..baceccb7 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,33 @@ +# Docker + +There is now an official [uMap](https://hub.docker.com/r/umap/umap) image. + +To run it with docker compose, use a `docker-compose.yml` like this: + +```yaml +version: '3' +services: + db: + image: postgis/postgis:14-3.3-alpine + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - db:/var/lib/postgresql/data + + app: + image: umap/umap:x.x.x + ports: + - "8001:8000" + environment: + - DATABASE_URL=postgis://postgres@db/postgres + - SECRET_KEY=some-long-and-weirdly-unrandom-secret-key + - SITE_URL=https://umap.local/ + - UMAP_ALLOW_ANONYMOUS=True + - DEBUG=1 + volumes: + - data:/srv/umap/uploads + +volumes: + data: + db: +``` diff --git a/mkdocs.yml b/mkdocs.yml index 1293c4ae..c09cc09d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,5 +7,6 @@ nav: - how-tos: - Ubuntu from scratch: ubuntu.md - Customize your uMap style: custom.md + - Install with Docker: docker.md - Changelog: changelog.md theme: readthedocs diff --git a/package.json b/package.json index 5254e0e5..94c1ed9f 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,14 @@ "happen": "~0.1.3", "lebab": "^3.2.1", "mocha": "^2.3.3", - "mocha-phantomjs": "^4.0.1", "optimist": "~0.4.0", - "phantomjs": "^1.9.18", "prettier": "^2.8.8", "sinon": "^15.1.0", "uglify-js": "~3.17.4" }, "scripts": { "test": "firefox test/index.html", - "build": "grunt" + "vendors": "scripts/vendorsjs.sh" }, "repository": { "type": "git", diff --git a/scripts/vendorsjs.sh b/scripts/vendorsjs.sh new file mode 100755 index 00000000..de375479 --- /dev/null +++ b/scripts/vendorsjs.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh + +mkdir -p umap/static/umap/vendors/leaflet/ && cp -r node_modules/leaflet/dist/** umap/static/umap/vendors/leaflet/ +mkdir -p umap/static/umap/vendors/editable/ && cp -r node_modules/leaflet-editable/src/*.js umap/static/umap/vendors/editable/ +mkdir -p umap/static/umap/vendors/editable/ && cp -r node_modules/leaflet.path.drag/src/*.js umap/static/umap/vendors/editable/ +mkdir -p umap/static/umap/vendors/hash/ && cp -r node_modules/leaflet-hash/*.js umap/static/umap/vendors/hash/ +mkdir -p umap/static/umap/vendors/i18n/ && cp -r node_modules/leaflet-i18n/*.js umap/static/umap/vendors/i18n/ +mkdir -p umap/static/umap/vendors/editinosm/ && cp -r node_modules/leaflet-editinosm/Leaflet.EditInOSM.* umap/static/umap/vendors/editinosm/ +mkdir -p umap/static/umap/vendors/editinosm/ && cp -r node_modules/leaflet-editinosm/edit-in-osm.png umap/static/umap/vendors/editinosm/ +mkdir -p umap/static/umap/vendors/minimap/ && cp -r node_modules/leaflet-minimap/src/** umap/static/umap/vendors/minimap/ +mkdir -p umap/static/umap/vendors/loading/ && cp -r node_modules/leaflet-loading/src/** umap/static/umap/vendors/loading/ +mkdir -p umap/static/umap/vendors/markercluster/ && cp -r node_modules/leaflet.markercluster/dist/** umap/static/umap/vendors/markercluster/ +mkdir -p umap/static/umap/vendors/contextmenu/ && cp -r node_modules/leaflet-contextmenu/dist/** umap/static/umap/vendors/contextmenu/ +mkdir -p umap/static/umap/vendors/heat/ && cp -r node_modules/leaflet.heat/dist/** umap/static/umap/vendors/heat/ +mkdir -p umap/static/umap/vendors/fullscreen/ && cp -r node_modules/leaflet-fullscreen/dist/** umap/static/umap/vendors/fullscreen/ +mkdir -p umap/static/umap/vendors/toolbar/ && cp -r node_modules/leaflet-toolbar/dist/** umap/static/umap/vendors/toolbar/ +mkdir -p umap/static/umap/vendors/formbuilder/ && cp -r node_modules/leaflet-formbuilder/*.js umap/static/umap/vendors/formbuilder/ +mkdir -p umap/static/umap/vendors/measurable/ && cp -r node_modules/leaflet-measurable/Leaflet.Measurable.* umap/static/umap/vendors/measurable/ +mkdir -p umap/static/umap/vendors/photon/ && cp -r node_modules/leaflet.photon/*.js umap/static/umap/vendors/photon/ +mkdir -p umap/static/umap/vendors/csv2geojson/ && cp -r node_modules/csv2geojson/*.js umap/static/umap/vendors/csv2geojson/ +mkdir -p umap/static/umap/vendors/togeojson/ && cp -r node_modules/togeojson/*.js umap/static/umap/vendors/togeojson/ +mkdir -p umap/static/umap/vendors/osmtogeojson/ && cp -r node_modules/osmtogeojson/osmtogeojson.js umap/static/umap/vendors/osmtogeojson/ +mkdir -p umap/static/umap/vendors/georsstogeojson/ && cp -r node_modules/georsstogeojson/GeoRSSToGeoJSON.js umap/static/umap/vendors/georsstogeojson/ +mkdir -p umap/static/umap/vendors/togpx/ && cp -r node_modules/togpx/togpx.js umap/static/umap/vendors/togpx/ +mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js umap/static/umap/vendors/tokml +mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/dist/L.Control.Locate.css umap/static/umap/vendors/locatecontrol/ +mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/src/L.Control.Locate.js umap/static/umap/vendors/locatecontrol/ +mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.js umap/static/umap/vendors/dompurify/ + +echo 'Done!' diff --git a/setup.cfg b/setup.cfg index 29352776..c9ac88a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ install_requires = Django>=4.1 django-agnocomplete==2.2.0 django-compressor==4.3.1 + django-environ==0.10.0 Pillow==9.5.0 psycopg2==2.9.6 requests==2.30.0 @@ -43,7 +44,8 @@ test = factory-boy==3.2.1 pytest==6.2.5 pytest-django==4.5.2 - +docker = + uwsgi==2.0.21 [options.entry_points] console_scripts = diff --git a/umap/settings/__init__.py b/umap/settings/__init__.py index fc25bed1..aef9ce63 100644 --- a/umap/settings/__init__.py +++ b/umap/settings/__init__.py @@ -17,31 +17,30 @@ if not path: path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'local.py') if not os.path.exists(path): - msg = ('You must configure UMAP_SETTINGS or define ' - '/etc/umap/umap.conf') - print(colorize(msg, fg='red')) - sys.exit(1) + print(colorize('No valid UMAP_SETTINGS found', fg='yellow')) + path = None -d = types.ModuleType('config') -d.__file__ = path -try: - with open(path) as config_file: - exec(compile(config_file.read(), path, 'exec'), d.__dict__) -except IOError as e: - msg = 'Unable to import {} from UMAP_SETTINGS'.format(path) - print(colorize(msg, fg='red')) - sys.exit(e) -else: - print('Loaded local config from', path) - for key in dir(d): - if key.isupper(): - value = getattr(d, key) - if key.startswith('LEAFLET_STORAGE'): - # Retrocompat pre 1.0, remove me in 1.1. - globals()['UMAP' + key[15:]] = value - elif key == 'UMAP_CUSTOM_TEMPLATES': - globals()['TEMPLATES'][0]['DIRS'].insert(0, value) - elif key == 'UMAP_CUSTOM_STATICS': - globals()['STATICFILES_DIRS'].insert(0, value) - else: - globals()[key] = value +if path: + d = types.ModuleType('config') + d.__file__ = path + try: + with open(path) as config_file: + exec(compile(config_file.read(), path, 'exec'), d.__dict__) + except IOError as e: + msg = 'Unable to import {} from UMAP_SETTINGS'.format(path) + print(colorize(msg, fg='red')) + sys.exit(e) + else: + print('Loaded local config from', path) + for key in dir(d): + if key.isupper(): + value = getattr(d, key) + if key.startswith('LEAFLET_STORAGE'): + # Retrocompat pre 1.0, remove me in 1.1. + globals()['UMAP' + key[15:]] = value + elif key == 'UMAP_CUSTOM_TEMPLATES': + globals()['TEMPLATES'][0]['DIRS'].insert(0, value) + elif key == 'UMAP_CUSTOM_STATICS': + globals()['STATICFILES_DIRS'].insert(0, value) + else: + globals()[key] = value diff --git a/umap/settings/base.py b/umap/settings/base.py index 4e051c40..ba739653 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -1,13 +1,24 @@ """Base settings shared by all environments""" # Import global settings to make it easier to extend settings. +from email.utils import parseaddr + from django.template.defaultfilters import slugify from django.conf.locale import LANG_INFO +import environ + +env = environ.Env() # ============================================================================= # Generic Django project settings # ============================================================================= -DEBUG = True + +INTERNAL_IPS = env.list('INTERNAL_IPS', default=['127.0.0.1']) +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) +ADMINS = tuple(parseaddr(email) for email in env.list('ADMINS', default=[])) + + +DEBUG = env.bool('DEBUG', default=False) SITE_ID = 1 # Add languages we're missing from Django @@ -92,7 +103,7 @@ LANGUAGES = ( ) # Make this unique, and don't share it with anybody. -SECRET_KEY = '' +SECRET_KEY = env('SECRET_KEY', default=None) INSTALLED_APPS = ( 'django.contrib.auth', @@ -200,21 +211,25 @@ MIDDLEWARE = ( # Auth / security # ============================================================================= -ENABLE_ACCOUNT_LOGIN = False +# Set to True if login into django account should be possible. Default is to +# only use OAuth flow. +ENABLE_ACCOUNT_LOGIN = env.bool("ENABLE_ACCOUNT_LOGIN", default=False) # ============================================================================= # Miscellaneous project settings # ============================================================================= -UMAP_ALLOW_ANONYMOUS = False +UMAP_ALLOW_ANONYMOUS = env.bool("UMAP_ALLOW_ANONYMOUS", default=False) + UMAP_EXTRA_URLS = { 'routing': 'http://www.openstreetmap.org/directions?engine=osrm_car&route={lat},{lng}&locale={locale}#map={zoom}/{lat}/{lng}', # noqa 'ajax_proxy': '/ajax-proxy/?url={url}&ttl={ttl}', 'search': 'https://photon.komoot.io/api/?', } -UMAP_KEEP_VERSIONS = 10 -SITE_URL = "http://umap.org" +UMAP_KEEP_VERSIONS = env.int('UMAP_KEEP_VERSIONS', default=10) +SITE_URL = env("SITE_URL", default="http://umap.org") +SHORT_SITE_URL = env('SHORT_SITE_URL', default=None) SITE_NAME = 'uMap' -UMAP_DEMO_SITE = False +UMAP_DEMO_SITE = env('UMAP_DEMO_SITE', default=False) UMAP_EXCLUDE_DEFAULT_MAPS = False UMAP_MAPS_PER_PAGE = 5 UMAP_MAPS_PER_PAGE_OWNER = 10 @@ -222,15 +237,18 @@ UMAP_SEARCH_CONFIGURATION = "simple" UMAP_FEEDBACK_LINK = "https://wiki.openstreetmap.org/wiki/UMap#Feedback_and_help" # noqa USER_MAPS_URL = 'user_maps' DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': 'umap', - } + 'default': env.db(default='postgis://localhost:5432/umap') } -UMAP_READONLY = False + +UMAP_READONLY = env('UMAP_READONLY', default=False) UMAP_GZIP = True LOCALE_PATHS = [os.path.join(PROJECT_DIR, 'locale')] +LEAFLET_LONGITUDE = env.int('LEAFLET_LONGITUDE', default=2) +LEAFLET_LATITUDE = env.int('LEAFLET_LATITUDE', default=51) +LEAFLET_ZOOM = env.int('LEAFLET_ZOOM', default=6) + + # ============================================================================= # Third party app settings # ============================================================================= @@ -243,3 +261,34 @@ SOCIAL_AUTH_NO_DEFAULT_PROTECTED_USER_FIELDS = True SOCIAL_AUTH_PROTECTED_USER_FIELDS = ("id", ) LOGIN_URL = "login" SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/login/popup/end/" + +AUTHENTICATION_BACKENDS = () + +SOCIAL_AUTH_OPENSTREETMAP_KEY = env('SOCIAL_AUTH_OPENSTREETMAP_KEY', default="") +SOCIAL_AUTH_OPENSTREETMAP_SECRET = env('SOCIAL_AUTH_OPENSTREETMAP_SECRET', default="") +if SOCIAL_AUTH_OPENSTREETMAP_KEY and SOCIAL_AUTH_OPENSTREETMAP_SECRET: + AUTHENTICATION_BACKENDS += ( + 'social_core.backends.openstreetmap.OpenStreetMapOAuth', + ) + +AUTHENTICATION_BACKENDS += ( + 'django.contrib.auth.backends.ModelBackend', +) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'level': 'ERROR', + 'filters': None, + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'ERROR', + }, + }, +}