Compare commits

..

No commits in common. "main" and "v0.5.0" have entirely different histories.
main ... v0.5.0

18 changed files with 451 additions and 846 deletions

View file

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

58
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,58 @@
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,6 +2,8 @@ name: Build Docker
on: on:
push: push:
branches:
- '*'
tags: tags:
- '*' - '*'

View file

@ -13,8 +13,6 @@ 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

View file

@ -2,7 +2,7 @@ name: security
on: on:
schedule: schedule:
- cron: '33 0 * * THU' - cron: '0 0 * * *'
push: push:
paths: paths:
- '**/Cargo.toml' - '**/Cargo.toml'

708
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,22 +5,20 @@ members = [
"server", "server",
"sqlite", "sqlite",
] ]
rust-version = "1.81.0" # MSRV
[workspace.dependencies] [workspace.dependencies]
uuid = { version = "^1.17.0", features = ["serde", "v4"] } uuid = { version = "^1.11.0", features = ["serde", "v4"] }
actix-web = "^4.11.0" actix-web = "^4.9.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", "env"] } clap = { version = "^4.5.6", features = ["string"] }
log = "^0.4.17" log = "^0.4.17"
env_logger = "^0.11.7" env_logger = "^0.11.5"
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"

View file

@ -1,25 +1,19 @@
# Versions must be major.minor # Versions must be major.minor
# Default versions are as below ARG RUST_VERSION
ARG RUST_VERSION=1.78 ARG ALPINE_VERSION
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 Cargo.lock Cargo.toml /data/ COPY . /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 apk add --no-cache su-exec && \ RUN adduser -S -D -H -h /var/lib/taskchampion-sync-server -s /sbin/nologin -G users \
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 -m1755 -o1092 -g1092 "/var/lib/taskchampion-sync-server" install -d -m755 -o100 -g100 "/var/lib/taskchampion-sync-server"
EXPOSE 8080 EXPOSE 8080
VOLUME /var/lib/taskchampion-sync-server/data VOLUME "/var/lib/taskchampion-sync-server"
COPY docker-entrypoint.sh /bin USER taskchampion
ENTRYPOINT [ "/bin/docker-entrypoint.sh" ] ENTRYPOINT [ "taskchampion-sync-server" ]
CMD [ "/bin/taskchampion-sync-server" ]

View file

@ -27,42 +27,31 @@ use a reverse proxy such as Nginx, haproxy, or Apache httpd.
### Using Docker-Compose ### Using Docker-Compose
Every release of the server generates a Docker image in The [`docker-compose.yml`](./docker-compose.yml) file in this repository is
`ghcr.io/gothenburgbitfactory/taskchampion-sync-server`. The tags include sufficient to run taskchampion-sync-server, including setting up TLS
`latest` for the latest release, and both minor and patch versions, e.g., `0.5` certificates using Lets Encrypt, thanks to [Caddy](https://caddyserver.com/).
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, download `docker-compose.yml` from the link above (it is pinned On that server, clone this repository (or just download `docker-compose.yml` to
to the latest release) into the current directory. Then run the current directory -- the rest of the contents of this repository are not
required) and run
```sh ```sh
TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com \ TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com docker compose up
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 message "certificate obtained successfully" when this is complete, or log a msg "certificate obtained successfully" when this is complete, or error
error messages if the process fails. Once this process is complete, configure messages if the process fails. Once this process is complete, configure your
your `.taskrc`'s to point to the server: `.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
@ -76,20 +65,11 @@ 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 `--listen` option specifies the interface and port the server listens on. The `--data-dir` option specifies where the server should store its data, and
It must contain an IP-Address or a DNS name and a port number. This option is `--port` gives the port on which the HTTP server runs.
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,
specify them in the environment variable `CLIENT_ID`, as a comma-separated list such as when running a personal server, use `--allow-client-id <client-id>`.
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
@ -108,11 +88,7 @@ release version. You can install Rust from your distribution package or use
rustup default stable rustup default stable
``` ```
The minimum supported Rust version (MSRV) is given in If you prefer, you can use the stable version only for install TaskChampion
[`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
@ -132,7 +108,6 @@ 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.
@ -154,12 +129,4 @@ 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. You may also set `DATA_DIR`, `CLIENT_ID`, or `LISTEN` with `-e`, e.g., delete container.
```sh
docker run -t -d \
--name=taskchampion \
-e LISTEN=0.0.0.0:9000 \
-p 9000:9000 \
taskchampion-sync-server
```

View file

@ -1,21 +0,0 @@
# 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,11 +1,9 @@
[package] [package]
name = "taskchampion-sync-server-core" name = "taskchampion-sync-server-core"
version = "0.6.2-pre" version = "0.5.0-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]

View file

@ -130,6 +130,7 @@ 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,
@ -142,33 +143,15 @@ impl StorageTxn for InnerTxn<'_> {
snap.versions_since += 1; snap.versions_since += 1;
} }
} else { } else {
anyhow::bail!("Client {} does not exist", self.client_id); return Err(anyhow::anyhow!("Client {} does not exist", self.client_id));
} }
if self self.guard
.guard
.children .children
.insert((self.client_id, parent_version_id), version_id) .insert((self.client_id, parent_version_id), version_id);
.is_some() self.guard
{
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(())
@ -276,25 +259,6 @@ 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();

View file

@ -1,13 +1,16 @@
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
@ -43,21 +46,19 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
tss: tss:
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:0.6.1 image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:main
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: /var/lib/taskchampion-sync-server/data target: /tss
read_only: false read_only: false
volume: volume:
nocopy: true nocopy: true
subpath: tss/taskchampion-sync-server subpath: tss
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

View file

@ -1,29 +0,0 @@
#!/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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "taskchampion-sync-server" name = "taskchampion-sync-server"
version = "0.6.2-pre" version = "0.4.1"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"] authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2021" edition = "2021"
publish = false publish = false
@ -24,4 +24,3 @@ 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

View file

@ -21,38 +21,30 @@ fn command() -> Command {
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about("Server for TaskChampion") .about("Server for TaskChampion")
.arg( .arg(
arg!(-l --listen <ADDRESS>) arg!(-p --port <PORT> "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") .help("Port on which to serve")
.value_delimiter(',') .value_parser(value_parser!(usize))
.value_parser(ValueParser::string()) .default_value("8080"),
.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),
) )
} }
@ -64,59 +56,35 @@ 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 server_args = ServerArgs::new(matches); let data_dir: &OsString = matches.get_one("data-dir").unwrap();
let config = ServerConfig { let port: usize = *matches.get_one("port").unwrap();
snapshot_days: server_args.snapshot_days, let snapshot_versions: u32 = *matches.get_one("snapshot-versions").unwrap();
snapshot_versions: server_args.snapshot_versions, let snapshot_days: i64 = *matches.get_one("snapshot-days").unwrap();
}; let client_id_allowlist: Option<HashSet<Uuid>> = matches
let server = WebServer::new( .get_many("allow-client-id")
config, .map(|ids| ids.copied().collect());
server_args.client_id_allowlist,
SqliteStorage::new(server_args.data_dir)?,
);
let mut http_server = HttpServer::new(move || { let config = ServerConfig {
snapshot_days,
snapshot_versions,
};
let server = WebServer::new(config, client_id_allowlist, SqliteStorage::new(data_dir)?);
log::info!("Serving on port {}", port);
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))
}); })
for listen_address in server_args.listen_addresses { .bind(format!("0.0.0.0:{}", port))?
log::info!("Serving on {}", listen_address); .run()
http_server = http_server.bind(listen_address)? .await?;
}
http_server.run().await?;
Ok(()) Ok(())
} }
@ -126,187 +94,55 @@ 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, sorted. /// Get the list of allowed client IDs
fn allowed(matches: ArgMatches) -> Option<Vec<Uuid>> { fn allowed(matches: &ArgMatches) -> Option<Vec<Uuid>> {
ServerArgs::new(matches) matches
.client_id_allowlist .get_many::<Uuid>("allow-client-id")
.map(|ids| ids.into_iter().collect::<Vec<_>>()) .map(|ids| ids.copied().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() {
with_var_unset("CLIENT_ID", || { let matches = command().get_matches_from(["tss"]);
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]); assert_eq!(allowed(&matches), None);
assert_eq!(allowed(matches), None);
});
} }
#[test] #[test]
fn command_allowed_client_ids_one() { fn command_allowed_client_ids_one() {
with_var_unset("CLIENT_ID", || { let matches =
let matches = command().get_matches_from([ command().get_matches_from(["tss", "-C", "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"]);
"tss", assert_eq!(
"--listen", allowed(&matches),
"localhost:8080", Some(vec![Uuid::parse_str(
"-C", "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
"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() {
with_var_unset("CLIENT_ID", || { let matches = command().get_matches_from([
let matches = command().get_matches_from([ "tss",
"tss", "-C",
"--listen", "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
"localhost:8080", "-C",
"-C", "bbaf4b61-344a-4a39-a19e-8caa0669b353",
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", ]);
"-C", assert_eq!(
"bbaf4b61-344a-4a39-a19e-8caa0669b353", allowed(&matches),
]); Some(vec![
assert_eq!( Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
allowed(matches), Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
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() {
with_var_unset("DATA_DIR", || { let matches = command().get_matches_from(["tss", "--data-dir", "/foo/bar"]);
let matches = command().get_matches_from([ assert_eq!(matches.get_one::<OsString>("data-dir").unwrap(), "/foo/bar");
"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]

View file

@ -1,15 +1,13 @@
[package] [package]
name = "taskchampion-sync-server-storage-sqlite" name = "taskchampion-sync-server-storage-sqlite"
version = "0.6.2-pre" version = "0.5.0-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.6.2-pre" } taskchampion-sync-server-core = { path = "../core", version = "0.5.0-pre" }
uuid.workspace = true uuid.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true

View file

@ -385,23 +385,6 @@ 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()?;