diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a9f45a9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +/cli +/server +/web +/gui +/env/ +/build/ +*.exe +/config.json +/.codex +/.vscode + +# for docker-compose.yml +/data \ No newline at end of file diff --git a/.gitignore b/.gitignore index cc9f142..a9f45a9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,9 @@ /env/ /build/ *.exe -/config.json \ No newline at end of file +/config.json +/.codex +/.vscode + +# for docker-compose.yml +/data \ No newline at end of file diff --git a/README.md b/README.md index cf6dffe..5e59730 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,270 @@ # CloudSave -The software is still in alpha. +CloudSave is a small client/server tool to keep save folders in sync across multiple computers. +It is aimed at games that do not provide their own cloud sync, such as emulators, old games, or any title that stores progress in a local directory. -A client/server that allows unsynchronized games (such as emulators, old games, etc.) to be kept up to date on multiple computers. +The project is still in alpha. + +## What Is In The Repository + +This repository currently contains three Go binaries: + +- `cmd/cli`: the end-user CLI (`cloudsave`) +- `cmd/server`: the HTTP API server +- `cmd/web`: a small read-only web UI that talks to the API server ## Build -You need go1.24 +The module targets Go `1.24` in [go.mod](/home/aurelie/src/cloudsave/go.mod), while the container image builds with Go `1.26.3` from [dockerfile](/home/aurelie/src/cloudsave/dockerfile). In practice, using a recent Go toolchain is recommended. -After downloading the go toolchain, just run the script `./build.sh` +To build all binaries for the platforms configured in the project: -## Usage - -### Server - -The server needs an empty directory. After creating this directory, you need to make a file that contains your credential. The format is "username:password". The server only understand bcrypt password hash for now. - -e.g.: +```bash +./build.sh ``` + +Artifacts are written to `./build`. + +If you only want one binary, you can also build it directly: + +```bash +go build -o cloudsave ./cmd/cli +go build -o cloudsave_server ./cmd/server +go build -o cloudsave_web ./cmd/web +``` + +## Server + +The server exposes an authenticated HTTP API on port `8080` by default. + +### Data Directory + +By default, the server uses: + +```text +/var/lib/cloudsave +``` + +You can override it with: + +```bash +cloudsave_server -document-root /path/to/cloudsave-data +``` + +Inside this directory, the server expects: + +- `.htpasswd`: credentials file +- `data/`: stored save archives and metadata + +### Authentication + +The API uses HTTP Basic Auth. Credentials are read from `.htpasswd`. + +Example: + +```text test:$2y$10$uULsuyROe3LVdTzFoBH7HO0zhvyKp6CX2FDNl7quXMFYqzitU0kc. ``` -To generate bcrypt password, I recommand [hash_utils](https://git.thelilfrog.com/thelilfrog/hash_utils), which is offline and secure +The code currently expects bcrypt hashes when validating passwords. -The default path to this directory is `/var/lib/cloudsave`, this can be changed with the `-document-root` argument +### Start The Server -### Client +```bash +cloudsave_server +``` -#### Register a game +Useful flags from [cmd/server/runner.go](/home/aurelie/src/cloudsave/cmd/server/runner.go): + +- `-document-root`: change the storage directory +- `-port`: change the listening port +- `-no-cache`: use the lazy repository instead of the eager cache +- `-verbose`: enable more logs + +On non-Windows systems, sending `SIGHUP` reloads the eager cache and the `.htpasswd` file. + +## Docker + +The repository contains a server-only container setup: + +- [dockerfile](/home/aurelie/src/cloudsave/dockerfile) +- [docker-compose.yml](/home/aurelie/src/cloudsave/docker-compose.yml) + +Run it with: + +```bash +docker compose up --build +``` + +This maps: + +- port `8080` +- local `./data` to `/var/lib/cloudsave` + +Before starting the container, you still need to create `./data/.htpasswd`. + +## Client + +The CLI stores its local database in the user config directory under `cloudsave/data`. + +It keeps: + +- per-game metadata +- current local archive +- backup archives +- `remote.json` per game when a remote is configured + +Saved credentials are stored separately in `credential.json`. + +Important: the `login` command stores credentials in plain text. This is also stated in the code. + +## Typical Workflow + +### 1. Register a game -You can register a game with the verb `add` ```bash cloudsave add /home/user/gamedata ``` -You can also change the name of the registration and add a remote +You can override the displayed name: + ```bash -cloudsave add -name "My Game" -remote "http://localhost:8080" /home/user/gamedata +cloudsave add -name "My Game" /home/user/gamedata ``` -#### Make an archive of the current state +Note: the `-remote` flag exists on `add`, but the current implementation does not persist it. Use `cloudsave remote -set` after `add`. -This is a command line tool, it cannot auto detect changes. -Run this command to start the scan, if needed, the tool will create a new archive +### 2. List registered games + +```bash +cloudsave list +``` + +To include local backup IDs: + +```bash +cloudsave list -include-backup +``` + +### 3. Create or refresh the local archive ```bash cloudsave scan ``` -#### Send everything on the server -This will pull and push data to the server. +This scans all registered folders. If a folder changed since the last scan, the current archive is moved to the backup history and a new `data.tar.gz` archive is created. -Note: If multiple computers are pushing to this server, a conflict may be generated. If so, the tool will ask for the version to keep +### 4. Configure the remote server for a game + +```bash +cloudsave remote -set GAME_ID http://localhost:8080 +``` + +To list configured remotes: + +```bash +cloudsave remote -list +``` + +### 5. Save credentials locally + +```bash +cloudsave login http://localhost:8080 +``` + +This verifies the credentials against the server and then stores them locally in plain text. + +### 6. Synchronize with the server ```bash cloudsave sync ``` + +The sync command: + +- groups games by remote URL +- authenticates once per remote +- compares local and remote metadata +- pushes or pulls as needed +- asks for a resolution if versions conflict + +### 7. Restore a save locally + +Apply the latest local archive for a game: + +```bash +cloudsave apply GAME_ID +``` + +Apply a specific backup: + +```bash +cloudsave apply GAME_ID BACKUP_ID +``` + +## Other CLI Commands + +Show metadata for one game: + +```bash +cloudsave show GAME_ID +``` + +Pull one game and its backups from a remote into a local path: + +```bash +cloudsave pull http://localhost:8080 GAME_ID /path/to/restore +``` + +Show local version information: + +```bash +cloudsave version +``` + +Show remote version information: + +```bash +cloudsave version -a http://localhost:8080 +``` + +Remove a registered game and its local backups: + +```bash +cloudsave remove GAME_ID +``` + +## Web UI + +The repository also contains a small web frontend in `cmd/web`. + +It uses a JSON config file, for example: + +```json +{ + "server": { + "port": 8081 + }, + "remote": { + "url": "http://localhost:8080" + } +} +``` + +Then start it with: + +```bash +cloudsave_web -config /path/to/config.json +``` + +The web UI itself does not manage users. It forwards HTTP Basic Auth credentials to the configured API server. + +## Current Caveats + +These points are worth knowing in the current state of the project: + +- the software is still alpha +- credentials saved by `cloudsave login` are stored in plain text +- the Docker setup only runs the API server, not the web UI +- `add -remote` is exposed by the CLI but is not currently persisted by the service layer + diff --git a/api.Dockerfile b/api.Dockerfile new file mode 100644 index 0000000..fcd7ffb --- /dev/null +++ b/api.Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.26.3-trixie AS build + +ENV GOOS=linux +ENV CGO_ENABLED=0 + +ENV GOAMD64=v3 +ENV GORISCV64=rva22u64 +ENV GOARM64=v8.2 + +COPY . /src + +RUN cd /src \ + && go build -ldflags="-s -w" -o server ./cmd/server \ + && chown 0:0 server \ + && chmod ugo+x server + +FROM busybox:1.37.0 AS prod + +COPY --from=build /etc/passwd /etc/passwd +COPY --from=build /etc/shadow /etc/shadow +COPY --from=build /src/server /server + +VOLUME [ "/var/lib/cloudsave" ] +ENTRYPOINT [ "/server" ] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8e76a7d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,63 @@ +services: + api: + build: + context: . + dockerfile: api.Dockerfile + volumes: + - "./data:/var/lib/cloudsave" + networks: + - cloudsave_net + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8080/heartbeat || exit 1 + interval: 3s + timeout: 2s + retries: 3 + start_period: 10s + labels: + - "traefik.enable=true" + - "traefik.http.routers.api-router.rule=Host(`${DOMAIN:-localhost}`) && PathPrefix(`/api`)" + - "traefik.http.routers.api-router.entrypoints=web" + - "traefik.http.services.cloudsave-api.loadbalancer.server.port=8080" + + web: + build: + context: . + dockerfile: web.Dockerfile + volumes: + - "./config.json:/var/lib/cloudsave/config.json" + networks: + - cloudsave_net + depends_on: + api: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.web-router.rule=Host(`${DOMAIN:-localhost}`) && PathPrefix(`/web`)" + - "traefik.http.routers.web-router.entrypoints=web" + - "traefik.http.services.cloudsave-web.loadbalancer.server.port=8080" + + proxy: + image: traefik:3.7.0 + ports: + - 127.0.0.1:80:80 + - 127.0.0.1:8080:8080 + networks: + - cloudsave_net + depends_on: + api: + condition: service_healthy + web: + condition: service_started + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + - --api.dashboard=true + - --api.insecure=true + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --log.level=DEBUG + - --accesslog=true + +networks: + cloudsave_net: \ No newline at end of file diff --git a/web.Dockerfile b/web.Dockerfile new file mode 100644 index 0000000..87d8f06 --- /dev/null +++ b/web.Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.26.3-trixie AS build + +ENV GOOS=linux +ENV CGO_ENABLED=0 + +ENV GOAMD64=v3 +ENV GORISCV64=rva22u64 +ENV GOARM64=v8.2 + +COPY . /src + +RUN cd /src \ + && go build -ldflags="-s -w" -o web ./cmd/web \ + && chown 0:0 web \ + && chmod ugo+x web + +FROM scratch AS prod + +COPY --from=build /etc/passwd /etc/passwd +COPY --from=build /etc/shadow /etc/shadow +COPY --from=build /src/web /web + +VOLUME [ "/var/lib/cloudsave" ] +ENTRYPOINT [ "/web" ] \ No newline at end of file