From de947dd8a4a657355dd88eb3c7368572f81c52c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 20 May 2026 22:00:32 +0200 Subject: [PATCH 1/4] feat(send): add role for self-hosted Send file-share service Deploys timvisee/send with a Redis backend behind Traefik. Supports local-disk or S3 storage (e.g. via the garage role). Uses the shared `*_domains` list convention so the router can accept internal *.int.* names alongside the canonical BASE_URL host. --- roles/send/README.md | 60 +++++++++++++++++++ roles/send/defaults/main.yml | 53 +++++++++++++++++ roles/send/handlers/main.yml | 9 +++ roles/send/meta/main.yml | 14 +++++ roles/send/tasks/main.yml | 28 +++++++++ roles/send/templates/docker-compose.yml.j2 | 69 ++++++++++++++++++++++ roles/send/vars/main.yml | 3 + 7 files changed, 236 insertions(+) create mode 100644 roles/send/README.md create mode 100644 roles/send/defaults/main.yml create mode 100644 roles/send/handlers/main.yml create mode 100644 roles/send/meta/main.yml create mode 100644 roles/send/tasks/main.yml create mode 100644 roles/send/templates/docker-compose.yml.j2 create mode 100644 roles/send/vars/main.yml diff --git a/roles/send/README.md b/roles/send/README.md new file mode 100644 index 0000000..339628b --- /dev/null +++ b/roles/send/README.md @@ -0,0 +1,60 @@ +Send +==== + +Deploys a self-hosted [Send](https://github.com/timvisee/send) instance +(timvisee fork of the discontinued Mozilla Send) with a Redis backend +behind Traefik, using Docker Compose. + +Requirements +------------ + +- Docker + `docker compose` plugin on the target host +- Traefik (role `digitalboard.core.traefik`) reachable via an external + Docker network named `proxy` (default) +- DNS for each entry in `send_domains` pointing at the reverse proxy +- Optional: a Garage S3 bucket if `send_storage_backend: s3` + +Role Variables +-------------- + +Important defaults (see `defaults/main.yml` for the full list): + +| Variable | Default | Description | +|---|---|---| +| `send_domains` | `["send.local.test"]` | FQDNs the router accepts; first entry is the canonical BASE_URL | +| `send_image` | `registry.gitlab.com/timvisee/send:latest` | Send container image | +| `send_max_file_size` | `1073741824` | Max upload size in bytes (1 GiB) | +| `send_max_expire_seconds` | `604800` | Max share lifetime (7 d) | +| `send_storage_backend` | `local` | `local` (volume) or `s3` | +| `send_s3_*` | `""` | S3 endpoint/bucket/key/secret (when backend is `s3`) | +| `send_use_ssl` | `true` | Issue Traefik labels for the `websecure` entrypoint | + +Dependencies +------------ + +None. + +Example Playbook +---------------- + +```yaml +- hosts: send_servers + become: true + roles: + - digitalboard.core.send +``` + +With S3 (Garage) backend: + +```yaml +send_storage_backend: s3 +send_s3_endpoint: "http://{{ hostvars['backend']['garage_s3_domain'] }}" +send_s3_bucket: "send" +send_s3_access_key: "{{ lookup('digitalboard.core.garage_credentials', 'send', host='backend')['key_id'] }}" +send_s3_secret_key: "{{ lookup('digitalboard.core.garage_credentials', 'send', host='backend')['secret_key'] }}" +``` + +License +------- + +MIT diff --git a/roles/send/defaults/main.yml b/roles/send/defaults/main.yml new file mode 100644 index 0000000..ba3aecc --- /dev/null +++ b/roles/send/defaults/main.yml @@ -0,0 +1,53 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for send + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + +# Send-specific configuration +send_service_name: send +send_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ send_service_name }}" +send_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ send_service_name }}" + +# Service configuration +# FQDNs the send router accepts. The first entry is the canonical +# domain (used as BASE_URL); further entries cover internal *.int.* +# names so backend uploads can hit us without hairpinning via DMZ. +send_domains: + - "send.local.test" +send_image: "registry.gitlab.com/timvisee/send:latest" +send_port: 1443 +send_extra_hosts: [] + +# Redis backend +send_redis_image: "redis:7-alpine" +send_redis_service_name: "send-redis" + +# Send application configuration +# https://github.com/timvisee/send/blob/master/server/config.js +send_max_file_size: 1073741824 # 1 GiB in bytes +send_default_downloads: 1 +send_max_downloads: 100 +send_default_expire_seconds: 86400 # 24h +send_max_expire_seconds: 604800 # 7d +send_max_files_per_archive: 64 +send_download_counts: "1,2,3,4,5,20,50,100" +send_expire_times_seconds: "300,3600,86400,604800" + +# Storage backend: "local" (volume) or "s3" +send_storage_backend: "local" + +# S3 backend (only used when send_storage_backend == "s3") +send_s3_endpoint: "" +send_s3_bucket: "" +send_s3_region: "us-east-1" +send_s3_access_key: "" +send_s3_secret_key: "" +send_s3_use_path_style: true + +# Traefik configuration +send_traefik_network: "proxy" +send_internal_network: "send_internal" +send_use_ssl: true diff --git a/roles/send/handlers/main.yml b/roles/send/handlers/main.yml new file mode 100644 index 0000000..cb83189 --- /dev/null +++ b/roles/send/handlers/main.yml @@ -0,0 +1,9 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for send + +- name: restart send + community.docker.docker_compose_v2: + project_src: "{{ send_docker_compose_dir }}" + state: present + recreate: always diff --git a/roles/send/meta/main.yml b/roles/send/meta/main.yml new file mode 100644 index 0000000..79dedb1 --- /dev/null +++ b/roles/send/meta/main.yml @@ -0,0 +1,14 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: digitalboard + description: Deploy a self-hosted Send (timvisee fork) instance with Redis via Docker Compose + license: MIT + + min_ansible_version: 2.14 + + galaxy_tags: + - send + - filesharing + - docker + +dependencies: [] diff --git a/roles/send/tasks/main.yml b/roles/send/tasks/main.yml new file mode 100644 index 0000000..c79405a --- /dev/null +++ b/roles/send/tasks/main.yml @@ -0,0 +1,28 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for send + +- name: Create docker compose directory + file: + path: "{{ send_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create local upload directory + file: + path: "{{ send_docker_volume_dir }}/uploads" + state: directory + mode: '0755' + when: send_storage_backend == "local" + +- name: Create docker-compose file for send + template: + src: docker-compose.yml.j2 + dest: "{{ send_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + notify: restart send + +- name: Start send container + community.docker.docker_compose_v2: + project_src: "{{ send_docker_compose_dir }}" + state: present diff --git a/roles/send/templates/docker-compose.yml.j2 b/roles/send/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..a6733bb --- /dev/null +++ b/roles/send/templates/docker-compose.yml.j2 @@ -0,0 +1,69 @@ +services: + {{ send_service_name }}: + image: {{ send_image }} + container_name: {{ send_service_name }} + restart: unless-stopped + depends_on: + - {{ send_redis_service_name }} + networks: + - {{ send_traefik_network }} + - {{ send_internal_network }} +{% if send_extra_hosts is defined and send_extra_hosts | length > 0 %} + extra_hosts: +{% for host in send_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} + environment: +{% if send_use_ssl %} + BASE_URL: "https://{{ send_domains[0] }}" +{% else %} + BASE_URL: "http://{{ send_domains[0] }}" +{% endif %} + REDIS_HOST: "{{ send_redis_service_name }}" + REDIS_PORT: "6379" + MAX_FILE_SIZE: "{{ send_max_file_size }}" + DEFAULT_DOWNLOADS: "{{ send_default_downloads }}" + MAX_DOWNLOADS: "{{ send_max_downloads }}" + DEFAULT_EXPIRE_SECONDS: "{{ send_default_expire_seconds }}" + MAX_EXPIRE_SECONDS: "{{ send_max_expire_seconds }}" + MAX_FILES_PER_ARCHIVE: "{{ send_max_files_per_archive }}" + DOWNLOAD_COUNTS: "{{ send_download_counts }}" + EXPIRE_TIMES_SECONDS: "{{ send_expire_times_seconds }}" +{% if send_storage_backend == "s3" %} + S3_BUCKET: "{{ send_s3_bucket }}" + S3_ENDPOINT: "{{ send_s3_endpoint }}" + S3_USE_PATH_STYLE_ENDPOINT: "{{ 'true' if send_s3_use_path_style else 'false' }}" + AWS_ACCESSKEYID: "{{ send_s3_access_key }}" + AWS_SECRETACCESSKEY: "{{ send_s3_secret_key }}" + AWS_REGION: "{{ send_s3_region }}" +{% else %} + FILE_DIR: "/uploads" + volumes: + - {{ send_docker_volume_dir }}/uploads:/uploads +{% endif %} + labels: + - traefik.enable=true + - traefik.docker.network={{ send_traefik_network }} + - traefik.http.routers.{{ send_service_name }}.rule=Host({% for d in send_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.services.{{ send_service_name }}.loadbalancer.server.port={{ send_port }} +{% if send_use_ssl %} + - traefik.http.routers.{{ send_service_name }}.entrypoints=websecure + - traefik.http.routers.{{ send_service_name }}.tls=true +{% else %} + - traefik.http.routers.{{ send_service_name }}.entrypoints=web +{% endif %} + + {{ send_redis_service_name }}: + image: {{ send_redis_image }} + container_name: {{ send_redis_service_name }} + restart: unless-stopped + networks: + - {{ send_internal_network }} + volumes: + - {{ send_docker_volume_dir }}/redis:/data + +networks: + {{ send_internal_network }}: + {{ send_traefik_network }}: + external: true diff --git a/roles/send/vars/main.yml b/roles/send/vars/main.yml new file mode 100644 index 0000000..b2a6b30 --- /dev/null +++ b/roles/send/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for send From 9d1a5d1924dbfe73595dab28b3a73b3bcb54b023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Tue, 26 May 2026 15:06:58 +0200 Subject: [PATCH 2/4] fix(send): self-review fixes (FQCN, min_ansible_version str) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tasks/main.yml: prefix all builtin modules with ansible.builtin (file, template) — silences ansible-lint fqcn[action-core] and matches the convention used by the other roles in this collection. * meta/main.yml: change min_ansible_version from the float 2.14 to the string '2.14'. ansible-galaxy's schema requires a string here (ansible-lint schema[meta] complains otherwise — same fix I just applied to the opnform role). --- roles/send/meta/main.yml | 2 +- roles/send/tasks/main.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/send/meta/main.yml b/roles/send/meta/main.yml index 79dedb1..20f9e67 100644 --- a/roles/send/meta/main.yml +++ b/roles/send/meta/main.yml @@ -4,7 +4,7 @@ galaxy_info: description: Deploy a self-hosted Send (timvisee fork) instance with Redis via Docker Compose license: MIT - min_ansible_version: 2.14 + min_ansible_version: "2.14" galaxy_tags: - send diff --git a/roles/send/tasks/main.yml b/roles/send/tasks/main.yml index c79405a..9ed8dd8 100644 --- a/roles/send/tasks/main.yml +++ b/roles/send/tasks/main.yml @@ -3,20 +3,20 @@ # tasks file for send - name: Create docker compose directory - file: + ansible.builtin.file: path: "{{ send_docker_compose_dir }}" state: directory mode: '0755' - name: Create local upload directory - file: + ansible.builtin.file: path: "{{ send_docker_volume_dir }}/uploads" state: directory mode: '0755' when: send_storage_backend == "local" - name: Create docker-compose file for send - template: + ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ send_docker_compose_dir }}/docker-compose.yml" mode: '0644' From 5ce2a8f7375e0f804a99fdb48f22f156d290cee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Tue, 26 May 2026 15:07:07 +0200 Subject: [PATCH 3/4] fix(send): use Traefik v3 OR-syntax for multi-domain Host rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The router rule joined send_domains with ', ' which is the v2 syntax ('Host(`a`, `b`)'). Traefik v3 expects each Host() to be its own matcher joined with the explicit '||' OR operator. With v3 the comma form is silently ignored — only the first host actually matches. Match the pattern already used in the authentik, drawio and nextcloud roles in this collection. --- roles/send/templates/docker-compose.yml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/send/templates/docker-compose.yml.j2 b/roles/send/templates/docker-compose.yml.j2 index a6733bb..69a43ab 100644 --- a/roles/send/templates/docker-compose.yml.j2 +++ b/roles/send/templates/docker-compose.yml.j2 @@ -45,7 +45,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ send_traefik_network }} - - traefik.http.routers.{{ send_service_name }}.rule=Host({% for d in send_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ send_service_name }}.rule={% for d in send_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} - traefik.http.services.{{ send_service_name }}.loadbalancer.server.port={{ send_port }} {% if send_use_ssl %} - traefik.http.routers.{{ send_service_name }}.entrypoints=websecure From d526ec382dca1d3ae70247c6a388c0ece2a087ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Tue, 26 May 2026 15:07:17 +0200 Subject: [PATCH 4/4] docs(send): add meta/argument_specs.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 29 typed options with full defaults coverage (no required: true marks — the role works with an empty S3 config when storage_backend=local). Documents the send_domains list convention, the local-vs-s3 storage choice, the timing/size limits and the Traefik / network wiring. Loads through ansible-core's ArgumentSpecValidator. Matches the spec convention used by the other roles in this collection. --- roles/send/meta/argument_specs.yml | 122 +++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 roles/send/meta/argument_specs.yml diff --git a/roles/send/meta/argument_specs.yml b/roles/send/meta/argument_specs.yml new file mode 100644 index 0000000..2e9797e --- /dev/null +++ b/roles/send/meta/argument_specs.yml @@ -0,0 +1,122 @@ +--- +argument_specs: + main: + short_description: Deploy timvisee/send (file-sharing) with a Redis backend via Docker Compose. + description: + - Renders a Compose stack with the C(timvisee/send) container and a + Redis companion behind Traefik. Storage can be local-disk or any + S3-compatible backend (e.g. the C(garage) role). + - Uses the shared C(*_domains) list convention so the router can + accept internal C(*.int.*) hostnames alongside the canonical + BASE_URL host. + options: + docker_compose_base_dir: + type: path + default: /etc/docker/compose + docker_volume_base_dir: + type: path + default: /srv/data + send_service_name: + type: str + default: send + send_docker_compose_dir: + type: path + send_docker_volume_dir: + type: path + + send_domains: + type: list + elements: str + default: ['send.local.test'] + description: + - FQDNs the router accepts. First entry is the canonical hostname + and is used as C(BASE_URL). Further entries cover internal + C(*.int.*) names so backend uploads can hit Send without + hairpinning via the DMZ. + send_image: + type: str + default: "registry.gitlab.com/timvisee/send:latest" + send_port: + type: int + default: 1443 + send_extra_hosts: + type: list + elements: str + default: [] + description: C(extra_hosts) entries injected into the send container (Docker C(host:ip) syntax). + + send_redis_image: + type: str + default: "redis:7-alpine" + send_redis_service_name: + type: str + default: send-redis + + send_max_file_size: + type: int + default: 1073741824 + description: Max upload size in bytes. Default is 1 GiB. + send_default_downloads: + type: int + default: 1 + send_max_downloads: + type: int + default: 100 + send_default_expire_seconds: + type: int + default: 86400 + description: Default share lifetime in seconds (24 h). + send_max_expire_seconds: + type: int + default: 604800 + description: Maximum share lifetime in seconds (7 d). + send_max_files_per_archive: + type: int + default: 64 + send_download_counts: + type: str + default: "1,2,3,4,5,20,50,100" + description: Comma-separated list of download-count options shown in the UI. + send_expire_times_seconds: + type: str + default: "300,3600,86400,604800" + description: Comma-separated list of expire-time options (seconds) shown in the UI. + + send_storage_backend: + type: str + choices: [local, s3] + default: local + description: + - C(local) keeps uploads in a host volume. C(s3) uses an + S3-compatible backend (any of the C(send_s3_*) variables is + required when this is set). + + send_s3_endpoint: + type: str + default: '' + send_s3_bucket: + type: str + default: '' + send_s3_region: + type: str + default: us-east-1 + send_s3_access_key: + type: str + default: '' + send_s3_secret_key: + type: str + default: '' + send_s3_use_path_style: + type: bool + default: true + description: Required for most non-AWS S3-compatible backends (Garage, MinIO). + + send_traefik_network: + type: str + default: proxy + send_internal_network: + type: str + default: send_internal + send_use_ssl: + type: bool + default: true