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
17 changed files with 155 additions and 114 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

@ -61,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

View File

@ -97,11 +97,9 @@ privileges:
'posts:create:anonymous': regular
'posts:create:identified': regular
'posts:list': anonymous
'posts:list:unsafe': regular
'posts:reverse_search': regular
'posts:view': anonymous
'posts:view:featured': anonymous
'posts:view:unsafe': regular
'posts:edit:content': power
'posts:edit:flags': regular
'posts:edit:notes': regular

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

@ -114,8 +114,6 @@ def create_snapshots_for_post(
def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
auth.verify_privilege(ctx.user, "posts:view")
post = _get_post(params)
if post.safety == model.Post.SAFETY_UNSAFE:
auth.verify_privilege(ctx.user, "posts:view:unsafe")
return _serialize_post(ctx, post)

View File

@ -3,7 +3,7 @@ from typing import Any, Dict, Optional, Tuple
import sqlalchemy as sa
from szurubooru import db, errors, model
from szurubooru.func import auth, util
from szurubooru.func import util
from szurubooru.search import criteria, tokens
from szurubooru.search.configs import util as search_util
from szurubooru.search.configs.base_search_config import (
@ -150,15 +150,6 @@ def _category_filter(
return query.filter(expr)
def _safety_filter(
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
) -> SaQuery:
assert criterion
return search_util.create_str_filter(
model.Post.safety, _safety_transformer
)(query, criterion, negated)
class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None:
self.user = None # type: Optional[model.User]
@ -217,22 +208,8 @@ class PostSearchConfig(BaseSearchConfig):
return db.session.query(model.Post)
def finalize_query(self, query: SaQuery) -> SaQuery:
if self.user and not auth.has_privilege(self.user, "posts:list:unsafe"):
# exclude unsafe posts:
query = _safety_filter(
query,
criteria.PlainCriterion(
model.Post.SAFETY_UNSAFE, model.Post.SAFETY_UNSAFE
),
negated=True,
)
return query.order_by(model.Post.post_id.desc())
@property
def can_list_unsafe(self) -> bool:
return self.user and auth.has_privilege(self.user, "posts:list:unsafe")
@property
def id_column(self) -> SaColumn:
return model.Post.post_id
@ -386,7 +363,12 @@ class PostSearchConfig(BaseSearchConfig):
model.Post.last_feature_time
),
),
(["safety", "rating"], _safety_filter),
(
["safety", "rating"],
search_util.create_str_filter(
model.Post.safety, _safety_transformer
),
),
(["note-text"], _note_filter),
(
["flag"],

View File

@ -93,10 +93,7 @@ class Executor:
if token.name == "random":
disable_eager_loads = True
can_list_unsafe = getattr(self.config, "can_list_unsafe", False)
key = (id(self.config), hash(search_query), offset, limit, can_list_unsafe)
key = (id(self.config), hash(search_query), offset, limit)
if cache.has(key):
return cache.get(key)

View File

@ -14,8 +14,6 @@ def inject_config(config_injector):
"privileges": {
"posts:list": model.User.RANK_REGULAR,
"posts:view": model.User.RANK_REGULAR,
"posts:view:unsafe": model.User.RANK_REGULAR,
"posts:list:unsafe": model.User.RANK_REGULAR,
},
}
)
@ -75,10 +73,7 @@ def test_trying_to_use_special_tokens_without_logging_in(
):
config_injector(
{
"privileges": {
"posts:list": "anonymous",
"posts:list:unsafe": "regular",
},
"privileges": {"posts:list": "anonymous"},
}
)
with pytest.raises(errors.SearchError):
@ -130,23 +125,3 @@ def test_trying_to_retrieve_single_without_privileges(
context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)),
{"post_id": 999},
)
def test_trying_to_retrieve_unsafe_without_privileges(
user_factory, context_factory, post_factory, config_injector
):
config_injector(
{
"privileges": {
"posts:view": "anonymous",
"posts:view:unsafe": "regular",
},
}
)
db.session.add(post_factory(id=1, safety=model.Post.SAFETY_UNSAFE))
db.session.flush()
with pytest.raises(errors.AuthError):
api.post_api.get_post(
context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)),
{"post_id": 1},
)

View File

@ -3,12 +3,6 @@ from datetime import datetime
import pytest
from szurubooru import db, errors, model, search
from szurubooru.func import cache
@pytest.fixture(autouse=True)
def purge_cache():
cache.purge()
@pytest.fixture
@ -60,15 +54,7 @@ def executor():
@pytest.fixture
def auth_executor(executor, user_factory, config_injector):
config_injector(
{
"privileges": {
"posts:list:unsafe": model.User.RANK_REGULAR,
}
}
)
def auth_executor(executor, user_factory):
def wrapper():
auth_user = user_factory()
db.session.add(auth_user)
@ -929,28 +915,3 @@ def test_search_by_tag_category(
)
db.session.flush()
verify_unpaged(input, expected_post_ids)
def test_filter_unsafe_without_privilege(
auth_executor,
verify_unpaged,
post_factory,
):
post1 = post_factory(id=1)
post2 = post_factory(id=2, safety=model.Post.SAFETY_SKETCHY)
post3 = post_factory(id=3, safety=model.Post.SAFETY_UNSAFE)
db.session.add_all([post1, post2, post3])
db.session.flush()
user = auth_executor()
user.rank = model.User.RANK_ANONYMOUS
verify_unpaged("", [1, 2])
verify_unpaged("safety:safe", [1])
verify_unpaged("safety:safe,sketchy", [1, 2])
verify_unpaged("safety:safe,sketchy,unsafe", [1, 2])
# adjust user's rank and retry
user.rank = model.User.RANK_REGULAR
cache.purge()
verify_unpaged("", [1, 2, 3])
verify_unpaged("safety:safe", [1])
verify_unpaged("safety:safe,sketchy", [1, 2])
verify_unpaged("safety:safe,sketchy,unsafe", [1, 2, 3])

View File

@ -2,19 +2,10 @@ import unittest.mock
import pytest
from szurubooru import search, model
from szurubooru import search
from szurubooru.func import cache
@pytest.fixture(autouse=True)
def inject_config(config_injector):
config_injector(
{
"privileges": {"posts:list:unsafe": model.User.RANK_REGULAR},
}
)
def test_retrieving_from_cache():
config = unittest.mock.MagicMock()
with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch(