start_daemon and stop_daemon polled "while True" with no bound, and no
seaf-cli exit code was checked. A daemon that failed to start left the
container spinning forever with nothing pointing at the cause, and there was
no signal handling, so "docker stop" waited the full 10 second grace period
and then SIGKILLed the process with the daemon still running.
Readiness polling is now bounded by DAEMON_START_TIMEOUT and
DAEMON_STOP_TIMEOUT. The stop timeout is 8 seconds so a graceful shutdown
fits inside the docker stop grace period, and a test asserts that relation.
SIGTERM and SIGINT raise GracefulShutdown, and the lifecycle moved into
start.run(), which always stops the daemon on the way out and returns the
process exit code: 0 on a clean finish or a stop signal, 1 on a daemon or
configuration failure.
seaf-cli exit codes are checked. A failed sync is reported per library and
does not stop the remaining libraries, since one bad library should not take
the container down.
Verified in the built image: "docker stop" now returns in 0s with exit code 0
and the daemon stopped, against 10s and exit 137 before. A daemon that cannot
start exits 1 with the failing command and its exit code in the log.
Also bumps actions/setup-python to v7, which clears the last Node 20
deprecation warning; v7 only drops the unused pip-install input.
Commands were joined into a string and passed to "su -c", so a login shell
parsed every interpolated value. A library name is chosen on the server, and
"sync -d /dsc/seafile/<name>" put it straight into that string: a library
named 'Docs; touch /tmp/pwned; #' ran as an arbitrary command. Confirmed
against the built image before and after this change.
Every command is now an argument list executed by "runuser --", which execs
directly with no shell. runuser -u sets the same HOME and a PATH that still
finds the seaf-cli wrapper, so the daemon environment is unchanged.
The password no longer needs its surrounding quotes, which also fixes
passwords containing quotes, dollar signs, and backticks.
Library names are untrusted, so dsc/paths.py reduces each one to a single
path component and proves the result stays under the libraries directory
with symlinks resolved. Empty, ".", "..", and names containing a separator
are skipped with a warning; names that would collide are suffixed with their
library ID rather than sharing a directory.
Tests cover quotes, command substitutions, separators, newlines, absolute
paths, parent traversal, Unicode, and colliding names. They run in a separate
CI job so application failures stay distinguishable from packaging failures.
Add a weekly Dependabot configuration for pip, the Dockerfile base image,
and the GitHub Actions used by the publish workflow.
The Seafile AppImage is fetched by URL with a pinned checksum, which
Dependabot cannot resolve, so it stays a manual bump.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add a .dockerignore that whitelists only requirements.txt, start.py and
dsc/. The build context previously included .git, .idea, __pycache__ and
any local .env, which slowed builds and put a secrets file within reach
of any future COPY of the whole tree.
Remove the unused urlparse import from dsc/client.py and the redundant
import os.path from start.py; importing os already binds os.path.
Record both in IMPROVEMENTS.md, including the part still outstanding:
binutils and squashfs-tools remain in the runtime image because the
AppImage is unpacked in the same stage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The push job called build-push-action twice, once for the ref tag and once
for latest. Two builds meant the two tags could resolve to different manifest
digests. Build once and let docker/metadata-action supply both tags and the
OCI labels for that single build.
Replace the deprecated actions/checkout@v2 and docker/build-push-action@v1,
default workflow permissions to contents: read so pull requests cannot reach
publication credentials, and authenticate with a scoped DOCKER_TOKEN through
docker/login-action instead of DOCKER_PASSWORD.
The test job's docker-compose.test.yml branch was dead; that file does not
exist in the repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The signature acceptance criterion assumed the signed APT repository.
Upstream ships a bare AppImage with no digest or signature, so as written
it could never be met and would block the release gate indefinitely.
State what the build actually enforces, and track the weakened supply
chain as its own item rather than only a note.
Seafile discontinued linux-clients.seafile.com: it resolves but accepts
no connections on 80 or 443, and the official docs no longer mention APT
at all. Every `docker build` has failed since. Upstream now ships the CLI
as an AppImage, so install 9.0.19 from the download site.
The AppImage is unpacked rather than executed. Running one needs FUSE and
so a privileged container, and its self-extraction also fails when the
build host cannot execute the payload's architecture. Unpacking the
appended squashfs directly avoids both.
Also add the bundled seafile/pysearpc bindings to the venv path. The APT
package installed them system-wide, where `venv --system-site-packages`
picked them up; dsc imports seafile directly for RPC, not only through
seaf-cli, so it broke when that implicit dependency went away.
Drops apt-key, pins the version, and verifies a SHA256 before unpacking.
Build-time smoke checks assert the seaf-cli subcommands and the app's own
imports.
Note: the AppImage is x86_64-only, and upstream publishes no digest or
signature, so the pinned checksum is trust-on-first-use.
The list-form environment syntax in the README passed quote characters
through as part of the value, so LIBRARY_ID was read as '"<id>"' and no
matching library was found. Switch to mapping syntax, where YAML strips
the quotes.
- Pin the example to a versioned image instead of implicit latest
- Add a checked-in compose.example.yaml
- Validate it with `docker compose config` in CI
- Link the Docker Hub image from the README