11 Commits

Author SHA1 Message Date
Neo
518056cc75 Merge d120f00fb5 into ee7e9ef2a3 2025-06-05 16:05:14 +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
Eva
d120f00fb5 client/upload: send state of image's anonymous checkbox as boolean
Anonymous checkbox on images would not actually do anything, when the
global checkbox was unchecked. The value of anonymous becomes a node,
and it fails the anonymous === true check in save().
I don't understand why anonymous is treated differently from other
post parameters and supplied as an argument to save().
Keeping it the way it is, I guess...
2024-02-21 17:54:12 +01:00
Eva
94d145e8d0 client/views: fix incorrectly 'checked' checkboxes
When similar posts were found, the anonymous upload checkbox for that
image would become checked, because we treat anything !== undefined
as 'checked', and in post_upload_views.js we set 'anonymous' to
a querySelector, which returns null on failure and not undefined.
Treat null as 'unchecked' to fix this issue, and prevent future mistakes
slipping past.
2024-02-21 17:54:12 +01:00
66143dce20 client: Use expanders and full tag input control on the upload page 2024-02-21 17:54:12 +01:00
d17d37ceb0 Fix tag name escaping on upload 2024-02-21 17:54:12 +01:00
a06b3bb37f Better layout for upload options 2024-02-21 17:54:11 +01:00
ec39500bbd Remove nullcheck operator 2024-02-21 17:54:11 +01:00
9533de5c8c Allow default anonymous uploads 2024-02-21 17:54:11 +01:00
881cfe026c Default upload tags 2024-02-21 17:54:11 +01:00
f90c190baf Pin pillow-avif-plugin to compatible version range 2024-02-21 01:46:28 +01:00
14 changed files with 251 additions and 49 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

@ -19,7 +19,7 @@
font-size: 1em
color: $inactive-link-color
float: right
line-height: 2em
line-height: 2rem
.expander-content
padding: 0.5em 0.5em 2em 0.5em

View File

@ -15,13 +15,22 @@ $cancel-button-color = tomato
&.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.inactive .upload-all-anonymous
&.inactive .expander
&.inactive #common-tags,
&.uploading input[type=submit],
&.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&.uploading .upload-all-anonymous
&.uploading .expander
&:not(.uploading) .cancel
display: none
&.inactive .control-strip
&.uploading .control-strip
gap: 0
.dropper-container
margin: 0 auto
.file-dropper
@ -30,28 +39,36 @@ $cancel-button-color = tomato
small
font-size: 60%
input[type=submit]
margin-top: 1em
.expander
&.collapsed
margin-bottom: 0
.expander-content
padding: 0.5em
.cancel
margin-top: 1em
background: $cancel-button-color
border-color: $cancel-button-color
&:focus
border: 2px solid $text-color
.skip-duplicates
margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages
margin-top: 1em
.control-strip
display: flex
flex-direction: column
gap: 1em
margin-top: 1em
.control-options
display: flex
flex-wrap: wrap
gap: 0 1em
span
flex: 1 1 30%
white-space: nowrap
.uploadables-container
list-style-type: none
margin: 0

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,32 +3,45 @@
<div class='dropper-container'></div>
<div class='control-strip'>
<div class='control-options'>
<span class='skip-duplicates'>
<%= ctx.makeCheckbox({
text: 'Skip duplicate',
name: 'skip-duplicates',
checked: false,
}) %>
</span>
<span class='always-upload-similar'>
<%= ctx.makeCheckbox({
text: 'Force upload similar',
name: 'always-upload-similar',
checked: false,
}) %>
</span>
<span class='pause-remain-on-error'>
<%= ctx.makeCheckbox({
text: 'Pause on error',
name: 'pause-remain-on-error',
checked: true,
}) %>
</span>
<span class='upload-all-anonymous'>
<%= ctx.makeCheckbox({
text: 'Upload anonymously',
name: 'upload-all-anonymous',
checked: false,
}) %>
</span>
</div>
<section class='common-tags'>
<%= ctx.makeTextInput({name: 'common-tags'}) %>
</section>
<input type='submit' value='Upload all' class='submit'/>
<span class='skip-duplicates'>
<%= ctx.makeCheckbox({
text: 'Skip duplicate',
name: 'skip-duplicates',
checked: false,
}) %>
</span>
<span class='always-upload-similar'>
<%= ctx.makeCheckbox({
text: 'Force upload similar',
name: 'always-upload-similar',
checked: false,
}) %>
</span>
<span class='pause-remain-on-error'>
<%= ctx.makeCheckbox({
text: 'Pause on error',
name: 'pause-remain-on-error',
checked: true,
}) %>
</span>
<input type='button' value='Cancel' class='cancel'/>
</div>

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.");
});

View File

@ -81,7 +81,7 @@ function makeCheckbox(options) {
name: options.name,
value: options.value,
type: "checkbox",
checked: options.checked !== undefined ? options.checked : false,
checked: options.checked !== undefined && options.checked !== null ? options.checked : false,
disabled: options.readonly,
required: options.required,
}),

View File

@ -4,10 +4,14 @@ const events = require("../events.js");
const api = require("../api.js");
const views = require("../util/views.js");
const FileDropperControl = require("../controls/file_dropper_control.js");
const ExpanderControl = require("../controls/expander_control.js");
const TagInputControl = require("../controls/tag_input_control.js");
const template = views.getTemplate("post-upload");
const rowTemplate = views.getTemplate("post-upload-row");
const TagList = require("../models/tag_list.js");
function _mimeTypeToPostType(mimeType) {
return (
{
@ -160,6 +164,7 @@ class PostUploadView extends events.EventTarget {
this._uploadables.find = (u) => {
return this._uploadables.findIndex((u2) => u.key === u2.key);
};
this._commonTags = new TagList();
this._contentFileDropper = new FileDropperControl(
this._contentInputNode,
@ -185,6 +190,23 @@ class PostUploadView extends events.EventTarget {
this._evtFormSubmit(e)
);
this._formNode.classList.add("inactive");
this._commonTagsExpander = new ExpanderControl(
"common-tags",
"Common Tags (0)",
this._hostNode.querySelectorAll(".common-tags")
);
if (this._commonTagsInputNode) {
this._commonTagsControl = new TagInputControl(
this._commonTagsInputNode,
this._commonTags
);
this._commonTagsControl.addEventListener("change", (_) => {
this._commonTagsExpander.title = `Common Tags (${this._commonTags.length})`;
});
}
}
enableForm() {
@ -299,14 +321,16 @@ class PostUploadView extends events.EventTarget {
uploadable.safety = safetyNode.value;
}
const anonymousNode = rowNode.querySelector(
".anonymous input:checked"
);
if (anonymousNode) {
uploadable.anonymous = true;
let anonymous = this._uploadAllAnonymous.checked;
if (!anonymous && rowNode.querySelector(".anonymous input:checked")) {
anonymous = true;
}
uploadable.anonymous = anonymous;
uploadable.tags = [];
if (this._commonTagsInputNode) {
uploadable.tags = this._commonTags.map((tag) => tag.names[0]);
}
uploadable.relations = [];
for (let [i, lookalike] of uploadable.lookalikes.entries()) {
let lookalikeNode = rowNode.querySelector(
@ -441,6 +465,12 @@ class PostUploadView extends events.EventTarget {
);
}
get _uploadAllAnonymous() {
return this._hostNode.querySelector(
"form [name=upload-all-anonymous]"
);
}
get _submitButtonNode() {
return this._hostNode.querySelector("form [type=submit]");
}
@ -452,6 +482,10 @@ class PostUploadView extends events.EventTarget {
get _contentInputNode() {
return this._formNode.querySelector(".dropper-container");
}
get _commonTagsInputNode() {
return this._formNode.querySelector(".common-tags input");
}
}
module.exports = PostUploadView;

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

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