diff --git a/.dockerignore b/.dockerignore index 9e95e1ed..f0539e4a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,16 +1,25 @@ .git -.buildkite +.gitignore +.env +.prettierignore +.stylelintrc +.editorconfig .vscode env mounts *.pyc __pycache__/ -settings.py -celeryconfig.py +web/settings.py +web/settings.*.py +web/celeryconfig.py +web/celeryconfig.*.py .eggs/ -build/ -html/ pip-log.txt .DS_Store *.swp .deploy.env +README.md +LICENSE +web/yoyo.*.ini +web/yoyo.ini +Makefile diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index 3ac3adc4..5406db43 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -32,7 +32,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 with: - file: ./Dockerfile + file: ./setup/web/Dockerfile context: . push: true tags: mltshp/mltshp-web:build-${{ env.build_number }} @@ -56,7 +56,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 with: - file: ./Dockerfile.worker + file: ./setup/worker/Dockerfile context: . push: true tags: mltshp/mltshp-worker:build-${{ env.build_number }} @@ -69,22 +69,22 @@ jobs: uses: actions/checkout@v4 - name: Start services with Docker Compose - run: docker compose -f release/docker-compose.yml up -d + run: docker compose -f setup/test/docker-compose.yml --project-directory=. up -d - name: Wait for services to be ready run: sleep 10 - name: Run tests - run: docker exec -t release-mltshp-1 ./run-tests.sh + run: docker exec -t mltshp-mltshp-1 ./test/run-tests.sh - name: Run Coveralls report env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - run: docker exec -t -e COVERALLS_REPO_TOKEN release-mltshp-1 ./coveralls-report.sh + run: docker exec -t -e COVERALLS_REPO_TOKEN mltshp-mltshp-1 ./test/coveralls-report.sh - name: Stop services and clean up run: | - docker compose -f release/docker-compose.yml down + docker compose -f setup/test/docker-compose.yml --project-directory=. down docker container prune -f staging: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7be9894e..8da9d4de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,15 +15,15 @@ jobs: uses: actions/checkout@v4 - name: Build and start services with Docker Compose - run: docker compose -f release/docker-compose.yml up -d --build + run: docker compose -f setup/test/docker-compose.yml --project-directory=. up -d --build - name: Wait for services to be ready run: sleep 10 - name: Run tests - run: docker exec -t release-mltshp-1 ./run-tests.sh + run: docker exec -t mltshp-mltshp-1 ./test/run-tests.sh - name: Stop services and clean up run: | - docker compose -f release/docker-compose.yml down + docker compose -f setup/test/docker-compose.yml --project-directory=. down docker container prune -f diff --git a/.gitignore b/.gitignore index 1934add2..0602e33b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ *.pyc -/settings.py -celeryconfig.py +web/settings.py +web/settings.*.py +web/yoyo.ini +web/yoyo.*.ini +web/celeryconfig.py .eggs/ -build/ -html/ .figleaf pip-log.txt .DS_Store @@ -11,7 +12,7 @@ pip-log.txt env mounts .env -.deploy.env +.*.env .coverage coverage.xml .vscode/ diff --git a/.stylelintrc b/.stylelintrc index 839eae9a..52b69940 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -40,7 +40,7 @@ "declaration-colon-space-before": "never", "declaration-empty-line-before": "never", "declaration-block-no-duplicate-properties": [ true, { - ignore: ["consecutive-duplicates-with-different-values"], + "ignore": ["consecutive-duplicates-with-different-values"], } ], "declaration-block-no-ignored-properties": true, "declaration-block-no-redundant-longhand-properties": true, @@ -82,10 +82,10 @@ "selector-list-comma-newline-after": "always", "selector-list-comma-space-before": "never", "rule-nested-empty-line-before": [ "always-multi-line", { - ignore: ["after-comment"], + "ignore": ["after-comment"], } ], "rule-non-nested-empty-line-before": [ "always-multi-line", { - ignore: ["after-comment"], + "ignore": ["after-comment"], } ], "media-feature-colon-space-after": "always", "media-feature-colon-space-before": "never", diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0f895a7a..00000000 --- a/Dockerfile +++ /dev/null @@ -1,72 +0,0 @@ -FROM ubuntu:24.04 -LABEL maintainer="brad@bradchoate.com" -ENV PYTHONUNBUFFERED=1 - -# Installs the base system dependencies for running the site. -# None of this will change with the codebase itself, so this -# whole layer and steps to build it should be cached. -RUN apt-get -y update && \ - apt-get install -y \ - supervisor \ - build-essential \ - pkg-config \ - python3-dev \ - python3-full \ - python3-pip \ - libmysqlclient-dev \ - mysql-client \ - libjpeg-dev \ - libcurl4-openssl-dev \ - curl \ - wget \ - libpcre3 \ - libpcre3-dev \ - libssl-dev \ - libffi-dev && \ - rm -rf /var/lib/apt/lists/* && \ - # install nginx + upload module - mkdir -p /tmp/install && \ - cd /tmp/install && \ - wget http://nginx.org/download/nginx-1.25.3.tar.gz && tar zxf nginx-1.25.3.tar.gz && \ - wget https://github.com/fdintino/nginx-upload-module/archive/2.3.0.tar.gz && tar zxf 2.3.0.tar.gz && \ - cd /tmp/install/nginx-1.25.3 && \ - ./configure \ - --with-http_ssl_module \ - --with-http_stub_status_module \ - --with-pcre \ - --sbin-path=/usr/sbin/nginx \ - --pid-path=/run/nginx.pid \ - --conf-path=/etc/nginx/nginx.conf \ - --error-log-path=/srv/mltshp.com/nginx-error.log \ - --http-log-path=/srv/mltshp.com/nginx-access.log \ - --add-module=/tmp/install/nginx-upload-module-2.3.0 && \ - make && make install && \ - mkdir -p /etc/nginx && \ - rm -rf /tmp/install && \ - groupadd mltshp --gid=1010 && \ - useradd mltshp --create-home --home-dir=/home/mltshp \ - --uid=1010 --gid=1010 && \ - mkdir -p /mnt/tmpuploads/0 /mnt/tmpuploads/1 /mnt/tmpuploads/2 \ - /mnt/tmpuploads/3 /mnt/tmpuploads/4 /mnt/tmpuploads/5 \ - /mnt/tmpuploads/6 /mnt/tmpuploads/7 /mnt/tmpuploads/8 \ - /mnt/tmpuploads/9 && \ - chmod 777 /mnt/tmpuploads/* && \ - mkdir -p /srv/mltshp.com/uploaded /srv/mltshp.com/logs && \ - chown -R mltshp:mltshp /srv/mltshp.com - -# Install python dependencies which will be cached on the -# contents of requirements.txt: -COPY requirements.txt /tmp -# It's okay to install to the system packages; we're in a container -RUN pip install --break-system-packages -r /tmp/requirements.txt && rm /tmp/requirements.txt - -# Copy configuration settings into place -COPY setup/production/supervisord-web.conf /etc/supervisor/conf.d/mltshp.conf -COPY setup/production/nginx.conf /etc/nginx/nginx.conf - -# Add "." for the app code itself (also allows for local dev) -ADD . /srv/mltshp.com/mltshp -WORKDIR /srv/mltshp.com/mltshp - -EXPOSE 80 -CMD ["/usr/bin/supervisord"] diff --git a/Dockerfile.worker b/Dockerfile.worker deleted file mode 100644 index cc8804cc..00000000 --- a/Dockerfile.worker +++ /dev/null @@ -1,45 +0,0 @@ -FROM ubuntu:24.04 -LABEL maintainer="brad@bradchoate.com" -ENV PYTHONUNBUFFERED=1 - -RUN apt-get -y update && \ - apt-get install -y \ - software-properties-common \ - build-essential \ - supervisor \ - cron \ - pkg-config \ - libmysqlclient-dev \ - mysql-client \ - python3-dev \ - python3-full \ - python3-pip \ - libjpeg-dev \ - libcurl4-openssl-dev \ - curl \ - run-one \ - ffmpeg \ - wget \ - vim \ - libpcre3 \ - libpcre3-dev \ - libssl-dev \ - libffi-dev && \ - rm -rf /var/lib/apt/lists/* && \ - groupadd mltshp --gid=1010 && \ - useradd mltshp --create-home --home-dir=/home/mltshp \ - --uid=1010 --gid=1010 && \ - mkdir -p /srv/mltshp.com/logs && \ - chown -R mltshp:mltshp /srv/mltshp.com - -COPY requirements.txt /tmp -RUN pip install --break-system-packages -r /tmp/requirements.txt && rm /tmp/requirements.txt - -COPY setup/production/supervisord-worker.conf /etc/supervisor/conf.d/mltshp.conf - -# NOTE: /srv/mltshp.com/logs should be a mounted volume for this image -ADD . /srv/mltshp.com/mltshp -WORKDIR /srv/mltshp.com/mltshp -RUN crontab -u mltshp setup/production/mltshp-worker--crontab - -CMD ["/usr/bin/supervisord"] diff --git a/Makefile b/Makefile index f63e59c9..1a702c9c 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,42 @@ -.PHONY: init start stop custom-build build shell test destroy migrate mysql +.PHONY: init start stop custom-build build shell test destroy migrate mysql straw init: - cp settings.example.py settings.py - cp celeryconfig.example.py celeryconfig.py - mkdir -p mounts/mysql mounts/logs mounts/fakes3 mounts/uploaded + cp web/settings.example.py web/settings.py + cp web/celeryconfig.example.py web/celeryconfig.py + mkdir -p mounts/mysql mounts/logs mounts/fakes3 mounts/uploaded \ + mounts/tmpuploads/0 mounts/tmpuploads/1 mounts/tmpuploads/2 \ + mounts/tmpuploads/3 mounts/tmpuploads/4 mounts/tmpuploads/5 \ + mounts/tmpuploads/6 mounts/tmpuploads/7 mounts/tmpuploads/8 \ + mounts/tmpuploads/9 start: - docker compose up -d + docker compose -f setup/web/docker-compose.dev.yml --project-directory=. up -d stop: - docker compose down + docker compose -f setup/web/docker-compose.dev.yml --project-directory=. down custom-build: @read -p "build tag (default is 'latest'): " build_tag; \ - docker build -t mltshp/mltshp-web:$${build_tag:-latest} + docker build -f setup/web/Dockerfile -t mltshp/mltshp-web:$${build_tag:-latest} build: - docker build -t mltshp/mltshp-web:latest . + docker build -f setup/web/Dockerfile -t mltshp/mltshp-web:latest . shell: - docker compose exec mltshp bash + docker compose -f setup/web/docker-compose.dev.yml --project-directory=. exec web ash test: - docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; python3 -u test.py $(TEST)" + docker compose -f setup/test/docker-compose.yml --project-directory=. exec web su mltshp -c "python3 -u test/test.py $(TEST)" destroy: - docker compose down && rm -rf mounts + docker compose -f setup/web/docker-compose.dev.yml --project-directory=. down && rm -rf mounts migrate: - docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; python3 migrate.py" + docker compose -f setup/web/docker-compose.dev.yml --project-directory=. exec web su mltshp -c "cd /srv/mltshp.com/mltshp; python3 migrate.py" mysql: - docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; mysql -u root --host mysql mltshp" + docker compose -f setup/web/docker-compose.dev.yml --project-directory=. exec web su mltshp -c "cd /srv/mltshp.com/mltshp; mysql -u root --host mysql mltshp" + +web/static/straw/compact.js: web/static/straw/source.js + closure-compiler --js web/static/straw/source.js --js_output_file web/static/straw/compact.js || echo "closure-compiler not found" + diff --git a/README.md b/README.md index 6b8f83c5..c6e2b6df 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## Project Description This project is the codebase for running [mltshp.com](https://mltshp.com). -It's a Python 3 application, utilizing a MySQL database, Amazon S3 for +It's a Python 3 application, utilizing a MySQL database, Backblaze B2 for asset storage, and RabbitMQ for background jobs. ## Development Environment @@ -97,26 +97,13 @@ The directory structure looks like this: * mysql/ * (mysql data files) -## AWS S3 Storage +## S3 Storage -MLTSHP utilizes AWS S3 for storing uploaded images. The development -environment provides a dummy S3 server for local operation. But it requires -a license key in order to use it. Visit [this page](https://supso.org/projects/fake-s3) -to obtain a license key. For individual developers and small organizations, -there is no cost. Add the following to a local `.env` file in the root -of the project: - -``` -FAKES3_LICENSE_KEY=your-license-key-here -``` +MLTSHP utilizes S3 for storing uploaded images. The development +environment provides a dummy S3 server for local operation. You will find any uploaded files under the `mounts/fakes3' directory. -**Note:** As of this time, the Docker image for the fake S3 server is -incompatible with Apple Silicon CPU architectures. If you are using a -computer with an Apple ARM CPU, you will need to use a real S3 bucket -(see below). - If you would rather use a real S3 bucket, you can do that too. Create one and then assign these in your local settings.py file: @@ -203,6 +190,23 @@ $ make stop Then, run another `make start`. +## Straw + +There's one resource that requires manual compilation when it is updated. +That's the `static/straw/source.js` script. This script is for the bookmkarklet +for MLTSHP. It can be compiled using this command: + +```shell +$ make straw +``` + +After this, you'll need to check in the modified source.js and compact.js +files. + +The command requires that the `closure-compiler` binary is available. This +can be installed via Homebrew (`brew install closure-compiler`) or your +favorite package manager. + ## Relationship with MLTSHP-Patterns The CSS in this repo is just the compiled version of the styles from the MLTSHP diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4ea6c322..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,56 +0,0 @@ -services: - mltshp: - build: . - env_file: - - .env - image: mltshp/mltshp-web:latest - volumes: - - .:/srv/mltshp.com/mltshp - - ./mounts/logs:/srv/mltshp.com/logs - - ./mounts/uploaded:/srv/mltshp.com/uploaded - - ./setup/dev/nginx.conf:/etc/nginx/nginx.conf - - ./setup/dev/supervisord.conf:/etc/supervisor/conf.d/mltshp.conf - ports: - - "8000:80" - depends_on: - - mysql - - fakes3 - links: - - mysql - - fakes3 - networks: - app_net: - aliases: - - mltshp.localhost - fakes3: - build: ./setup/dev/fakes3 - env_file: - - .env - entrypoint: fakes3 --root /srv --license ${FAKES3_LICENSE_KEY} --port 4567 - ports: - - "4567:4567" - volumes: - - ./mounts/fakes3:/srv - networks: - app_net: - aliases: - - mltshp-testing.fakes3 - - mltshp-dev.fakes3 - mysql: - image: mysql:8 - ports: - - "3306:3306" - volumes: - - ./mounts/mysql:/var/lib/mysql - - ./setup/dev/mysql-conf.d:/etc/mysql/conf.d - - ./setup/db-install.sql:/docker-entrypoint-initdb.d/00_db-install.sql - - ./setup/db-fixtures.sql:/docker-entrypoint-initdb.d/01_db-fixtures.sql - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - MYSQL_DATABASE: "mltshp" - MYSQL_USER: "mltshp" - networks: - - app_net - -networks: - app_net: diff --git a/lib/feathers/__init__.py b/lib/feathers/__init__.py deleted file mode 100644 index 9f9f9dc7..00000000 --- a/lib/feathers/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -import sys -import urllib.request, urllib.parse, urllib.error -import urllib.parse -import hmac -import hashlib -import binascii -import time -import uuid - -import tornado.httpclient -import tornado.ioloop - -class Feathers(object): - """ - An experimental, minimalist twitter client to be used with Tornado. - - Translates an API method: - - (GET) statuses/public_timeline.format?include_entities=1 - - Using chained property-access syntax: - - feather = Feathers(key='your app key', secret='your app secret') - feather.statuses.public_timeline.get({'include_entitites' : 1}) - """ - __current_method = '' - - endpoint = "https://api.twitter.com/" - api_version = '1' - format = 'json' - key = '' - secret = '' - - def __init__(self, key='', secret=''): - assert key - assert secret - self.key = key - self.secret = secret - - def __getattr__(self, name): - """ - Missing attributes get concatenated to final api path. Each - new attribute is seperated by a / Returns self so its chainable. - Gets reset when get() is called. - """ - self.__current_method = self.__current_method + '/' + name - return self - - def get(self, params={}, headers={}, callback=None, token_key=None, token_secret=None): - """ - Send a GET request. - - If a user's token_key and token_secret are passed in, prepare an authenticated request - on behalf of the user. - """ - base_url = self._build_base_url(self.__current_method) - if token_key and token_secret: - params['oauth_consumer_key'] = self.key - params['oauth_signature_method'] ="HMAC-SHA1" - params['oauth_timestamp'] = str(int(time.time())) - params['oauth_nonce'] = binascii.b2a_hex(uuid.uuid4().bytes) - params['oauth_version'] = "1.0" - params['oauth_token'] = token_key - signature = self._oauth_signature(self.secret, "GET", base_url, params, token_secret) - params['oauth_signature'] = signature - url = self._build_url(base_url, params) - self.__current_method = '' - return self._fetch(url, headers, callback) - - def _build_base_url(self, method): - return self.endpoint + self.api_version + method + '.' + self.format - - def _build_url(self, base_url, params): - query = '' if not params else '?' + urllib.parse.urlencode(params) - return base_url + query - - async def _fetch(self, url, headers={}, callback=None): - """ - Make the request. If an IOloop is available make request asynchronous and use the - passed in callback if it's provided. - """ - request = tornado.httpclient.HTTPRequest(url=url, method="GET", headers=headers) - if tornado.ioloop.IOLoop.initialized(): - http = tornado.httpclient.AsyncHTTPClient() - fut = http.fetch(request) - response = await fut - callback(response) - else: - http = tornado.httpclient.HTTPClient() - return http.fetch(request) - - @classmethod - def _oauth_signature(self, consumer_secret, method, url, parameters={}, token_secret=None): - """ - From tornado.auth - - Calculates the HMAC-SHA1 OAuth 1.0 signature for the given request. - - See http://oauth.net/core/1.0/#signing_process - """ - parts = urllib.parse.urlparse(url) - scheme, netloc, path = parts[:3] - normalized_url = scheme.lower() + "://" + netloc.lower() + path - - base_elems = [] - base_elems.append(method.upper()) - base_elems.append(normalized_url) - base_elems.append("&".join("%s=%s" % (k, self._oauth_escape(str(v))) - for k, v in sorted(parameters.items()))) - base_string = "&".join(self._oauth_escape(e) for e in base_elems) - - key_elems = [consumer_secret] - key_elems.append(token_secret if token_secret else "") - key = "&".join(key_elems) - - hash = hmac.new(key.encode("ascii"), base_string.encode("ascii"), hashlib.sha1) - return binascii.b2a_base64(hash.digest())[:-1] - - @classmethod - def _oauth_escape(self, val): - """ - From tornado.auth - """ - if isinstance(val, str): - val = val.encode("utf-8") - return urllib.parse.quote(val, safe="~") diff --git a/release/docker-compose.yml b/release/docker-compose.yml deleted file mode 100644 index ca42909e..00000000 --- a/release/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -services: - mltshp: - image: "mltshp/mltshp-web:build-${build_number}" - build: .. - volumes: - - ./settings.py:/srv/mltshp.com/mltshp/settings.py - - ../.git:/srv/mltshp.com/mltshp/.git - - ./supervisord.conf:/etc/supervisor/conf.d/mltshp.conf - ports: - - "8001:80" - depends_on: - - mysql - links: - - mysql - networks: - app_net: - aliases: - - mltshp.dev - mysql: - image: mysql:8 - ports: - - "3306:3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - MYSQL_DATABASE: "mltshp_testing" - MYSQL_USER: "mltshp" - networks: - - app_net - -networks: - app_net: diff --git a/release/supervisord.conf b/release/supervisord.conf deleted file mode 100644 index 54296b8b..00000000 --- a/release/supervisord.conf +++ /dev/null @@ -1,2 +0,0 @@ -[supervisord] -nodaemon=true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index bee4fe7c..00000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -amqplib==1.0.2 -beautifulsoup4==4.12.3 -boto3==1.35.86 -celery==5.4.0 -ffmpy==0.5.0 -kombu==5.3.7 -mock==5.1.0 -mysqlclient==2.2.6 -Pillow==11.0.0 -pycurl==7.45.4 -pyOpenSSL==24.3.0 -python-dateutil==2.9.0.post0 -python-postmark==0.6.0 -requests==2.32.3 -setuptools==75.8.0 -stripe==11.4.1 -tornado==6.4.2 -yoyo-migrations==8.2.0 diff --git a/run-tests.sh b/run-tests.sh deleted file mode 100755 index aeaec8f6..00000000 --- a/run-tests.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# exit if any command fails (e); strict variable substitution (u); -# set exit code to non-zero for any failed piped commands (o pipefail) -# See also: http://redsymbol.net/articles/unofficial-bash-strict-mode/ -set -euo pipefail - -export PYTHONUNBUFFERED=1 - -# These commands expect to be run in a Docker container -pip3 install --break-system-packages -r requirements-test.txt; -coverage run --source=handlers,models,tasks,lib test.py -coverage xml diff --git a/setup/db-drop.sql b/setup/database/db-drop.sql similarity index 100% rename from setup/db-drop.sql rename to setup/database/db-drop.sql diff --git a/setup/db-fixtures.sql b/setup/database/db-fixtures.sql similarity index 100% rename from setup/db-fixtures.sql rename to setup/database/db-fixtures.sql diff --git a/setup/db-install.sql b/setup/database/db-install.sql similarity index 100% rename from setup/db-install.sql rename to setup/database/db-install.sql diff --git a/setup/db-truncate.sql b/setup/database/db-truncate.sql similarity index 100% rename from setup/db-truncate.sql rename to setup/database/db-truncate.sql diff --git a/setup/dev/fakes3/Dockerfile b/setup/dev/fakes3/Dockerfile deleted file mode 100644 index de8f6717..00000000 --- a/setup/dev/fakes3/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM ruby:2.7 as builder - -ENV FAKES3_VERSION 2.0.0 - -RUN gem install fakes3 -v ${FAKES3_VERSION} \ - && rm -rf /usr/local/bundle/cache/* - -FROM ruby:2.7-slim - -COPY --from=builder /usr/local/bundle /usr/local/bundle/ - -VOLUME /srv -RUN mkdir -p /srv \ - && chown nobody:nogroup /srv \ - && chmod 750 /srv \ - && ln -s /usr/local/bundle/bin/fakes3 /usr/bin/fakes3 -WORKDIR /srv - -EXPOSE 4567 - -USER nobody -ENTRYPOINT ["fakes3", "--port", "4567"] -CMD ["--root", "/srv"] diff --git a/setup/dev/start-nginx.sh b/setup/dev/start-nginx.sh deleted file mode 100644 index d4e2a157..00000000 --- a/setup/dev/start-nginx.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo resolver $(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf) ";" > /etc/nginx/resolvers.conf -exec /usr/sbin/nginx -g 'daemon off;' \ No newline at end of file diff --git a/setup/dev/mysql-conf.d/mltshp.cnf b/setup/mysql/dev.conf similarity index 100% rename from setup/dev/mysql-conf.d/mltshp.cnf rename to setup/mysql/dev.conf diff --git a/setup/production/mysql.conf b/setup/mysql/prod.conf similarity index 88% rename from setup/production/mysql.conf rename to setup/mysql/prod.conf index da44f0d8..2610191e 100644 --- a/setup/production/mysql.conf +++ b/setup/mysql/prod.conf @@ -8,11 +8,11 @@ thread_cache_size = 8 query_cache_limit = 1M query_cache_size = 32M query_cache_type = 1 -innodb_buffer_pool_size = 8G +innodb_buffer_pool_size = 12G innodb_ft_min_token_size = 2 # # Here you can see queries with especially long duration #log_slow_queries = /var/log/mysql/mysql-slow.log #long_query_time = 2 -#log-queries-not-using-indexes \ No newline at end of file +#log-queries-not-using-indexes diff --git a/setup/dev/nginx.conf b/setup/nginx/dev.conf similarity index 98% rename from setup/dev/nginx.conf rename to setup/nginx/dev.conf index 6397b275..124da6df 100644 --- a/setup/dev/nginx.conf +++ b/setup/nginx/dev.conf @@ -6,6 +6,8 @@ worker_rlimit_nofile 20480; error_log /srv/mltshp.com/logs/error.log; +include /etc/nginx/modules/*.conf; + events { worker_connections 20480; use epoll; @@ -16,7 +18,7 @@ http { default_type application/octet-stream; upstream frontends { - server 127.0.0.1:8000; + server mltshp-web:8001; } sendfile on; diff --git a/setup/production/nginx.conf b/setup/nginx/prod.conf similarity index 99% rename from setup/production/nginx.conf rename to setup/nginx/prod.conf index 1c640ab9..e1a66150 100644 --- a/setup/production/nginx.conf +++ b/setup/nginx/prod.conf @@ -4,6 +4,8 @@ worker_processes auto; worker_rlimit_nofile 20480; +include /etc/nginx/modules/*.conf; + events { worker_connections 20480; use epoll; diff --git a/setup/production/update_linode_web_firewall.py b/setup/production/update_linode_web_firewall.py new file mode 100755 index 00000000..c8e7d22b --- /dev/null +++ b/setup/production/update_linode_web_firewall.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +# This script makes dynamic updates to our Linode web firewall, allowing access +# from the published Fastly IP address list. It should be run as a cron job, updating +# on a regular basis. It only applies updates when changes are detected, comparing +# against the existing rules. Linode issues email notifications when the firewall rules +# change, so we shouldn't blindly make updates, but only when changes are required. + +import os, requests + +# Retrieve settings from the environment +linode_api_token = os.environ.get("LINODE_API_TOKEN") +if not linode_api_token: + print("LINODE_API_TOKEN not set") + exit(1) + +linode_firewall_label = "mltshp-web-us-ord" +linode_inbound_rule_label = "Fastly" + +all_firewalls = requests.get( + "https://api.linode.com/v4/networking/firewalls", + headers={ + "accept": "application/json", + "authorization": f"Bearer { linode_api_token }", + }).json() + +web_firewall = next(iter([fw + for fw in all_firewalls["data"] + if fw["label"] == linode_firewall_label]), None) +if not web_firewall: + print(f"No Linode firewall found with label '{ linode_firewall_label }'") + exit(1) + +# Look for an existing Fastly rule and gather the IP addresses for comparison +# with what is currently provided by Fastly. +fastly_inbound = next(iter([data + for data in web_firewall["rules"]["inbound"] + if data["label"] == linode_inbound_rule_label]), None) +other_inbound = [data + for data in web_firewall["rules"]["inbound"] + if data["label"] != linode_inbound_rule_label] +if fastly_inbound: + existing_ipv4s = set(fastly_inbound["addresses"]["ipv4"]) + existing_ipv6s = set(fastly_inbound["addresses"]["ipv6"]) +else: + existing_ipv4s = set() + existing_ipv6s = set() + +fastly_ips = requests.get( + "https://api.fastly.com/public-ip-list", + headers={ + "accept": "application/json", + }).json() +fastly_ipv4s = set(fastly_ips.get("addresses")) +fastly_ipv6s = set(fastly_ips.get("ipv6_addresses")) + +# If no IP addresses have changed, we're done. +if existing_ipv4s == fastly_ipv4s and existing_ipv6s == fastly_ipv6s: + print("Fastly IPs have not changed") + exit(0) + +print("Updating firewall with new Fastly IPs") + +response = requests.put( + f"https://api.linode.com/v4/networking/firewalls/{ web_firewall["id"] }/rules", + json={ + "inbound": [ + { + "label": "Fastly", + "description": "DO NOT EDIT - Updated programmatically", + "addresses": { + "ipv4": fastly_ips.get("addresses"), + "ipv6": fastly_ips.get("ipv6_addresses"), + }, + "action": "ACCEPT", + "ports": "80", + "protocol": "TCP", + }, + ] + other_inbound, + "inbound_policy": "DROP", + "outbound_policy": "ACCEPT", + }, + headers={ + "accept": "application/json", + "authorization": f"Bearer { linode_api_token }", + "content-type": "application/json", + } +) + +if response.status_code == 200: + print("Firewall updated successfully") +else: + print(f"Failed to update firewall (status { response.status_code }): { response.text }") + exit(1) diff --git a/setup/test/docker-compose.yml b/setup/test/docker-compose.yml new file mode 100644 index 00000000..2568af82 --- /dev/null +++ b/setup/test/docker-compose.yml @@ -0,0 +1,35 @@ +services: + mltshp: + image: mltshp/mltshp-web:${build_number:-latest} + build: + context: . + dockerfile: setup/web/Dockerfile + command: "sleep infinity" + volumes: + - ./setup/test/settings.py:/srv/mltshp.com/mltshp/settings.py + - .git:/srv/mltshp.com/mltshp/.git + - ./test:/srv/mltshp.com/mltshp/test + - ./worker/scripts:/srv/mltshp.com/mltshp/scripts + - ./setup/database:/srv/mltshp.com/mltshp/setup/database + - ./mounts/logs:/srv/mltshp.com/logs + depends_on: + - mysql + links: + - mysql + networks: + app_net: + aliases: + - mltshp.dev + mysql: + image: mysql:8 + ports: + - "3306:3306" + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_DATABASE: "mltshp_testing" + MYSQL_USER: "mltshp" + networks: + - app_net + +networks: + app_net: diff --git a/release/settings.py b/setup/test/settings.py similarity index 71% rename from release/settings.py rename to setup/test/settings.py index 52d0eb1f..b8ecb8d2 100644 --- a/release/settings.py +++ b/setup/test/settings.py @@ -14,7 +14,6 @@ "xsrf_cookies": True, "uploaded_files" : "/srv/mltshp.com/uploaded", "debug": True, - # docker container's host ip address "database_user": "root", "database_name": "mltshp_testing", "database_password" : "", @@ -28,9 +27,4 @@ "debug_workers": True, "superuser_list": "admin", "tornado_logging": False, - # these must be set for testing test/unit/externalservice_tests.py - # "twitter_consumer_key" : "twitter_consumer_key_here", - # "twitter_consumer_secret" : "twitter_consumer_secret_key_here", - # "twitter_access_key" : "twitter_access_key_here", - # "twitter_access_secret" : "twitter_access_secret_here", } diff --git a/setup/web/Dockerfile b/setup/web/Dockerfile new file mode 100644 index 00000000..396e6707 --- /dev/null +++ b/setup/web/Dockerfile @@ -0,0 +1,39 @@ +FROM python:3.12-alpine +LABEL maintainer="brad@bradchoate.com" +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/srv/mltshp.com/mltshp:/srv/mltshp.com/mltshp/test:/usr/lib/python3.12/site-packages + +# Installs the base system dependencies for running the site. +# None of this will change with the codebase itself, so this +# whole layer and steps to build it should be cached. + +RUN apk add --no-cache \ + py3-mysqlclient \ + py3-beautifulsoup4 \ + py3-boto3 \ + py3-curl \ + py3-dateutil \ + py3-kombu \ + py3-mock \ + py3-mysqlclient \ + py3-pip \ + py3-pillow \ + py3-requests \ + py3-tornado \ + py3-yoyo-migrations \ + supervisor + +# Install python dependencies which will be cached on the +# contents of requirements.txt: +RUN --mount=type=bind,source=web/requirements.txt,target=/tmp/requirements.txt \ + pip install --break-system-packages -r /tmp/requirements.txt + +RUN mkdir -p /srv/mltshp.com/uploaded /srv/mltshp.com/logs /srv/mltshp.com/bin && \ + adduser -D mltshp && \ + chown -R mltshp /srv/mltshp.com + +COPY web /srv/mltshp.com/mltshp/ + +WORKDIR /srv/mltshp.com/mltshp + +CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/mltshp/supervisor.conf", "--logfile", "/srv/mltshp.com/logs/supervisord.log", "--pidfile", "/srv/mltshp.com/logs/supervisord.pid"] diff --git a/setup/web/docker-compose.dev.yml b/setup/web/docker-compose.dev.yml new file mode 100644 index 00000000..bcbeb46f --- /dev/null +++ b/setup/web/docker-compose.dev.yml @@ -0,0 +1,90 @@ +services: + web: + build: + context: . + dockerfile: setup/web/Dockerfile + env_file: + - .env + image: mltshp/mltshp-web:latest + volumes: + - ./web:/srv/mltshp.com/mltshp + - ./test:/srv/mltshp.com/mltshp/test + - ./worker/scripts:/srv/mltshp.com/mltshp/scripts + - ./setup/database:/srv/mltshp.com/mltshp/setup/database + - ./mounts/logs:/srv/mltshp.com/logs + - ./mounts/uploaded:/srv/mltshp.com/uploaded + - ./mounts/tmpuploads/0:/mnt/tmpuploads/0 + - ./mounts/tmpuploads/1:/mnt/tmpuploads/1 + - ./mounts/tmpuploads/2:/mnt/tmpuploads/2 + - ./mounts/tmpuploads/3:/mnt/tmpuploads/3 + - ./mounts/tmpuploads/4:/mnt/tmpuploads/4 + - ./mounts/tmpuploads/5:/mnt/tmpuploads/5 + - ./mounts/tmpuploads/6:/mnt/tmpuploads/6 + - ./mounts/tmpuploads/7:/mnt/tmpuploads/7 + - ./mounts/tmpuploads/8:/mnt/tmpuploads/8 + - ./mounts/tmpuploads/9:/mnt/tmpuploads/9 + - ./setup/web/supervisor/dev.conf:/etc/mltshp/supervisor.conf + depends_on: + - mysql + - fakes3 + links: + - mysql + - fakes3 + networks: + app_net: + aliases: + - mltshp-web + nginx: + image: mltshp/nginx:latest + ports: + - "8000:80" + volumes: + - ./setup/nginx/dev.conf:/etc/nginx/nginx.conf + - ./mounts/logs:/srv/mltshp.com/logs + - ./mounts/uploaded:/srv/mltshp.com/uploaded + - ./mounts/tmpuploads/0:/mnt/tmpuploads/0 + - ./mounts/tmpuploads/1:/mnt/tmpuploads/1 + - ./mounts/tmpuploads/2:/mnt/tmpuploads/2 + - ./mounts/tmpuploads/3:/mnt/tmpuploads/3 + - ./mounts/tmpuploads/4:/mnt/tmpuploads/4 + - ./mounts/tmpuploads/5:/mnt/tmpuploads/5 + - ./mounts/tmpuploads/6:/mnt/tmpuploads/6 + - ./mounts/tmpuploads/7:/mnt/tmpuploads/7 + - ./mounts/tmpuploads/8:/mnt/tmpuploads/8 + - ./mounts/tmpuploads/9:/mnt/tmpuploads/9 + depends_on: + - web + links: + - web + networks: + app_net: + aliases: + - mltshp.localhost + fakes3: + image: mltshp/fakes3:latest + entrypoint: fakes3 --root /srv --license dummy --port 4567 + volumes: + - ./mounts/fakes3:/srv + networks: + app_net: + aliases: + - mltshp-testing.fakes3 + - mltshp-dev.fakes3 + mysql: + image: mysql:8 + ports: + - "3306:3306" + volumes: + - ./mounts/mysql:/var/lib/mysql + - ./setup/mysql/dev.conf:/etc/mysql/conf.d/mltshp.cnf + - ./setup/database/db-install.sql:/docker-entrypoint-initdb.d/00_db-install.sql + - ./setup/database/db-fixtures.sql:/docker-entrypoint-initdb.d/01_db-fixtures.sql + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_DATABASE: "mltshp" + MYSQL_USER: "mltshp" + networks: + - app_net + +networks: + app_net: diff --git a/setup/web/docker-compose.prod.yml b/setup/web/docker-compose.prod.yml new file mode 100644 index 00000000..d8ae2c2e --- /dev/null +++ b/setup/web/docker-compose.prod.yml @@ -0,0 +1,62 @@ +services: + mltshp-web: + build: + context: ../.. + dockerfile: setup/web/Dockerfile + env_file: + - ../../.env + image: mltshp/mltshp-web:latest + volumes: + - .:/srv/mltshp.com/mltshp + - ./mounts/logs:/srv/mltshp.com/logs + - ./mounts/uploaded:/srv/mltshp.com/uploaded + - ./mounts/tmpuploads/0:/mnt/tmpuploads/0 + - ./mounts/tmpuploads/1:/mnt/tmpuploads/1 + - ./mounts/tmpuploads/2:/mnt/tmpuploads/2 + - ./mounts/tmpuploads/3:/mnt/tmpuploads/3 + - ./mounts/tmpuploads/4:/mnt/tmpuploads/4 + - ./mounts/tmpuploads/5:/mnt/tmpuploads/5 + - ./mounts/tmpuploads/6:/mnt/tmpuploads/6 + - ./mounts/tmpuploads/7:/mnt/tmpuploads/7 + - ./mounts/tmpuploads/8:/mnt/tmpuploads/8 + - ./mounts/tmpuploads/9:/mnt/tmpuploads/9 + - ./setup/web/supervisor/prod.conf:/etc/mltshp/supervisor.conf + depends_on: + - mysql + - fakes3 + links: + - mysql + - fakes3 + networks: + app_net: + aliases: + - mltshp-web + nginx: + image: mltshp/nginx:latest + ports: + - "8000:80" + volumes: + - ./setup/nginx/prod.conf:/etc/nginx/nginx.conf + - ./mounts/logs:/srv/mltshp.com/logs + - ./mounts/uploaded:/srv/mltshp.com/uploaded + - ./mounts/tmpuploads/0:/mnt/tmpuploads/0 + - ./mounts/tmpuploads/1:/mnt/tmpuploads/1 + - ./mounts/tmpuploads/2:/mnt/tmpuploads/2 + - ./mounts/tmpuploads/3:/mnt/tmpuploads/3 + - ./mounts/tmpuploads/4:/mnt/tmpuploads/4 + - ./mounts/tmpuploads/5:/mnt/tmpuploads/5 + - ./mounts/tmpuploads/6:/mnt/tmpuploads/6 + - ./mounts/tmpuploads/7:/mnt/tmpuploads/7 + - ./mounts/tmpuploads/8:/mnt/tmpuploads/8 + - ./mounts/tmpuploads/9:/mnt/tmpuploads/9 + depends_on: + - mltshp-web + links: + - mltshp-web + networks: + app_net: + aliases: + - mltshp.localhost + +networks: + app_net: diff --git a/setup/dev/supervisord.conf b/setup/web/supervisor/dev.conf similarity index 57% rename from setup/dev/supervisord.conf rename to setup/web/supervisor/dev.conf index 9fa59427..a3b6308b 100644 --- a/setup/dev/supervisord.conf +++ b/setup/web/supervisor/dev.conf @@ -1,19 +1,15 @@ [supervisord] nodaemon=true -[program:nginx] -command=sh /srv/mltshp.com/mltshp/setup/dev/start-nginx.sh -autorestart=true - [group:mltshp] -programs=main-8000 +programs=main-8001 -[program:main-8000] -command=python3 main.py --on_port=8000 +[program:main-8001] +command=python3 main.py --on_port=8001 directory=/srv/mltshp.com/mltshp autorestart=true redirect_stderr=true -stdout_logfile=/srv/mltshp.com/logs/main-8000.log +stdout_logfile=/srv/mltshp.com/logs/main-8001.log stdout_logfile_maxbytes=500MB stdout_logfile_backups=50 stdout_capture_maxbytes=1MB diff --git a/setup/production/supervisord-web.conf b/setup/web/supervisor/prod.conf similarity index 86% rename from setup/production/supervisord-web.conf rename to setup/web/supervisor/prod.conf index 9697d33e..199aa22f 100644 --- a/setup/production/supervisord-web.conf +++ b/setup/web/supervisor/prod.conf @@ -1,10 +1,6 @@ [supervisord] nodaemon=true -[program:nginx] -command=/usr/sbin/nginx -g 'daemon off;' -autorestart=true - [program:main] process_name=mltshp-%(process_num)d command=python3 main.py --on_port=80%(process_num)02d diff --git a/setup/worker/Dockerfile b/setup/worker/Dockerfile new file mode 100644 index 00000000..ee0c4340 --- /dev/null +++ b/setup/worker/Dockerfile @@ -0,0 +1,44 @@ +FROM python:3.12-alpine +LABEL maintainer="brad@bradchoate.com" +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/usr/lib/python3.12/site-packages + +# Installs the base system dependencies for running the site. +# None of this will change with the codebase itself, so this +# whole layer and steps to build it should be cached. + +RUN apk add --no-cache \ + apk-cron \ + py3-mysqlclient \ + py3-beautifulsoup4 \ + py3-boto3 \ + py3-curl \ + py3-dateutil \ + py3-kombu \ + py3-mysqlclient \ + py3-pip \ + py3-pillow \ + py3-requests \ + py3-tornado \ + py3-yoyo-migrations \ + ffmpeg \ + supervisor + +RUN mkdir -p /srv/mltshp.com/logs /srv/mltshp.com/bin && \ + adduser -D mltshp && \ + chown -R mltshp /srv/mltshp.com + +# Install python dependencies which will be cached on the +# contents of requirements.txt: +RUN --mount=type=bind,source=worker/requirements.txt,target=/tmp/requirements.txt \ + pip install --break-system-packages -r /tmp/requirements.txt + +# Copy configuration settings into place +COPY setup/worker/supervisor/prod.conf /etc/mltshp/supervisor.conf +COPY web/lib web/models web/tasks web/mltshpoptions.py web/torndb.py worker /srv/mltshp.com/mltshp/ + +WORKDIR /srv/mltshp.com/mltshp + +RUN crontab -u mltshp setup/worker/crontab/prod.cron + +CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/mltshp/supervisor.conf", "--logfile", "/srv/mltshp.com/logs/supervisord.log", "--pidfile", "/srv/mltshp.com/logs/supervisord.pid"] diff --git a/setup/production/mltshp-worker--crontab b/setup/worker/crontab/prod.cron similarity index 100% rename from setup/production/mltshp-worker--crontab rename to setup/worker/crontab/prod.cron diff --git a/setup/production/supervisord-worker.conf b/setup/worker/supervisor/prod.conf similarity index 100% rename from setup/production/supervisord-worker.conf rename to setup/worker/supervisor/prod.conf diff --git a/static/straw/compact.js b/static/straw/compact.js deleted file mode 100644 index fec3f3e2..00000000 --- a/static/straw/compact.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - - Straw - a MLTSHP Bookmarklet. - Source: http://mltshp.com/static/straw/source.js - Bookmarklet URL: javascript:void((function(){var%20e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','https://mltshp.com/static/straw/compact.js?r='+Math.random()*99999999);document.body.appendChild(e)})()); -*/ -(function(e){if(e.MLTSHPStraw!==void 0)e.MLTSHPStraw.toggle();else{var i=function(){var a=!!e.document.addEventListener;return{bind:function(b,d,c){var e=function(a){return c.apply(b,arguments)};a?b.addEventListener(d,e,!1):b.attachEvent("on"+d,e)}}}(),j=function(a,b){return function(){a.apply(b,arguments)}},k={style:function(a,b){var d=null;a.currentStyle?d=a.currentStyle[b]:e.getComputedStyle&&(d=e.document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return d}},f=function(a,b,d){this.category= -a;this.asset=b;this.el=d};f.prototype.append_root=e.document.body;f.prototype.position=function(){var a=this.el,b=0,d=0;do{if(k.style(a,"position")=="relative"){this.append_root=a;break}b+=a.offsetLeft;d+=a.offsetTop}while(a=a.offsetParent);return[b,d]};f.prototype.hide=function(){if(this.view!==void 0)this.view.style.display="none"};f.prototype.show=function(){if(this.view!==void 0)this.view.style.display="block"};f.prototype.draw_overlay=function(){var a=this.position(),b=this.width(),d=this.height(), -c=document.createElement("div"),e=this.append_root;c.className="mltshp-sf-overlay";c.style.left=a[0]+"px";c.style.top=a[1]+"px";c.style.width=b-4+"px";c.style.height=d-4+"px";if(b>300&&d>100)html='Save on MLTSHP',c.innerHTML=html;e.appendChild(c);this.view=c;i.bind(c,"click",j(this.click_overlay,this))};f.prototype.click_overlay=function(a){a.stopPropagation? -a.stopPropagation():a.cancelBubble=!0;left_location=screen.width/2-450;top_location=screen.height/2-300;e.open("https://mltshp.com/tools/p?url="+encodeURI(this.asset)+"&title="+encodeURI(e.document.title)+"&source_url="+encodeURI(e.location.href),"save_image","width=850,height=650,menubar=yes,toolbar=yes,scrollbars=yes,resizable=yes,left="+left_location+",top="+top_location+"screenX="+left_location+",screenY="+top_location)};f.prototype.height=function(){if(this._height===void 0)this._height=this.el.offsetHeight; -return this._height};f.prototype.width=function(){if(this._width===void 0)this._width=this.el.offsetWidth;return this._width};var h={_parse_query:function(a){for(var a=a.split("&"),b=a.length,d={},c=0;c0)b=a.getAttribute("id").replace("player","").split("_")[0],e.push({el:a,url:"http://vimeo.com/"+b});return e},images:function(){for(var a=document.getElementsByTagName("img"),b=a.length,d=[],c=0;cSave on MLTSHP',c.innerHTML=html);f.appendChild(c);this.view=c;n.bind(c,"click",p(this.click_overlay,this))};g.prototype.click_overlay=function(a){a.stopPropagation? +a.stopPropagation():a.cancelBubble=!0;left_location=screen.width/2-450;top_location=screen.height/2-300;a="width=850,height=650,menubar=yes,toolbar=yes,scrollbars=yes,resizable=yes,left="+left_location+",top="+top_location+"screenX="+left_location+",screenY="+top_location;e.open("https://mltshp.com/tools/p?url="+encodeURI(this.asset)+"&title="+encodeURI(e.document.title)+"&source_url="+encodeURI(e.location.href),"save_image",a)};g.prototype.height=function(){void 0===this._height&&(this._height=this.el.offsetHeight); +return this._height};g.prototype.width=function(){void 0===this._width&&(this._width=this.el.offsetWidth);return this._width};var m={_parse_query:function(a){a=a.split("&");for(var b=a.length,d={},c=0;ca[c].offsetWidth||100>a[c].offsetHeight||d.push(a[c]);return d}};e.MLTSHPStraw={found:[],init:function(){this.initiated=!0;this.init_styles();this.find_assets();this.draw_overlays()},find_assets:function(){var a= +m.images(),b=a.length;m.videos();for(var d=[],c=0;c