16 Commits

Author SHA1 Message Date
5c3455a38a v0.6.0 2025-03-01 18:16:48 +00:00
65ad035d8d feat(docker): simplify docker compose for end users (#96)
* feat(docker): simplify docker compose for end users

The previous docker-compose requires end user to manually handle
permissions of taskchampion data dir. And this commit has directories
automatically set up in docker-entrypoint.sh, just like what
postgresql did in https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh

* fix(docker): revert to anonymous data volume for compatibility

* feat: use uid 1092 for taskchampion

* fix(docker): revert mkdir

This is embarrassing that subpaths are not automatically created.

So we still need mkdir service in case of anonymous data volume.

* fix(docker): typo
2025-03-01 13:08:27 -05:00
c47612b3a0 Bump uuid from 1.14.0 to 1.15.1 (#95)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.14.0 to 1.15.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.14.0...v1.15.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 19:26:42 -05:00
8508d517a6 Bump uuid from 1.13.1 to 1.14.0 (#94)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.13.1 to 1.14.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.13.1...v1.14.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-22 13:19:02 -05:00
24a9496f18 Bump tempfile from 3.16.0 to 3.17.1 (#93)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.16.0 to 3.17.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.16.0...v3.17.1)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 08:40:46 -05:00
5c42107006 Add an MSRV (#89)
This just copies TaskChampion's MSRV for the moment.
2025-02-16 16:48:12 -05:00
e2600dadc5 Fix and test for clap errors (#92)
* fix clap error

Signed-off-by: Cameron Wong <cam@camdar.io>

* Refactor to test more arg-parsing

---------

Signed-off-by: Cameron Wong <cam@camdar.io>
Co-authored-by: Cameron Wong <cam@camdar.io>
2025-02-15 10:34:10 -05:00
e401b67c43 Bump uuid from 1.12.0 to 1.13.1 (#86)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.12.0 to 1.13.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.12.0...1.13.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 20:03:57 -05:00
7f51d2fa1f Allow specifying configuration params in env vars (#83) 2025-02-02 22:39:45 -05:00
5ffd179dcc Use version-specific references for docker-compose (#85) 2025-02-02 22:30:24 -05:00
401c102e94 Bump tempfile from 3.15.0 to 3.16.0 (#82)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.15.0 to 3.16.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.15.0...v3.16.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 13:35:40 -05:00
d5e7c88608 Customize Listen Address (#81)
Replace --port with --listen to allow specifying the interface as well

Also fix the docker-compose file and adjust tests to this change
2025-01-24 19:58:56 -05:00
84d942213c Bump uuid from 1.11.0 to 1.12.0 (#80)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.11.0...1.12.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 06:38:55 -05:00
5332d90c57 Improve error handling in the inmemory storage implementation. (#79)
Improve error handling in the inmemory storage

This addresses a TODO, in a type that is really only used for testing.

This also adds a test for a similar circumstance -- adding the same
version twice -- in the SQLite storage, but it is already handled
correctly.
2025-01-13 08:32:27 -05:00
f3445d558e Bump tempfile from 3.14.0 to 3.15.0 (#77)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.14.0 to 3.15.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.14.0...v3.15.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-02 22:21:04 -05:00
65a3d806d7 Followup to the 0.5.0 release (#76)
* Document RELEASING.md process
* Bump version numbers
* Do not automatically produce GH releases, and do not build binaries to attach to them
* Only build docker images on tags
* Use the `latest` Docker image tag in the Docker-compose config
2024-12-15 22:51:57 -05:00
17 changed files with 436 additions and 168 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
*
!Cargo.toml
!Cargo.lock
!core/
!server/
!sqlite/
!docker-entrypoint.sh

View File

@ -1,58 +0,0 @@
name: Build
on: [push, pull_request]
jobs:
build:
strategy:
fail-fast: false
matrix:
target:
- tag: amd64-musl
target: x86_64-unknown-linux-musl
- tag: amd64-glibc
target: x86_64-unknown-linux-gnu
name: Build TaskChampion Sync-Server ${{ matrix.target.tag }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load .env file
uses: xom9ikk/dotenv@v2
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.target.target }}
- name: Build
run: |
[ "${{ matrix.target.target }}" == "x86_64-unknown-linux-musl" ] && sudo apt update && sudo apt -y install musl-tools
cargo build --target ${{ matrix.target.target }} --release --locked
- name: Package current compilation
id: package-current
run: |
install -Dm755 "target/${{ matrix.target.target }}/release/taskchampion-sync-server" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}-${GITHUB_SHA}/taskchampion-sync-server"
install -Dm644 "README.md" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}-${GITHUB_SHA}/README.md"
install -Dm644 "LICENSE" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}-${GITHUB_SHA}/LICENSE"
echo "version=${GITHUB_REF##*/}-${GITHUB_SHA}" >> $GITHUB_OUTPUT
- name: Archive current compilation
uses: actions/upload-artifact@v4
with:
name: "taskchampion-sync-server-${{ matrix.target.tag }}-${{ steps.package-current.outputs.version }}"
path: "taskchampion-sync-server-${{ matrix.target.tag }}-${{ steps.package-current.outputs.version }}/"
- name: Package tagged compilation
id: package
if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request'
run: |
install -Dm755 "target/${{ matrix.target.target }}/release/taskchampion-sync-server" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}/taskchampion-sync-server"
install -Dm644 "README.md" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}/README.md"
install -Dm644 "LICENSE" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}/LICENSE"
tar cvJf "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}.tar.xz" "taskchampion-sync-server-${{ matrix.target.tag }}-${GITHUB_REF##*/}"
echo "version=${GITHUB_REF##*/}" >> $GITHUB_OUTPUT
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request'
with:
files: "taskchampion-sync-server-${{ matrix.target.tag }}-${{ steps.package.outputs.version }}.tar.xz"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -2,8 +2,6 @@ name: Build Docker
on:
push:
branches:
- '*'
tags:
- '*'

View File

@ -13,6 +13,8 @@ jobs:
# A simple matrix for now, but if we introduce an MSRV it can be added here.
matrix:
rust:
# MSRV
- "1.81.0"
- "stable"
runs-on: ubuntu-latest

67
Cargo.lock generated
View File

@ -208,7 +208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
@ -761,7 +761,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets",
]
[[package]]
@ -1154,7 +1166,7 @@ dependencies = [
"hermit-abi",
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@ -1320,7 +1332,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
@ -1558,7 +1570,7 @@ dependencies = [
[[package]]
name = "taskchampion-sync-server"
version = "0.4.1"
version = "0.6.0"
dependencies = [
"actix-rt",
"actix-web",
@ -1573,6 +1585,7 @@ dependencies = [
"serde_json",
"taskchampion-sync-server-core",
"taskchampion-sync-server-storage-sqlite",
"temp-env",
"tempfile",
"thiserror",
"uuid",
@ -1580,7 +1593,7 @@ dependencies = [
[[package]]
name = "taskchampion-sync-server-core"
version = "0.5.0-pre"
version = "0.6.0"
dependencies = [
"anyhow",
"chrono",
@ -1593,7 +1606,7 @@ dependencies = [
[[package]]
name = "taskchampion-sync-server-storage-sqlite"
version = "0.5.0-pre"
version = "0.6.0"
dependencies = [
"anyhow",
"chrono",
@ -1606,13 +1619,23 @@ dependencies = [
]
[[package]]
name = "tempfile"
version = "3.14.0"
name = "temp-env"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
dependencies = [
"parking_lot",
]
[[package]]
name = "tempfile"
version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@ -1772,11 +1795,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
dependencies = [
"getrandom",
"getrandom 0.3.1",
"serde",
]
@ -1798,6 +1821,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
@ -1944,6 +1976,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
]
[[package]]
name = "write16"
version = "1.0.0"

View File

@ -5,16 +5,17 @@ members = [
"server",
"sqlite",
]
rust-version = "1.81.0" # MSRV
[workspace.dependencies]
uuid = { version = "^1.11.0", features = ["serde", "v4"] }
uuid = { version = "^1.15.1", features = ["serde", "v4"] }
actix-web = "^4.9.0"
anyhow = "1.0"
thiserror = "2.0"
futures = "^0.3.25"
serde_json = "^1.0"
serde = { version = "^1.0.147", features = ["derive"] }
clap = { version = "^4.5.6", features = ["string"] }
clap = { version = "^4.5.6", features = ["string", "env"] }
log = "^0.4.17"
env_logger = "^0.11.5"
rusqlite = { version = "0.32", features = ["bundled"] }
@ -22,3 +23,4 @@ chrono = { version = "^0.4.38", features = ["serde"] }
actix-rt = "2"
tempfile = "3"
pretty_assertions = "1"
temp-env = "0.3"

View File

@ -1,19 +1,25 @@
# Versions must be major.minor
ARG RUST_VERSION
ARG ALPINE_VERSION
# Default versions are as below
ARG RUST_VERSION=1.78
ARG ALPINE_VERSION=3.19
FROM docker.io/rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS builder
COPY . /data
COPY Cargo.lock Cargo.toml /data/
COPY core /data/core/
COPY server /data/server/
COPY sqlite /data/sqlite/
RUN apk -U add libc-dev && \
cd /data && \
cargo build --release
FROM docker.io/alpine:${ALPINE_VERSION}
COPY --from=builder /data/target/release/taskchampion-sync-server /bin
RUN adduser -S -D -H -h /var/lib/taskchampion-sync-server -s /sbin/nologin -G users \
RUN apk add --no-cache su-exec && \
adduser -u 1092 -S -D -H -h /var/lib/taskchampion-sync-server -s /sbin/nologin -G users \
-g taskchampion taskchampion && \
install -d -m755 -o100 -g100 "/var/lib/taskchampion-sync-server"
install -d -m1755 -o1092 -g1092 "/var/lib/taskchampion-sync-server"
EXPOSE 8080
VOLUME "/var/lib/taskchampion-sync-server"
USER taskchampion
ENTRYPOINT [ "taskchampion-sync-server" ]
VOLUME /var/lib/task-champion-sync-server/data
COPY docker-entrypoint.sh /bin
ENTRYPOINT [ "/bin/docker-entrypoint.sh" ]
CMD [ "/bin/taskchampion-sync-server" ]

View File

@ -27,26 +27,32 @@ use a reverse proxy such as Nginx, haproxy, or Apache httpd.
### Using Docker-Compose
The [`docker-compose.yml`](./docker-compose.yml) file in this repository is
sufficient to run taskchampion-sync-server, including setting up TLS
certificates using Lets Encrypt, thanks to [Caddy](https://caddyserver.com/).
Every release of the server generates a Docker image in
`ghcr.io/gothenburgbitfactory/taskchampion-sync-server`. The tags include
`latest` for the latest release, and both minor and patch versions, e.g., `0.5`
and `0.5.1`.
The
[`docker-compose.yml`](https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/refs/tags/v0.6.0/docker-compose.yml)
file in this repository is sufficient to run taskchampion-sync-server,
including setting up TLS certificates using Lets Encrypt, thanks to
[Caddy](https://caddyserver.com/).
You will need a server with ports 80 and 443 open to the Internet and with a
fixed, publicly-resolvable hostname. These ports must be available both to your
Taskwarrior clients and to the Lets Encrypt servers.
On that server, clone this repository (or just download `docker-compose.yml` to
the current directory -- the rest of the contents of this repository are not
required) and run
On that server, download `docker-compose.yml` from the link above (it is pinned
to the latest release) into the current directory. Then run
```sh
TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com docker compose up
```
It can take a few minutes to obtain the certificate; the caddy container will
log a msg "certificate obtained successfully" when this is complete, or error
messages if the process fails. Once this process is complete, configure your
`.taskrc`'s to point to the server:
log a message "certificate obtained successfully" when this is complete, or
error messages if the process fails. Once this process is complete, configure
your `.taskrc`'s to point to the server:
```
sync.server.url=https://taskwarrior.example.com
@ -65,11 +71,20 @@ system startup. See the docker-compose documentation for more information.
The server is configured with command-line options. See
`taskchampion-sync-server --help` for full details.
The `--data-dir` option specifies where the server should store its data, and
`--port` gives the port on which the HTTP server runs.
The `--listen` option specifies the interface and port the server listens on.
It must contain an IP-Address or a DNS name and a port number. This option is
mandatory, but can be repeated to specify multiple interfaces or ports. This
value can be specified in environment variable `LISTEN`, as a comma-separated
list of values.
The `--data-dir` option specifies where the server should store its data. This
value can be specified in the environment variable `DATA_DIR`.
By default, the server allows all client IDs. To limit the accepted client IDs,
such as when running a personal server, use `--allow-client-id <client-id>`.
specify them in the environment variable `CLIENT_ID`, as a comma-separated list
of UUIDs. Client IDs can be specified with `--allow-client-id`, but this should
not be used on shared systems, as command line arguments are visible to all
users on the system.
The server only logs errors by default. To add additional logging output, set
environment variable `RUST_LOG` to `info` to get a log message for every
@ -88,7 +103,11 @@ release version. You can install Rust from your distribution package or use
rustup default stable
```
If you prefer, you can use the stable version only for install TaskChampion
The minimum supported Rust version (MSRV) is given in
[`Cargo.toml`](./Cargo.toml). Note that package repositories typically do not
have sufficiently new versions of Rust.
If you prefer, you can use the stable version only for installing TaskChampion
Sync-Server (you must clone the repository first).
```sh
rustup override set stable
@ -108,6 +127,7 @@ cargo build --release
After build the binary is located in
`target/release/taskchampion-sync-server`.
### Building the Container
To build the container execute the following commands.

21
RELEASING.md Normal file
View File

@ -0,0 +1,21 @@
# Release process
1. Run `git pull upstream main`
1. Run `cargo test`
1. Run `cargo clean && cargo clippy`
1. Remove the `-pre` from `version` in all `*/Cargo.toml`, and from the `version = ..` in any references between packages.
1. Update the link to `docker-compose.yml` in `README.md` to refer to the new version.
1. Update the docker image in `docker-compose.yml` to refer to the new version.
1. Run `cargo semver-checks` (https://crates.io/crates/cargo-semver-checks)
1. Run `cargo build --release`
1. Commit the changes (Cargo.lock will change too) with comment `vX.Y.Z`.
1. Run `git tag vX.Y.Z`
1. Run `git push upstream`
1. Run `git push upstream --tag vX.Y.Z`
1. Run `cargo publish -p taskchampion-sync-server-core`
1. Run `cargo publish -p taskchampion-sync-server-storage-sqlite` (and add any other new published packages here)
1. Bump the patch version in `*/Cargo.toml` and add the `-pre` suffix. This allows `cargo-semver-checks` to check for changes not accounted for in the version delta.
1. Run `cargo build --release` again to update `Cargo.lock`
1. Commit that change with comment "Bump to -pre version".
1. Run `git push upstream`
1. Navigate to the tag in the GitHub releases UI and create a release with general comments about the changes in the release

View File

@ -1,6 +1,6 @@
[package]
name = "taskchampion-sync-server-core"
version = "0.5.0-pre"
version = "0.6.0"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2021"
description = "Core of sync protocol for TaskChampion"

View File

@ -130,7 +130,6 @@ impl StorageTxn for InnerTxn<'_> {
parent_version_id: Uuid,
history_segment: Vec<u8>,
) -> anyhow::Result<()> {
// TODO: verify it doesn't exist (`.entry`?)
let version = Version {
version_id,
parent_version_id,
@ -143,15 +142,33 @@ impl StorageTxn for InnerTxn<'_> {
snap.versions_since += 1;
}
} else {
return Err(anyhow::anyhow!("Client {} does not exist", self.client_id));
anyhow::bail!("Client {} does not exist", self.client_id);
}
self.guard
if self
.guard
.children
.insert((self.client_id, parent_version_id), version_id);
self.guard
.insert((self.client_id, parent_version_id), version_id)
.is_some()
{
anyhow::bail!(
"Client {} already has a child for {}",
self.client_id,
parent_version_id
);
}
if self
.guard
.versions
.insert((self.client_id, version_id), version);
.insert((self.client_id, version_id), version)
.is_some()
{
anyhow::bail!(
"Client {} already has a version {}",
self.client_id,
version_id
);
}
self.written = true;
Ok(())
@ -259,6 +276,25 @@ mod test {
Ok(())
}
#[test]
fn test_add_version_exists() -> anyhow::Result<()> {
let storage = InMemoryStorage::new();
let client_id = Uuid::new_v4();
let mut txn = storage.txn(client_id)?;
let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4();
let history_segment = b"abc".to_vec();
txn.new_client(parent_version_id)?;
txn.add_version(version_id, parent_version_id, history_segment.clone())?;
assert!(txn
.add_version(version_id, parent_version_id, history_segment.clone())
.is_err());
txn.commit()?;
Ok(())
}
#[test]
fn test_snapshots() -> anyhow::Result<()> {
let storage = InMemoryStorage::new();

View File

@ -1,16 +1,13 @@
volumes:
data:
services:
# Make the necessary subdirectories of the `data` volume, and set ownership of the
# `tss/taskchampion-sync-server` directory, as the server runs as user 100.
mkdir:
image: caddy:2-alpine
command: |
/bin/sh -c "
mkdir -p /data/caddy/data /data/caddy/config /data/tss/taskchampion-sync-server &&
chown -R 100:100 /data/tss/taskchampion-sync-server
"
mkdir -p /data/caddy/data /data/caddy/config /data/tss/taskchampion-sync-server"
volumes:
- type: volume
source: data
@ -46,19 +43,20 @@ services:
condition: service_completed_successfully
tss:
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:main
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:0.6.0
restart: unless-stopped
environment:
- "RUST_LOG=info"
- "DATA_DIR=/var/lib/taskchampion-sync-server/data"
- "LISTEN=0.0.0.0:8080"
volumes:
- type: volume
source: data
target: /tss
target: /var/lib/taskchampion-sync-server/data
read_only: false
volume:
nocopy: true
subpath: tss
command: --data-dir /tss/taskchampion-sync-server --port 8080
environment:
- RUST_LOG=info
subpath: tss/taskchampion-sync-server
depends_on:
mkdir:
condition: service_completed_successfully

13
docker-entrypoint.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
set -e
echo "starting entrypoint script..."
if [ "$1" = "/bin/taskchampion-sync-server" ]; then
echo "setting data directories"
mkdir -p "/var/lib/taskchampion-sync-server/data"
chown -R 1092:1092 "/var/lib/taskchampion-sync-server/data"
chmod -R 700 "/var/lib/taskchampion-sync-server/data"
if [ "$(id -u)" = "0" ]; then
echo "switching to user 'taskchampion'"
exec su-exec taskchampion "$@"
fi
fi

View File

@ -1,6 +1,6 @@
[package]
name = "taskchampion-sync-server"
version = "0.4.1"
version = "0.6.0"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2021"
publish = false
@ -24,3 +24,4 @@ chrono.workspace = true
actix-rt.workspace = true
tempfile.workspace = true
pretty_assertions.workspace = true
temp-env.workspace = true

View File

@ -21,30 +21,38 @@ fn command() -> Command {
.version(env!("CARGO_PKG_VERSION"))
.about("Server for TaskChampion")
.arg(
arg!(-p --port <PORT> "Port on which to serve")
.help("Port on which to serve")
.value_parser(value_parser!(usize))
.default_value("8080"),
arg!(-l --listen <ADDRESS>)
.help("Address and Port on which to listen on. Can be an IP Address or a DNS name followed by a colon and a port e.g. localhost:8080")
.value_delimiter(',')
.value_parser(ValueParser::string())
.env("LISTEN")
.action(ArgAction::Append)
.required(true),
)
.arg(
arg!(-d --"data-dir" <DIR> "Directory in which to store data")
.value_parser(ValueParser::os_string())
.env("DATA_DIR")
.default_value("/var/lib/taskchampion-sync-server"),
)
.arg(
arg!(-C --"allow-client-id" <CLIENT_ID> "Client IDs to allow (can be repeated; if not specified, all clients are allowed)")
.value_delimiter(',')
.value_parser(value_parser!(Uuid))
.env("CLIENT_ID")
.action(ArgAction::Append)
.required(false),
)
.arg(
arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots")
.value_parser(value_parser!(u32))
.env("SNAPSHOT_VERSIONS")
.default_value(default_snapshot_versions),
)
.arg(
arg!(--"snapshot-days" <NUM> "Target number of days between snapshots")
.value_parser(value_parser!(i64))
.env("SNAPSHOT_DAYS")
.default_value(default_snapshot_days),
)
}
@ -56,35 +64,59 @@ fn print_error<B>(res: ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResp
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}
struct ServerArgs {
data_dir: OsString,
snapshot_versions: u32,
snapshot_days: i64,
client_id_allowlist: Option<HashSet<Uuid>>,
listen_addresses: Vec<String>,
}
impl ServerArgs {
fn new(matches: clap::ArgMatches) -> Self {
Self {
data_dir: matches.get_one::<OsString>("data-dir").unwrap().clone(),
snapshot_versions: *matches.get_one("snapshot-versions").unwrap(),
snapshot_days: *matches.get_one("snapshot-days").unwrap(),
client_id_allowlist: matches
.get_many("allow-client-id")
.map(|ids| ids.copied().collect()),
listen_addresses: matches
.get_many::<String>("listen")
.unwrap()
.cloned()
.collect(),
}
}
}
#[actix_web::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
let matches = command().get_matches();
let data_dir: &OsString = matches.get_one("data-dir").unwrap();
let port: usize = *matches.get_one("port").unwrap();
let snapshot_versions: u32 = *matches.get_one("snapshot-versions").unwrap();
let snapshot_days: i64 = *matches.get_one("snapshot-days").unwrap();
let client_id_allowlist: Option<HashSet<Uuid>> = matches
.get_many("allow-client-id")
.map(|ids| ids.copied().collect());
let server_args = ServerArgs::new(matches);
let config = ServerConfig {
snapshot_days,
snapshot_versions,
snapshot_days: server_args.snapshot_days,
snapshot_versions: server_args.snapshot_versions,
};
let server = WebServer::new(config, client_id_allowlist, SqliteStorage::new(data_dir)?);
let server = WebServer::new(
config,
server_args.client_id_allowlist,
SqliteStorage::new(server_args.data_dir)?,
);
log::info!("Serving on port {}", port);
HttpServer::new(move || {
let mut http_server = HttpServer::new(move || {
App::new()
.wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, print_error))
.wrap(Logger::default())
.configure(|cfg| server.config(cfg))
})
.bind(format!("0.0.0.0:{}", port))?
.run()
.await?;
});
for listen_address in server_args.listen_addresses {
log::info!("Serving on {}", listen_address);
http_server = http_server.bind(listen_address)?
}
http_server.run().await?;
Ok(())
}
@ -94,55 +126,187 @@ mod test {
use actix_web::{self, App};
use clap::ArgMatches;
use taskchampion_sync_server_core::InMemoryStorage;
use temp_env::{with_var, with_var_unset, with_vars, with_vars_unset};
/// Get the list of allowed client IDs
fn allowed(matches: &ArgMatches) -> Option<Vec<Uuid>> {
matches
.get_many::<Uuid>("allow-client-id")
.map(|ids| ids.copied().collect::<Vec<_>>())
/// Get the list of allowed client IDs, sorted.
fn allowed(matches: ArgMatches) -> Option<Vec<Uuid>> {
ServerArgs::new(matches)
.client_id_allowlist
.map(|ids| ids.into_iter().collect::<Vec<_>>())
.map(|mut ids| {
ids.sort();
ids
})
}
#[test]
fn command_listen_two() {
with_var_unset("LISTEN", || {
let matches = command().get_matches_from([
"tss",
"--listen",
"localhost:8080",
"--listen",
"otherhost:9090",
]);
assert_eq!(
ServerArgs::new(matches).listen_addresses,
vec!["localhost:8080".to_string(), "otherhost:9090".to_string()]
);
});
}
#[test]
fn command_listen_two_env() {
with_var("LISTEN", Some("localhost:8080,otherhost:9090"), || {
let matches = command().get_matches_from(["tss"]);
assert_eq!(
ServerArgs::new(matches).listen_addresses,
vec!["localhost:8080".to_string(), "otherhost:9090".to_string()]
);
});
}
#[test]
fn command_allowed_client_ids_none() {
let matches = command().get_matches_from(["tss"]);
assert_eq!(allowed(&matches), None);
with_var_unset("CLIENT_ID", || {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(allowed(matches), None);
});
}
#[test]
fn command_allowed_client_ids_one() {
let matches =
command().get_matches_from(["tss", "-C", "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"]);
assert_eq!(
allowed(&matches),
Some(vec![Uuid::parse_str(
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
)
.unwrap()])
with_var_unset("CLIENT_ID", || {
let matches = command().get_matches_from([
"tss",
"--listen",
"localhost:8080",
"-C",
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
]);
assert_eq!(
allowed(matches),
Some(vec![Uuid::parse_str(
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
)
.unwrap()])
);
});
}
#[test]
fn command_allowed_client_ids_one_env() {
with_var(
"CLIENT_ID",
Some("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"),
|| {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(
allowed(matches),
Some(vec![Uuid::parse_str(
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
)
.unwrap()])
);
},
);
}
#[test]
fn command_allowed_client_ids_two() {
let matches = command().get_matches_from([
"tss",
"-C",
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
"-C",
"bbaf4b61-344a-4a39-a19e-8caa0669b353",
]);
assert_eq!(
allowed(&matches),
Some(vec![
Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
])
with_var_unset("CLIENT_ID", || {
let matches = command().get_matches_from([
"tss",
"--listen",
"localhost:8080",
"-C",
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
"-C",
"bbaf4b61-344a-4a39-a19e-8caa0669b353",
]);
assert_eq!(
allowed(matches),
Some(vec![
Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
])
);
});
}
#[test]
fn command_allowed_client_ids_two_env() {
with_var(
"CLIENT_ID",
Some("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0,bbaf4b61-344a-4a39-a19e-8caa0669b353"),
|| {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(
allowed(matches),
Some(vec![
Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
])
);
},
);
}
#[test]
fn command_data_dir() {
let matches = command().get_matches_from(["tss", "--data-dir", "/foo/bar"]);
assert_eq!(matches.get_one::<OsString>("data-dir").unwrap(), "/foo/bar");
with_var_unset("DATA_DIR", || {
let matches = command().get_matches_from([
"tss",
"--data-dir",
"/foo/bar",
"--listen",
"localhost:8080",
]);
assert_eq!(ServerArgs::new(matches).data_dir, "/foo/bar");
});
}
#[test]
fn command_data_dir_env() {
with_var("DATA_DIR", Some("/foo/bar"), || {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(ServerArgs::new(matches).data_dir, "/foo/bar");
});
}
#[test]
fn command_snapshot() {
with_vars_unset(["SNAPSHOT_DAYS", "SNAPSHOT_VERSIONS"], || {
let matches = command().get_matches_from([
"tss",
"--listen",
"localhost:8080",
"--snapshot-days",
"13",
"--snapshot-versions",
"20",
]);
let server_args = ServerArgs::new(matches);
assert_eq!(server_args.snapshot_days, 13i64);
assert_eq!(server_args.snapshot_versions, 20u32);
});
}
#[test]
fn command_snapshot_env() {
with_vars(
[
("SNAPSHOT_DAYS", Some("13")),
("SNAPSHOT_VERSIONS", Some("20")),
],
|| {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
let server_args = ServerArgs::new(matches);
assert_eq!(server_args.snapshot_days, 13i64);
assert_eq!(server_args.snapshot_versions, 20u32);
},
);
}
#[actix_rt::test]

View File

@ -1,13 +1,13 @@
[package]
name = "taskchampion-sync-server-storage-sqlite"
version = "0.5.0-pre"
version = "0.6.0"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2021"
description = "SQLite backend for TaskChampion-sync-server"
license = "MIT"
[dependencies]
taskchampion-sync-server-core = { path = "../core", version = "0.5.0-pre" }
taskchampion-sync-server-core = { path = "../core", version = "0.6.0" }
uuid.workspace = true
anyhow.workspace = true
thiserror.workspace = true

View File

@ -385,6 +385,23 @@ mod test {
Ok(())
}
#[test]
fn test_add_version_exists() -> anyhow::Result<()> {
let tmp_dir = TempDir::new()?;
let storage = SqliteStorage::new(tmp_dir.path())?;
let client_id = Uuid::new_v4();
let mut txn = storage.txn(client_id)?;
let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4();
let history_segment = b"abc".to_vec();
txn.add_version(version_id, parent_version_id, history_segment.clone())?;
assert!(txn
.add_version(version_id, parent_version_id, history_segment.clone())
.is_err());
Ok(())
}
#[test]
fn test_snapshots() -> anyhow::Result<()> {
let tmp_dir = TempDir::new()?;