2 Commits

Author SHA1 Message Date
Eva
f66a8d2a96 client/css: revert accidental style removal
fixes 4b6b231fc8
2025-07-15 23:32:08 +02:00
ee7e9ef2a3 build: setup docker-compose.dev.yml dev iteration
This is based off of the 5-commit branch at
https://github.com/neobooru/szurubooru/blob/docker-development-setup.

Compared to said branch, we
* Exclude extraneous changes such as
    * Any formatting
    * The use of deprecated/ineffectual top-level `version:` in composer files
* Support controlling $THREADS (modernizing the branch to upstream)
* Integrate into master more cleanly

However, client/docker-start-dev uses a temporary hack -- due to
volume mounting overwriting node_modules at arbitrary points during the
`docker compose build` step, we run `npm i` before any given
`npm run watch`.

To see the effects of this commit in action, run:

    docker compose -f ./docker-compose.dev.yml up
2025-05-23 20:05:15 +02:00
20 changed files with 165 additions and 51 deletions

View File

@ -1,4 +1,5 @@
node_modules/*
public/
Dockerfile
.dockerignore
**/.gitignore

View File

@ -1,3 +1,26 @@
FROM --platform=$BUILDPLATFORM node:lts-alpine as development
WORKDIR /opt/app
RUN apk --no-cache add \
dumb-init \
nginx \
git
RUN ln -sf /opt/app/nginx.conf.docker /etc/nginx/nginx.conf
RUN rm -rf /var/www
RUN ln -sf /opt/app/public/ /var/www
COPY package.json package-lock.json ./
RUN npm install
ARG BUILD_INFO="docker-development"
ENV BUILD_INFO=${BUILD_INFO}
ENV BACKEND_HOST="server"
CMD ["/opt/app/docker-start-dev.sh"]
VOLUME ["/data"]
FROM --platform=$BUILDPLATFORM node:lts as builder
WORKDIR /opt/app

View File

@ -315,7 +315,7 @@ function makeOutputDirs() {
}
function watch() {
let wss = new WebSocket.Server({ port: 8080 });
let wss = new WebSocket.Server({ port: environment === "development" ? 8081 : 8080 });
const liveReload = !process.argv.includes('--no-live-reload');
function emitReload() {

View File

@ -121,6 +121,7 @@
.thumbnail
width: 4em
height: 3em
background-position: 50% 30%
li
margin: 0 0.3em 0.3em 0
display: inline-block

17
client/docker-start-dev.sh Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/dumb-init /bin/sh
# Integrate environment variables
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
/etc/nginx/nginx.conf
# Start server
nginx &
# Watch source for changes and build app
# FIXME: It's not ergonomic to run `npm i` outside of the build step.
# However, the mounting of different directories into the
# client container's /opt/app causes node_modules to disappear
# (the mounting causes client/Dockerfile's RUN npm install
# to silently clobber).
# Find a way to move `npm i` into client/Dockerfile.
npm i && npm run watch -- --polling

View File

@ -2,10 +2,10 @@
# Integrate environment variables
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
/etc/nginx/nginx.conf
/etc/nginx/nginx.conf
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
/var/www/index.htm \
/var/www/manifest.json
/var/www/index.htm \
/var/www/manifest.json
# Start server
exec nginx

View File

@ -3,7 +3,7 @@
const config = require("./config.js");
if (config.environment == "development") {
var ws = new WebSocket("ws://" + location.hostname + ":8080");
var ws = new WebSocket("ws://" + location.hostname + ":8081");
ws.addEventListener("open", function (event) {
console.log("Live-reloading websocket connected.");
});

54
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,54 @@
## Docker Compose configuration for dev iteration
##
## Data is transient by using named vols.
## Run: docker-compose -f ./docker-compose.dev.yml up
services:
server:
build:
context: ./server
target: development
depends_on:
- sql
environment:
## These should be the names of the dependent containers listed below,
## or FQDNs/IP addresses if these services are running outside of Docker
POSTGRES_HOST: sql
## Credentials for database:
POSTGRES_USER:
POSTGRES_PASSWORD:
## Commented Values are Default:
#POSTGRES_DB: defaults to same as POSTGRES_USER
#POSTGRES_PORT: 5432
#LOG_SQL: 0 (1 for verbose SQL logs)
THREADS:
volumes:
- "data:/data"
- "./server/:/opt/app/"
client:
build:
context: ./client
target: development
depends_on:
- server
volumes:
- "data:/data:ro"
- "./client/:/opt/app/"
- "/opt/app/public/"
ports:
- "${PORT}:80"
- "8081:8081"
sql:
image: postgres:11-alpine
restart: unless-stopped
environment:
POSTGRES_USER:
POSTGRES_PASSWORD:
volumes:
- "sql:/var/lib/postgresql/data"
volumes:
data:
sql:

View File

@ -27,7 +27,6 @@ RUN apk --no-cache add \
RUN pip3 install --no-cache-dir --disable-pip-version-check \
"alembic>=0.8.5" \
"coloredlogs==5.0" \
gallery_dl \
"pyheif==0.6.1" \
"heif-image-plugin>=0.3.2" \
yt-dlp \
@ -62,7 +61,42 @@ ENTRYPOINT ["pytest", "--tb=short"]
CMD ["szurubooru/"]
FROM prereqs as development
WORKDIR /opt/app
ARG PUID=1000
ARG PGID=1000
RUN apk --no-cache add \
dumb-init \
py3-pip \
py3-setuptools \
py3-waitress \
&& pip3 install --no-cache-dir --disable-pip-version-check \
hupper \
&& mkdir -p /opt/app /data \
&& addgroup -g ${PGID} app \
&& adduser -SDH -h /opt/app -g '' -G app -u ${PUID} app \
&& chown -R app:app /opt/app /data
USER app
CMD ["/opt/app/docker-start-dev.sh"]
ARG PORT=6666
ENV PORT=${PORT}
EXPOSE ${PORT}
ARG THREADS=4
ENV THREADS=${THREADS}
VOLUME ["/data/"]
FROM prereqs as release
COPY ./ /opt/app/
RUN rm -rf /opt/app/szurubooru/tests
WORKDIR /opt/app
ARG PUID=1000

8
server/docker-start-dev.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/dumb-init /bin/sh
set -e
cd /opt/app
alembic upgrade head
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
exec hupper -m waitress --port ${PORT} --threads ${THREADS} szurubooru.facade:app

View File

@ -3,7 +3,7 @@ line-length = 79
[tool.isort]
known_first_party = ["szurubooru"]
known_third_party = ["PIL", "alembic", "coloredlogs", "freezegun", "gallery_dl", "nacl", "numpy", "pyrfc3339", "pytest", "pytz", "sqlalchemy", "yaml", "youtube_dl"]
known_third_party = ["PIL", "alembic", "coloredlogs", "freezegun", "nacl", "numpy", "pyrfc3339", "pytest", "pytz", "sqlalchemy", "yaml", "youtube_dl"]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0

View File

@ -1,7 +1,6 @@
alembic>=0.8.5
certifi>=2017.11.5
coloredlogs==5.0
gallery_dl
heif-image-plugin==0.3.2
numpy>=1.8.2
pillow-avif-plugin~=1.1.0

View File

@ -61,7 +61,7 @@ def create_post(
auth.verify_privilege(ctx.user, "posts:create:identified")
content = ctx.get_file(
"content",
use_downloader=auth.has_privilege(
use_video_downloader=auth.has_privilege(
ctx.user, "uploads:use_downloader"
),
)
@ -128,7 +128,7 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
post,
ctx.get_file(
"content",
use_downloader=auth.has_privilege(
use_video_downloader=auth.has_privilege(
ctx.user, "uploads:use_downloader"
),
),

View File

@ -12,7 +12,7 @@ def create_temporary_file(
content = ctx.get_file(
"content",
allow_tokens=False,
use_downloader=auth.has_privilege(
use_video_downloader=auth.has_privilege(
ctx.user, "uploads:use_downloader"
),
)

View File

@ -21,22 +21,14 @@ class DownloadTooLargeError(DownloadError):
pass
def download(url: str, use_downloader: bool = False) -> bytes:
def download(url: str, use_video_downloader: bool = False) -> bytes:
assert url
dl_error = None
new_url = None
if use_downloader:
youtube_dl_error = None
if use_video_downloader:
try:
new_url = _get_gallery_dl_content_url(url)
url = _get_youtube_dl_content_url(url) or url
except errors.ThirdPartyError as ex:
dl_error = ex
if new_url:
url = new_url
else:
try:
url = _get_youtube_dl_content_url(url) or url
except errors.ThirdPartyError as ex:
dl_error = ex
youtube_dl_error = ex
request = urllib.request.Request(url)
if config.config["user_agent"]:
@ -63,10 +55,10 @@ def download(url: str, use_downloader: bool = False) -> bytes:
) from ex
if (
dl_error
youtube_dl_error
and mime.get_mime_type(content_buffer) == "application/octet-stream"
):
raise dl_error
raise youtube_dl_error
return content_buffer
@ -89,21 +81,6 @@ def _get_youtube_dl_content_url(url: str) -> str:
) from None
def _get_gallery_dl_content_url(url: str) -> str:
cmd = ["gallery-dl", "-q", "-g", url]
try:
return (
subprocess.run(cmd, text=True, capture_output=True, check=True)
.stdout.split("\n")[0]
.strip()
)
except subprocess.CalledProcessError:
raise errors.ThirdPartyError(
"Could not extract content location from URL.",
extra_fields={"URL": url},
) from None
def post_to_webhooks(payload: Dict[str, Any]) -> List[Thread]:
threads = [
Thread(target=_post_to_webhook, args=(webhook, payload), daemon=False)

View File

@ -48,7 +48,7 @@ class Context:
self,
name: str,
default: Union[object, bytes] = MISSING,
use_downloader: bool = False,
use_video_downloader: bool = False,
allow_tokens: bool = True,
) -> bytes:
if name in self._files and self._files[name]:
@ -57,7 +57,7 @@ class Context:
if name + "Url" in self._params:
return net.download(
self._params[name + "Url"],
use_downloader=use_downloader,
use_video_downloader=use_video_downloader,
)
if allow_tokens and name + "Token" in self._params:

View File

@ -214,7 +214,7 @@ def test_creating_from_url_saves_source(
)
)
net.download.assert_called_once_with(
"example.com", use_downloader=False
"example.com", use_video_downloader=False
)
posts.create_post.assert_called_once_with(
b"content", ["tag1", "tag2"], auth_user
@ -259,7 +259,7 @@ def test_creating_from_url_with_source_specified(
)
)
net.download.assert_called_once_with(
"example.com", use_downloader=True
"example.com", use_video_downloader=True
)
posts.create_post.assert_called_once_with(
b"content", ["tag1", "tag2"], auth_user

View File

@ -124,7 +124,7 @@ def test_uploading_from_url_saves_source(
{"post_id": post.post_id},
)
net.download.assert_called_once_with(
"example.com", use_downloader=True
"example.com", use_video_downloader=True
)
posts.update_post_content.assert_called_once_with(post, b"content")
posts.update_post_source.assert_called_once_with(post, "example.com")
@ -156,7 +156,7 @@ def test_uploading_from_url_with_source_specified(
{"post_id": post.post_id},
)
net.download.assert_called_once_with(
"example.com", use_downloader=True
"example.com", use_video_downloader=True
)
posts.update_post_content.assert_called_once_with(post, b"content")
posts.update_post_source.assert_called_once_with(post, "example2.com")

View File

@ -79,7 +79,7 @@ def test_download():
)
def test_too_large_download(url):
with pytest.raises(net.DownloadTooLargeError):
net.download(url, use_downloader=True)
net.download(url, use_video_downloader=True)
@pytest.mark.skipif(
@ -103,7 +103,7 @@ def test_too_large_download(url):
],
)
def test_content_download(url, expected_sha1):
actual_content = net.download(url, use_downloader=True)
actual_content = net.download(url, use_video_downloader=True)
assert get_sha1(actual_content) == expected_sha1
@ -113,7 +113,7 @@ def test_content_download(url, expected_sha1):
def test_bad_content_downlaod():
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
with pytest.raises(errors.ThirdPartyError):
net.download(url, use_downloader=True)
net.download(url, use_video_downloader=True)
def test_no_webhooks(config_injector):

View File

@ -29,7 +29,7 @@ def test_get_file_from_url():
)
assert ctx.get_file("key") == b"content"
net.download.assert_called_once_with(
"example.com", use_downloader=False
"example.com", use_video_downloader=False
)
with pytest.raises(errors.ValidationError):
assert ctx.get_file("non-existing")