mirror of
https://github.com/GothenburgBitFactory/taskchampion-sync-server.git
synced 2025-06-26 10:54:29 +02:00
Compare commits
40 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
953411bff8 | ||
![]() |
91763641c6 | ||
![]() |
721957d7c7 | ||
![]() |
35a4eefda3 | ||
![]() |
ad01f28a40 | ||
![]() |
29a4214117 | ||
![]() |
b9cdae975b | ||
![]() |
271e5eaf3d | ||
![]() |
67b441081d | ||
![]() |
5abb89c421 | ||
![]() |
cd15b2377b | ||
![]() |
ceed460707 | ||
![]() |
8a7df6d9d5 | ||
![]() |
92206f2488 | ||
![]() |
db8fbb3919 | ||
![]() |
ba69f98195 | ||
![]() |
cae0bb3fd8 | ||
![]() |
7bec7ce25d | ||
![]() |
4b55423595 | ||
![]() |
a9b9921833 | ||
![]() |
a7dc9e84b4 | ||
![]() |
7430d6feec | ||
![]() |
ecdfb6bdfd | ||
![]() |
55892d3b2d | ||
![]() |
5c3455a38a | ||
![]() |
65ad035d8d | ||
![]() |
c47612b3a0 | ||
![]() |
8508d517a6 | ||
![]() |
24a9496f18 | ||
![]() |
5c42107006 | ||
![]() |
e2600dadc5 | ||
![]() |
e401b67c43 | ||
![]() |
7f51d2fa1f | ||
![]() |
5ffd179dcc | ||
![]() |
401c102e94 | ||
![]() |
d5e7c88608 | ||
![]() |
84d942213c | ||
![]() |
5332d90c57 | ||
![]() |
f3445d558e | ||
![]() |
65a3d806d7 |
18 changed files with 847 additions and 452 deletions
7
.dockerignore
Normal file
7
.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
*
|
||||||
|
!Cargo.toml
|
||||||
|
!Cargo.lock
|
||||||
|
!core/
|
||||||
|
!server/
|
||||||
|
!sqlite/
|
||||||
|
!docker-entrypoint.sh
|
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
|
@ -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 }}
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -2,8 +2,6 @@ name: Build Docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
|
|
2
.github/workflows/rust-tests.yml
vendored
2
.github/workflows/rust-tests.yml
vendored
|
@ -13,6 +13,8 @@ jobs:
|
||||||
# A simple matrix for now, but if we introduce an MSRV it can be added here.
|
# A simple matrix for now, but if we introduce an MSRV it can be added here.
|
||||||
matrix:
|
matrix:
|
||||||
rust:
|
rust:
|
||||||
|
# MSRV
|
||||||
|
- "1.81.0"
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
2
.github/workflows/security.yml
vendored
2
.github/workflows/security.yml
vendored
|
@ -2,7 +2,7 @@ name: security
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '33 0 * * THU'
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
|
|
714
Cargo.lock
generated
714
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -5,20 +5,22 @@ members = [
|
||||||
"server",
|
"server",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
]
|
]
|
||||||
|
rust-version = "1.81.0" # MSRV
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
uuid = { version = "^1.11.0", features = ["serde", "v4"] }
|
uuid = { version = "^1.17.0", features = ["serde", "v4"] }
|
||||||
actix-web = "^4.9.0"
|
actix-web = "^4.11.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
futures = "^0.3.25"
|
futures = "^0.3.25"
|
||||||
serde_json = "^1.0"
|
serde_json = "^1.0"
|
||||||
serde = { version = "^1.0.147", features = ["derive"] }
|
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"
|
log = "^0.4.17"
|
||||||
env_logger = "^0.11.5"
|
env_logger = "^0.11.7"
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
chrono = { version = "^0.4.38", features = ["serde"] }
|
chrono = { version = "^0.4.38", features = ["serde"] }
|
||||||
actix-rt = "2"
|
actix-rt = "2"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
|
temp-env = "0.3"
|
||||||
|
|
22
Dockerfile
22
Dockerfile
|
@ -1,19 +1,25 @@
|
||||||
# Versions must be major.minor
|
# Versions must be major.minor
|
||||||
ARG RUST_VERSION
|
# Default versions are as below
|
||||||
ARG ALPINE_VERSION
|
ARG RUST_VERSION=1.78
|
||||||
|
ARG ALPINE_VERSION=3.19
|
||||||
|
|
||||||
FROM docker.io/rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS builder
|
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 && \
|
RUN apk -U add libc-dev && \
|
||||||
cd /data && \
|
cd /data && \
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
FROM docker.io/alpine:${ALPINE_VERSION}
|
FROM docker.io/alpine:${ALPINE_VERSION}
|
||||||
COPY --from=builder /data/target/release/taskchampion-sync-server /bin
|
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 && \
|
-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
|
EXPOSE 8080
|
||||||
VOLUME "/var/lib/taskchampion-sync-server"
|
VOLUME /var/lib/taskchampion-sync-server/data
|
||||||
USER taskchampion
|
COPY docker-entrypoint.sh /bin
|
||||||
ENTRYPOINT [ "taskchampion-sync-server" ]
|
ENTRYPOINT [ "/bin/docker-entrypoint.sh" ]
|
||||||
|
CMD [ "/bin/taskchampion-sync-server" ]
|
||||||
|
|
67
README.md
67
README.md
|
@ -27,31 +27,42 @@ use a reverse proxy such as Nginx, haproxy, or Apache httpd.
|
||||||
|
|
||||||
### Using Docker-Compose
|
### Using Docker-Compose
|
||||||
|
|
||||||
The [`docker-compose.yml`](./docker-compose.yml) file in this repository is
|
Every release of the server generates a Docker image in
|
||||||
sufficient to run taskchampion-sync-server, including setting up TLS
|
`ghcr.io/gothenburgbitfactory/taskchampion-sync-server`. The tags include
|
||||||
certificates using Lets Encrypt, thanks to [Caddy](https://caddyserver.com/).
|
`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.1/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
|
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
|
fixed, publicly-resolvable hostname. These ports must be available both to your
|
||||||
Taskwarrior clients and to the Lets Encrypt servers.
|
Taskwarrior clients and to the Lets Encrypt servers.
|
||||||
|
|
||||||
On that server, clone this repository (or just download `docker-compose.yml` to
|
On that server, download `docker-compose.yml` from the link above (it is pinned
|
||||||
the current directory -- the rest of the contents of this repository are not
|
to the latest release) into the current directory. Then run
|
||||||
required) and run
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com docker compose up
|
TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com \
|
||||||
|
TASKCHAMPION_SYNC_SERVER_CLIENT_ID=your-client-id \
|
||||||
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `TASKCHAMPION_SYNC_SERVER_CLIENT_ID` limits the server to the given client
|
||||||
|
ID; omit it to allow all client IDs.
|
||||||
|
|
||||||
It can take a few minutes to obtain the certificate; the caddy container will
|
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
|
log a message "certificate obtained successfully" when this is complete, or
|
||||||
messages if the process fails. Once this process is complete, configure your
|
error messages if the process fails. Once this process is complete, configure
|
||||||
`.taskrc`'s to point to the server:
|
your `.taskrc`'s to point to the server:
|
||||||
|
|
||||||
```
|
```
|
||||||
sync.server.url=https://taskwarrior.example.com
|
sync.server.url=https://taskwarrior.example.com
|
||||||
sync.server.client_id=[your client-id]
|
sync.server.client_id=your-client-id
|
||||||
sync.encryption_secret=[your encryption secret]
|
sync.encryption_secret=your-encryption-secret
|
||||||
```
|
```
|
||||||
|
|
||||||
The docker-compose images store data in a docker volume named
|
The docker-compose images store data in a docker volume named
|
||||||
|
@ -65,11 +76,20 @@ system startup. See the docker-compose documentation for more information.
|
||||||
The server is configured with command-line options. See
|
The server is configured with command-line options. See
|
||||||
`taskchampion-sync-server --help` for full details.
|
`taskchampion-sync-server --help` for full details.
|
||||||
|
|
||||||
The `--data-dir` option specifies where the server should store its data, and
|
The `--listen` option specifies the interface and port the server listens on.
|
||||||
`--port` gives the port on which the HTTP server runs.
|
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,
|
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
|
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
|
environment variable `RUST_LOG` to `info` to get a log message for every
|
||||||
|
@ -88,7 +108,11 @@ release version. You can install Rust from your distribution package or use
|
||||||
rustup default stable
|
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).
|
Sync-Server (you must clone the repository first).
|
||||||
```sh
|
```sh
|
||||||
rustup override set stable
|
rustup override set stable
|
||||||
|
@ -108,6 +132,7 @@ cargo build --release
|
||||||
|
|
||||||
After build the binary is located in
|
After build the binary is located in
|
||||||
`target/release/taskchampion-sync-server`.
|
`target/release/taskchampion-sync-server`.
|
||||||
|
|
||||||
### Building the Container
|
### Building the Container
|
||||||
|
|
||||||
To build the container execute the following commands.
|
To build the container execute the following commands.
|
||||||
|
@ -129,4 +154,12 @@ docker run -t -d \
|
||||||
|
|
||||||
This start TaskChampion Sync-Server and publish the port to host. Please
|
This start TaskChampion Sync-Server and publish the port to host. Please
|
||||||
note that this is a basic run, all data will be destroyed after stop and
|
note that this is a basic run, all data will be destroyed after stop and
|
||||||
delete container.
|
delete container. You may also set `DATA_DIR`, `CLIENT_ID`, or `LISTEN` with `-e`, e.g.,
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run -t -d \
|
||||||
|
--name=taskchampion \
|
||||||
|
-e LISTEN=0.0.0.0:9000 \
|
||||||
|
-p 9000:9000 \
|
||||||
|
taskchampion-sync-server
|
||||||
|
```
|
||||||
|
|
21
RELEASING.md
Normal file
21
RELEASING.md
Normal 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
|
|
@ -1,9 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "taskchampion-sync-server-core"
|
name = "taskchampion-sync-server-core"
|
||||||
version = "0.5.0-pre"
|
version = "0.6.2-pre"
|
||||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Core of sync protocol for TaskChampion"
|
description = "Core of sync protocol for TaskChampion"
|
||||||
|
homepage = "https://github.com/GothenburgBitFactory/taskchampion"
|
||||||
|
repository = "https://github.com/GothenburgBitFactory/taskchampion-sync-server"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -130,7 +130,6 @@ impl StorageTxn for InnerTxn<'_> {
|
||||||
parent_version_id: Uuid,
|
parent_version_id: Uuid,
|
||||||
history_segment: Vec<u8>,
|
history_segment: Vec<u8>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// TODO: verify it doesn't exist (`.entry`?)
|
|
||||||
let version = Version {
|
let version = Version {
|
||||||
version_id,
|
version_id,
|
||||||
parent_version_id,
|
parent_version_id,
|
||||||
|
@ -143,15 +142,33 @@ impl StorageTxn for InnerTxn<'_> {
|
||||||
snap.versions_since += 1;
|
snap.versions_since += 1;
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
.children
|
||||||
.insert((self.client_id, parent_version_id), version_id);
|
.insert((self.client_id, parent_version_id), version_id)
|
||||||
self.guard
|
.is_some()
|
||||||
|
{
|
||||||
|
anyhow::bail!(
|
||||||
|
"Client {} already has a child for {}",
|
||||||
|
self.client_id,
|
||||||
|
parent_version_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.guard
|
||||||
.versions
|
.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;
|
self.written = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -259,6 +276,25 @@ mod test {
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn test_snapshots() -> anyhow::Result<()> {
|
fn test_snapshots() -> anyhow::Result<()> {
|
||||||
let storage = InMemoryStorage::new();
|
let storage = InMemoryStorage::new();
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
|
|
||||||
|
|
||||||
services:
|
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:
|
mkdir:
|
||||||
image: caddy:2-alpine
|
image: caddy:2-alpine
|
||||||
command: |
|
command: |
|
||||||
/bin/sh -c "
|
/bin/sh -c "
|
||||||
mkdir -p /data/caddy/data /data/caddy/config /data/tss/taskchampion-sync-server &&
|
mkdir -p /data/caddy/data /data/caddy/config /data/tss/taskchampion-sync-server"
|
||||||
chown -R 100:100 /data/tss/taskchampion-sync-server
|
|
||||||
"
|
|
||||||
volumes:
|
volumes:
|
||||||
- type: volume
|
- type: volume
|
||||||
source: data
|
source: data
|
||||||
|
@ -46,19 +43,21 @@ services:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
tss:
|
tss:
|
||||||
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:main
|
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:0.6.1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- "RUST_LOG=info"
|
||||||
|
- "DATA_DIR=/var/lib/taskchampion-sync-server/data"
|
||||||
|
- "LISTEN=0.0.0.0:8080"
|
||||||
|
- "CLIENT_ID=${TASKCHAMPION_SYNC_SERVER_CLIENT_ID}"
|
||||||
volumes:
|
volumes:
|
||||||
- type: volume
|
- type: volume
|
||||||
source: data
|
source: data
|
||||||
target: /tss
|
target: /var/lib/taskchampion-sync-server/data
|
||||||
read_only: false
|
read_only: false
|
||||||
volume:
|
volume:
|
||||||
nocopy: true
|
nocopy: true
|
||||||
subpath: tss
|
subpath: tss/taskchampion-sync-server
|
||||||
command: --data-dir /tss/taskchampion-sync-server --port 8080
|
|
||||||
environment:
|
|
||||||
- RUST_LOG=info
|
|
||||||
depends_on:
|
depends_on:
|
||||||
mkdir:
|
mkdir:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
29
docker-entrypoint.sh
Executable file
29
docker-entrypoint.sh
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
echo "starting entrypoint script..."
|
||||||
|
if [ "$1" = "/bin/taskchampion-sync-server" ]; then
|
||||||
|
: ${DATA_DIR:=/var/lib/taskchampion-sync-server}
|
||||||
|
export DATA_DIR
|
||||||
|
echo "setting up data directory ${DATA_DIR}"
|
||||||
|
mkdir -p "${DATA_DIR}"
|
||||||
|
chown -R taskchampion:users "${DATA_DIR}"
|
||||||
|
chmod -R 700 "${DATA_DIR}"
|
||||||
|
|
||||||
|
: ${LISTEN:=0.0.0.0:8080}
|
||||||
|
export LISTEN
|
||||||
|
echo "Listen set to ${LISTEN}"
|
||||||
|
|
||||||
|
if [ -n "${CLIENT_ID}" ]; then
|
||||||
|
export CLIENT_ID
|
||||||
|
echo "Limiting to client ID ${CLIENT_ID}"
|
||||||
|
else
|
||||||
|
unset CLIENT_ID
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
echo "Running server as user 'taskchampion'"
|
||||||
|
exec su-exec taskchampion "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
eval "${@}"
|
||||||
|
fi
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "taskchampion-sync-server"
|
name = "taskchampion-sync-server"
|
||||||
version = "0.4.1"
|
version = "0.6.2-pre"
|
||||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
@ -24,3 +24,4 @@ chrono.workspace = true
|
||||||
actix-rt.workspace = true
|
actix-rt.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
temp-env.workspace = true
|
||||||
|
|
|
@ -21,30 +21,38 @@ fn command() -> Command {
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("Server for TaskChampion")
|
.about("Server for TaskChampion")
|
||||||
.arg(
|
.arg(
|
||||||
arg!(-p --port <PORT> "Port on which to serve")
|
arg!(-l --listen <ADDRESS>)
|
||||||
.help("Port on which to serve")
|
.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_parser(value_parser!(usize))
|
.value_delimiter(',')
|
||||||
.default_value("8080"),
|
.value_parser(ValueParser::string())
|
||||||
|
.env("LISTEN")
|
||||||
|
.action(ArgAction::Append)
|
||||||
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
arg!(-d --"data-dir" <DIR> "Directory in which to store data")
|
arg!(-d --"data-dir" <DIR> "Directory in which to store data")
|
||||||
.value_parser(ValueParser::os_string())
|
.value_parser(ValueParser::os_string())
|
||||||
|
.env("DATA_DIR")
|
||||||
.default_value("/var/lib/taskchampion-sync-server"),
|
.default_value("/var/lib/taskchampion-sync-server"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
arg!(-C --"allow-client-id" <CLIENT_ID> "Client IDs to allow (can be repeated; if not specified, all clients are allowed)")
|
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))
|
.value_parser(value_parser!(Uuid))
|
||||||
|
.env("CLIENT_ID")
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
.required(false),
|
.required(false),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots")
|
arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots")
|
||||||
.value_parser(value_parser!(u32))
|
.value_parser(value_parser!(u32))
|
||||||
|
.env("SNAPSHOT_VERSIONS")
|
||||||
.default_value(default_snapshot_versions),
|
.default_value(default_snapshot_versions),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
arg!(--"snapshot-days" <NUM> "Target number of days between snapshots")
|
arg!(--"snapshot-days" <NUM> "Target number of days between snapshots")
|
||||||
.value_parser(value_parser!(i64))
|
.value_parser(value_parser!(i64))
|
||||||
|
.env("SNAPSHOT_DAYS")
|
||||||
.default_value(default_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()))
|
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]
|
#[actix_web::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let matches = command().get_matches();
|
let matches = command().get_matches();
|
||||||
|
|
||||||
let data_dir: &OsString = matches.get_one("data-dir").unwrap();
|
let server_args = ServerArgs::new(matches);
|
||||||
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 config = ServerConfig {
|
let config = ServerConfig {
|
||||||
snapshot_days,
|
snapshot_days: server_args.snapshot_days,
|
||||||
snapshot_versions,
|
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);
|
let mut http_server = HttpServer::new(move || {
|
||||||
HttpServer::new(move || {
|
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, print_error))
|
.wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, print_error))
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.configure(|cfg| server.config(cfg))
|
.configure(|cfg| server.config(cfg))
|
||||||
})
|
});
|
||||||
.bind(format!("0.0.0.0:{}", port))?
|
for listen_address in server_args.listen_addresses {
|
||||||
.run()
|
log::info!("Serving on {}", listen_address);
|
||||||
.await?;
|
http_server = http_server.bind(listen_address)?
|
||||||
|
}
|
||||||
|
http_server.run().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,55 +126,187 @@ mod test {
|
||||||
use actix_web::{self, App};
|
use actix_web::{self, App};
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use taskchampion_sync_server_core::InMemoryStorage;
|
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
|
/// Get the list of allowed client IDs, sorted.
|
||||||
fn allowed(matches: &ArgMatches) -> Option<Vec<Uuid>> {
|
fn allowed(matches: ArgMatches) -> Option<Vec<Uuid>> {
|
||||||
matches
|
ServerArgs::new(matches)
|
||||||
.get_many::<Uuid>("allow-client-id")
|
.client_id_allowlist
|
||||||
.map(|ids| ids.copied().collect::<Vec<_>>())
|
.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]
|
#[test]
|
||||||
fn command_allowed_client_ids_none() {
|
fn command_allowed_client_ids_none() {
|
||||||
let matches = command().get_matches_from(["tss"]);
|
with_var_unset("CLIENT_ID", || {
|
||||||
assert_eq!(allowed(&matches), None);
|
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
|
||||||
|
assert_eq!(allowed(matches), None);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_allowed_client_ids_one() {
|
fn command_allowed_client_ids_one() {
|
||||||
let matches =
|
with_var_unset("CLIENT_ID", || {
|
||||||
command().get_matches_from(["tss", "-C", "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"]);
|
let matches = command().get_matches_from([
|
||||||
assert_eq!(
|
"tss",
|
||||||
allowed(&matches),
|
"--listen",
|
||||||
Some(vec![Uuid::parse_str(
|
"localhost:8080",
|
||||||
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
|
"-C",
|
||||||
)
|
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
|
||||||
.unwrap()])
|
]);
|
||||||
|
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]
|
#[test]
|
||||||
fn command_allowed_client_ids_two() {
|
fn command_allowed_client_ids_two() {
|
||||||
let matches = command().get_matches_from([
|
with_var_unset("CLIENT_ID", || {
|
||||||
"tss",
|
let matches = command().get_matches_from([
|
||||||
"-C",
|
"tss",
|
||||||
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
|
"--listen",
|
||||||
"-C",
|
"localhost:8080",
|
||||||
"bbaf4b61-344a-4a39-a19e-8caa0669b353",
|
"-C",
|
||||||
]);
|
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
|
||||||
assert_eq!(
|
"-C",
|
||||||
allowed(&matches),
|
"bbaf4b61-344a-4a39-a19e-8caa0669b353",
|
||||||
Some(vec![
|
]);
|
||||||
Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
|
assert_eq!(
|
||||||
Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
|
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]
|
#[test]
|
||||||
fn command_data_dir() {
|
fn command_data_dir() {
|
||||||
let matches = command().get_matches_from(["tss", "--data-dir", "/foo/bar"]);
|
with_var_unset("DATA_DIR", || {
|
||||||
assert_eq!(matches.get_one::<OsString>("data-dir").unwrap(), "/foo/bar");
|
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]
|
#[actix_rt::test]
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
[package]
|
[package]
|
||||||
name = "taskchampion-sync-server-storage-sqlite"
|
name = "taskchampion-sync-server-storage-sqlite"
|
||||||
version = "0.5.0-pre"
|
version = "0.6.2-pre"
|
||||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "SQLite backend for TaskChampion-sync-server"
|
description = "SQLite backend for TaskChampion-sync-server"
|
||||||
|
homepage = "https://github.com/GothenburgBitFactory/taskchampion"
|
||||||
|
repository = "https://github.com/GothenburgBitFactory/taskchampion-sync-server"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
taskchampion-sync-server-core = { path = "../core", version = "0.5.0-pre" }
|
taskchampion-sync-server-core = { path = "../core", version = "0.6.2-pre" }
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
|
@ -385,6 +385,23 @@ mod test {
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn test_snapshots() -> anyhow::Result<()> {
|
fn test_snapshots() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new()?;
|
let tmp_dir = TempDir::new()?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue