Compare commits

..

1 Commits

Author SHA1 Message Date
Michal Tuszowski f3f07bc921 file share: route the bot's client through config + shared auth
file_search_functions was the only bot-side service client that hardcoded the
musician's LAN address and sent no auth header. Both musician endpoints it
calls (/get_share_list, /get_share_links) run _authorize_request(), so the
whole file-share feature 401'd on any deployment that set CONJURER_API_KEY -
which the deployment guides instruct you to do - and the hardcoded IP made a
musician on another host (the Proxmox/docker split) unreachable regardless.

Use FILE_SERVICE_ADDRESS + service_headers() like music_functions does, add
the two endpoint paths to constants alongside the existing ones, and set an
explicit timeout so a wedged musician can't hang the calling cog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:02:27 +02:00
11 changed files with 48 additions and 441 deletions
+5 -14
View File
@@ -7,13 +7,9 @@ import argparse
import sys
from pathlib import Path
# — CONFIGURATION — env-overridable; defaults = original bare-metal Pi layout
# APACHE_LOG must be the share-specific access log (see docker/share-vhost.conf);
# pointing it at the site-wide log also works but wastes time scanning noise.
APACHE_LOG = os.getenv('CONJURER_APACHE_LOG', '/var/log/apache2/share_access.log')
SHARE_DIR = Path(os.getenv('CONJURER_SHARE_DIR', '/var/www/html/share'))
# Seconds to keep a link alive after its FIRST download (not after creation).
TTL_SECONDS = int(os.getenv('CONJURER_SHARE_TTL_SECONDS', '3600'))
# — CONFIGURATION — adjust as needed
APACHE_LOG = '/var/log/apache2/share_access.log' # <- point to your share-specific log!
SHARE_DIR = Path('/var/www/html/share')
METADATA_FILE = SHARE_DIR / '.downloads.json'
STATE_FILE = SHARE_DIR / '.logpos'
# Regex to match lines like: "GET /share/abcdef1234... HTTP/1.1"
@@ -70,7 +66,7 @@ def record_downloads(lines, metadata, args):
def revoke_old(metadata, args):
now = time.time()
to_remove = [tok for tok, ts in metadata.items() if now - ts >= TTL_SECONDS]
to_remove = [tok for tok, ts in metadata.items() if now - ts >= 3600]
for tok in to_remove:
link = SHARE_DIR / tok
try:
@@ -82,12 +78,7 @@ def revoke_old(metadata, args):
return bool(to_remove)
def main():
p = argparse.ArgumentParser(
description=(
"Revoke share links CONJURER_SHARE_TTL_SECONDS "
f"(currently {TTL_SECONDS}s) after their first download"
)
)
p = argparse.ArgumentParser(description="Revoke share links 2min after download")
p.add_argument('--debug', action='store_true', help='print debug info to stderr')
args = p.parse_args()
+5 -12
View File
@@ -1,18 +1,12 @@
#!/usr/bin/env python3
"""Build the share index that the musician's /get_share_list endpoint searches.
Walks the media root and writes a JSON catalogue of every file/dir found.
Meant to run periodically (cron on bare metal, the scheduler loop in the share
container). Paths are environment-overridable; the defaults reproduce the
original Raspberry Pi layout, so existing cron entries keep working unchanged.
"""
import os
import json
import time
from pathlib import Path
# Configuration (env-overridable; defaults = original bare-metal Pi layout)
SCAN_PATH = os.getenv('CONJURER_SHARE_SCAN_PATH', '/mnt/shares')
OUTPUT_FILE = os.getenv('CONJURER_SHARE_DB', '/var/log/share_scan.json')
# Configuration
SCAN_PATH = '/mnt/shares'
OUTPUT_FILE = '/var/log/share_scan.json'
def scan_directory(root):
"""Recursively walk `root` and collect metadata."""
@@ -40,8 +34,7 @@ def main():
'root': SCAN_PATH,
'entries': scan_directory(SCAN_PATH),
}
# Write atomically (create the target dir first so a fresh volume works)
os.makedirs(os.path.dirname(OUTPUT_FILE) or '.', exist_ok=True)
# Write atomically
temp = OUTPUT_FILE + '.tmp'
with open(temp, 'w') as f:
json.dump(data, f, indent=2)
+2
View File
@@ -82,6 +82,8 @@ MUZYKA_MOJEGO_LUDU_PLAJLISTA = 30
GET_MP3 = "/mp3"
SEND_MP3 = "/update_mp3"
GET_PLAYLIST = "/get_music"
GET_SHARE_LIST = "/get_share_list"
GET_SHARE_LINKS = "/get_share_links"
ADD_TO_PRIO_PLAYLIST = "/add_to_priority"
CREATE_PRIO_PLAYLIST = "/create_priority_playlist"
-36
View File
@@ -1,36 +0,0 @@
# Conjurer share service: Apache publishing short-lived symlink shares, plus
# the two periodic jobs that build the search index and revoke expired links.
#
# This is the piece that used to live only as hand-placed scripts + hand-edited
# Apache config on the host. Everything it needs is now in the image; the state
# it shares with the musician (the link dir and the index) comes in as volumes.
#
# Build from the repository root:
# docker build -f docker/Dockerfile.share -t conjurer-share .
FROM debian:bookworm-slim
# apache2 serves the links; python3 runs the scanner/revoker (both stdlib-only,
# so there is nothing to pip install).
RUN apt-get update && apt-get install -y --no-install-recommends \
apache2 python3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Same scripts the bare-metal deployment uses - no forked copies.
COPY conjurer_musician/scan_shares.py ./scan_shares.py
COPY conjurer_musician/revoke_shares.py ./revoke_shares.py
COPY docker/share-vhost.conf.tpl ./share-vhost.conf.tpl
COPY docker/entrypoint.share.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# /mnt/shares - media library (mount read-only; symlink targets)
# /var/www/html/share - the symlinks themselves (SHARED with the musician,
# which is what creates them)
# /srv/share/db - share_scan.json (SHARED with the musician, which
# searches it)
# /var/log/apache2 - access log the revoker tails
VOLUME ["/mnt/shares", "/var/www/html/share", "/srv/share/db", "/var/log/apache2"]
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
-43
View File
@@ -1,43 +0,0 @@
# Share VM/host: Apache publishing short-lived file links + the scan/revoke jobs.
# Run from the repository root:
# cp docker/env/share.env.example docker/env/share.env # then edit
# docker compose -f docker/compose.share.yaml up -d --build
#
# IMPORTANT - this service shares two paths with the musician, which is the
# process that actually creates the links:
#
# /srv/share/links -> the symlinks (musician writes, Apache reads)
# /srv/share/db -> share_scan.json (this service writes, musician reads)
#
# So the musician must mount the same two host paths (see the CONJURER_SHARE_*
# block in docker/env/musician.env.example). If the musician runs on a different
# machine, put both on shared storage - otherwise it will happily create links
# that this container cannot see.
#
# The media library must be mounted here at the SAME absolute path that the
# index recorded, because the symlink targets are absolute. Default: /mnt/shares.
services:
conjurer-share:
build:
context: ..
dockerfile: docker/Dockerfile.share
image: conjurer-share:latest
container_name: conjurer-share
restart: unless-stopped
env_file:
- env/share.env
ports:
# Plain HTTP. Terminate TLS for czernobog.pl on your existing reverse
# proxy and forward /share here.
- "8081:80"
volumes:
# Media library the links point at. Read-only: this service only ever
# serves these files, it never writes them.
- /srv/share/media:/mnt/shares:ro
# The published symlinks - shared with the musician.
- /srv/share/links:/var/www/html/share
# The search index - shared with the musician.
- /srv/share/db:/srv/share/db
# Access log the revoker tails; kept on the host so link history survives
# a container rebuild.
- /srv/share/logs:/var/log/apache2
-67
View File
@@ -1,67 +0,0 @@
#!/bin/sh
# Entrypoint for the Conjurer share container: renders the vhost, seeds the
# share index, then runs the two periodic jobs alongside Apache.
#
# Cron is deliberately not used. Both scripts are already written to be safe to
# re-run (the scanner writes atomically, the revoker persists its log position),
# so plain sleep loops give the same result with one less moving part and with
# their output on the container's stdout where `docker logs` can see it.
set -eu
: "${CONJURER_SHARE_SERVER_NAME:=localhost}"
: "${CONJURER_SHARE_SCAN_PATH:=/mnt/shares}"
: "${CONJURER_SHARE_DB:=/srv/share/db/share_scan.json}"
: "${CONJURER_SHARE_DIR:=/var/www/html/share}"
: "${CONJURER_APACHE_LOG:=/var/log/apache2/share_access.log}"
: "${CONJURER_SHARE_SCAN_INTERVAL:=3600}"
: "${CONJURER_SHARE_REVOKE_INTERVAL:=60}"
: "${CONJURER_SHARE_TTL_SECONDS:=3600}"
export CONJURER_SHARE_SCAN_PATH CONJURER_SHARE_DB CONJURER_SHARE_DIR \
CONJURER_APACHE_LOG CONJURER_SHARE_TTL_SECONDS
# Render the vhost (same placeholder pattern as icecast.xml.tpl).
sed "s|__SERVER_NAME__|${CONJURER_SHARE_SERVER_NAME}|g" \
/app/share-vhost.conf.tpl > /etc/apache2/sites-available/000-default.conf
mkdir -p "$CONJURER_SHARE_DIR" \
"$(dirname "$CONJURER_SHARE_DB")" \
"$(dirname "$CONJURER_APACHE_LOG")" \
/var/run/apache2
chown -R www-data:www-data "$CONJURER_SHARE_DIR"
# revoke_shares.py seeks to a stored byte offset, so the log must exist before
# the first pass or it starts by reporting an error.
touch "$CONJURER_APACHE_LOG"
# Seed the index immediately when absent - otherwise /get_share_list returns
# nothing until the first scheduled scan, which looks like a broken feature.
if [ ! -f "$CONJURER_SHARE_DB" ]; then
echo "[share] no index at $CONJURER_SHARE_DB - running initial scan of $CONJURER_SHARE_SCAN_PATH"
python3 /app/scan_shares.py || echo "[share] initial scan failed, continuing"
fi
# Periodic index rebuild.
(
while true; do
sleep "$CONJURER_SHARE_SCAN_INTERVAL"
echo "[share] rescanning $CONJURER_SHARE_SCAN_PATH"
python3 /app/scan_shares.py || echo "[share] scan failed"
done
) &
SCAN_PID=$!
# Periodic revocation of links whose TTL expired after first download.
(
while true; do
sleep "$CONJURER_SHARE_REVOKE_INTERVAL"
python3 /app/revoke_shares.py || echo "[share] revoke failed"
done
) &
REVOKE_PID=$!
trap 'kill "$SCAN_PID" "$REVOKE_PID" 2>/dev/null || true' INT TERM
echo "[share] serving $CONJURER_SHARE_DIR as ${CONJURER_SHARE_SERVER_NAME}/share"
echo "[share] scan every ${CONJURER_SHARE_SCAN_INTERVAL}s, revoke sweep every ${CONJURER_SHARE_REVOKE_INTERVAL}s, TTL ${CONJURER_SHARE_TTL_SECONDS}s after first download"
exec apache2ctl -D FOREGROUND
-14
View File
@@ -15,17 +15,3 @@ CONJURER_MUSIC_FOLDER=/music
# Runtime paths (mounted volume) — playlists/logs the service writes.
CONJURER_MUSICIAN_BASE=/data
CONJURER_LOGSTORE=/data/logs
# --- File sharing (optional; see docs/deployment/FILE_SHARING.md) --------
# The musician SEARCHES the index and CREATES the symlinks; the share container
# builds the index, serves the links and revokes them. Both must therefore point
# at the same two host paths — mount them into this container as well:
# - /srv/share/db:/srv/share/db
# - /srv/share/links:/var/www/html/share
# Leave these unset to keep the feature off (the endpoints then just find
# nothing).
# CONJURER_SHARE_DB=/srv/share/db/share_scan.json
# CONJURER_SHARE_DIR=/var/www/html/share
# Public URL prefix handed back to Discord; host half must match the share
# container's CONJURER_SHARE_SERVER_NAME.
# CONJURER_SHARE_BASE_URL=https://czernobog.pl/share
-49
View File
@@ -1,49 +0,0 @@
# Apache vhost for the Conjurer file-share links.
#
# The entrypoint substitutes __SERVER_NAME__ from CONJURER_SHARE_SERVER_NAME and
# writes the result to /etc/apache2/sites-available/000-default.conf (same
# pattern as docker/icecast.xml.tpl).
#
# What this serves: media_search_functions.publish() creates a symlink named
# after a random 32-hex token inside the share dir, pointing at a file in the
# media library. Apache hands that file out at /share/<token>; revoke_shares.py
# later unlinks it. Nothing is ever copied.
<VirtualHost *:80>
ServerName __SERVER_NAME__
DocumentRoot /var/www/html
# The share links ARE symlinks into the media library, so FollowSymLinks is
# mandatory. SymLinksIfOwnerMatch is deliberately NOT used: the targets are
# owned by whoever owns the media, not by www-data, so it would break every
# link. -Indexes is equally mandatory - a directory listing here would hand
# out every currently live token at once.
<Directory /var/www/html/share>
Options +FollowSymLinks -Indexes
AllowOverride None
Require all granted
# revoke_shares.py keeps its state (.downloads.json, .logpos) in this
# very directory. .downloads.json maps every live token to its download
# time, so serving it would leak the entire active link set.
<FilesMatch "^\.">
Require all denied
</FilesMatch>
</Directory>
# Nothing outside /share is published. The more specific block above wins
# for the share dir itself.
<Directory /var/www/html>
Options -Indexes -FollowSymLinks
AllowOverride None
Require all denied
</Directory>
# Dedicated access log. revoke_shares.py tails exactly this file and matches
# the standard "%r" request line against:
# "\s*GET\s+/share/([0-9a-f]{32})\s+HTTP/
# so the format must stay `combined` (or any format containing %r).
SetEnvIf Request_URI "^/share/" conjurer_share
CustomLog ${APACHE_LOG_DIR}/share_access.log combined env=conjurer_share
ErrorLog ${APACHE_LOG_DIR}/share_error.log
</VirtualHost>
-5
View File
@@ -7,11 +7,6 @@ Runbook for running Conjurer as Docker containers across Proxmox VMs:
| **Main bot** | VM-bot | `conjurer-bot` | 5000 (Flask comm) | `docker/Dockerfile.bot` |
| **Librarian** | VM-librarian | `conjurer-librarian` | 5001 | `docker/Dockerfile.librarian` |
| **Musician** | VM-musician | `conjurer-musician` | 5000 (+ radio) | `docker/Dockerfile.musician` |
| **Share** (optional) | VM-musician or its own | `conjurer-share` | 8081 → 80 | `docker/Dockerfile.share` |
The share service publishes short-lived file links over Apache and is documented
separately in [FILE_SHARING.md](FILE_SHARING.md) — it shares two volumes with the
musician, so set it up after the musician is running.
The three talk to each other over HTTP on the Proxmox LAN. Direction of calls:
-197
View File
@@ -1,197 +0,0 @@
# File sharing (short-lived share links)
Publishes a file from the media library behind a random, unguessable URL that
stops working shortly after someone downloads it. Driven from Discord.
Historically only the Python half of this lived in the repo; the Apache config
and the cron entries were placed on the host by hand. This document plus
`docker/Dockerfile.share` close that gap.
## What it actually does
1. A **scanner** walks the media library and writes a JSON index of every path.
2. From Discord you search that index and pick files.
3. **Publishing** creates a `symlink` named after a random token
(`uuid.uuid4().hex`, 32 hex chars) inside the share directory, pointing at the
real file. Nothing is copied.
4. **Apache** serves that symlink at `https://<host>/share/<token>`.
5. A **revoker** tails Apache's access log, records when each token was *first*
downloaded, and unlinks it once the TTL expires.
Two things worth being precise about, because the names suggest otherwise:
- The token is **random, not a hash** of the file. Nothing about the content is
recoverable from the URL, and the same file shared twice gets two URLs.
- The "database" is a **JSON file**, not a SQL database.
(`utils/import_to_mysql.py` is an unrelated, unfinished scratch script.)
## The pieces
| Path | Role | Runs in |
|---|---|---|
| `conjurer_musician/scan_shares.py` | Builds the JSON index from the media root | share container (or cron) |
| `conjurer_musician/revoke_shares.py` | Tails the access log, unlinks expired tokens | share container (or cron) |
| `conjurer_musician/media_search_functions.py` | Search + publish helpers used by the service | musician |
| `conjurer_musician.py``/get_share_list`, `/get_share_links` | HTTP API, both behind `X-Conjurer-Api-Key` | musician |
| `file_search_functions.py` | Bot-side HTTP client | bot |
| `file_search_commands.py` | Discord cog: search UI + Accept/Cancel | bot |
| `docker/share-vhost.conf.tpl` | Apache vhost (symlinks, no indexes, dedicated log) | share container |
| `conjurer_musician/search_and_share.py` | Standalone CLI doing steps 13 by hand | anywhere (optional) |
Flow:
```
media library ──► scan_shares.py ──► share_scan.json ──┐
│ (reads)
Discord ──► bot cog ──► musician /get_share_list ─────────┘
└─► musician /get_share_links ──► symlink in share dir
│ (serves)
Apache vhost ─────────────────────┘
│ (logs)
└──► share_access.log ──► revoke_shares.py ──► unlink
```
The share directory and the index are **shared state between the musician and
the share service**: the musician creates links and reads the index, the share
service serves links and writes the index. Both must see the same two paths.
## Enabling it — Docker (recommended)
```bash
cp docker/env/share.env.example docker/env/share.env
# edit: CONJURER_SHARE_SERVER_NAME, and the TTL/intervals if you want
docker compose -f docker/compose.share.yaml up -d --build
```
Host layout the compose file expects:
| Host path | Contents |
|---|---|
| `/srv/share/media` | the media library (mounted read-only) |
| `/srv/share/links` | the published symlinks — **also mount into the musician** |
| `/srv/share/db` | `share_scan.json`**also mount into the musician** |
| `/srv/share/logs` | Apache logs, incl. `share_access.log` |
Then point the musician at the same two paths. In `docker/env/musician.env`:
```ini
CONJURER_SHARE_DB=/srv/share/db/share_scan.json
CONJURER_SHARE_DIR=/var/www/html/share
CONJURER_SHARE_BASE_URL=https://czernobog.pl/share
```
and add the matching mounts to `docker/compose.musician.yaml`:
```yaml
- /srv/share/db:/srv/share/db
- /srv/share/links:/var/www/html/share
```
Finally forward `/share` from your public TLS endpoint to the container's port
(`8081` by default). The container speaks plain HTTP on purpose — TLS for
`czernobog.pl` stays on the reverse proxy you already run.
> **The media library must be mounted at the same absolute path the index
> recorded** (`/mnt/shares` by default). The symlinks are absolute, so if the
> scanner saw `/mnt/shares/x.mkv` and Apache has the library somewhere else, every
> link 404s.
The container does not use cron: the entrypoint runs the scanner and the revoker
in sleep loops next to Apache, so their output lands in `docker logs`.
## Enabling it — existing Apache on the host
If you keep serving from the host's Apache instead:
1. Install the vhost. Take `docker/share-vhost.conf.tpl`, replace
`__SERVER_NAME__`, and drop it into `/etc/apache2/sites-available/`, then
`a2ensite` + `systemctl reload apache2`. The three settings that matter are
explained in the Security section below — do not simplify them away.
2. Schedule both jobs. The scripts default to the original bare-metal paths, so
plain cron entries work with no environment at all:
```cron
*/5 * * * * /usr/bin/python3 /opt/conjurer/conjurer_musician/revoke_shares.py
17 * * * * /usr/bin/python3 /opt/conjurer/conjurer_musician/scan_shares.py
```
Defaults: media root `/mnt/shares`, index `/var/log/share_scan.json`, links
`/var/www/html/share`, log `/var/log/apache2/share_access.log`, TTL 3600s.
Override any of them with the `CONJURER_SHARE_*` variables from
`docker/env/share.env.example`.
3. Point the musician at the same paths via `CONJURER_SHARE_DB`,
`CONJURER_SHARE_DIR` and `CONJURER_SHARE_BASE_URL`.
## Using it
```
$tajna_biblioteka_inkwizycji <1-10> <keywords>
```
Restricted to the **Jarl** and **Thane** roles. It replies with the matches, you
select from the dropdown and press Accept; the bot answers with one URL per file.
(The docstring in the cog still says `!findfiles` — that name is stale.)
## Security
The vhost is not boilerplate; three of its settings are load-bearing:
- **`Options +FollowSymLinks`** — the shares *are* symlinks, so without it every
link 403s. `SymLinksIfOwnerMatch` cannot be substituted: the targets are owned
by whoever owns the media, not by `www-data`.
- **`Options -Indexes`** — a directory listing of the share dir would hand out
every currently live token in one request.
- **Deny dotfiles** — `revoke_shares.py` keeps `.downloads.json` (a map of every
live token to its download time) and `.logpos` *inside the served directory*.
Serving `.downloads.json` would leak the entire active link set.
Also worth knowing:
- Both HTTP endpoints call `_authorize_request()`, so they require
`X-Conjurer-Api-Key` whenever `CONJURER_API_KEY` is set. The bot sends it via
`service_headers()`.
- Anyone with the URL can download the file until it expires — the token is the
only credential.
- Mount the media read-only, as the compose file does. This service never needs
to write to the library.
## Verifying
```bash
# index built?
docker compose -f docker/compose.share.yaml exec conjurer-share \
python3 -c "import json;d=json.load(open('/srv/share/db/share_scan.json'));print(len(d['entries']),'entries')"
# a link resolves? (create one from Discord first)
curl -sI https://czernobog.pl/share/<token> | head -1
# the listing must NOT work, and neither must the state file
curl -sI https://czernobog.pl/share/ | head -1 # expect 403
curl -sI https://czernobog.pl/share/.downloads.json | head -1 # expect 403
# revoker seeing downloads?
docker compose -f docker/compose.share.yaml logs --tail=20 conjurer-share
```
Common failures:
| Symptom | Cause |
|---|---|
| Search finds nothing | index missing/empty — check the scanner ran and `CONJURER_SHARE_SCAN_PATH` is right |
| Every link 404s | media not mounted at the path the index recorded |
| Every link 403s | `FollowSymLinks` missing on the share directory |
| Links never expire | revoker not running, or reading a log the vhost isn't writing |
| Bot gets 401 | `CONJURER_API_KEY` set on the musician but not on the bot |
## Known limitations
- **A link nobody downloads is never revoked.** The TTL starts at the first
download, so unused symlinks accumulate in the share directory forever. Prune
it periodically if that matters.
- **Log rotation confuses the revoker.** It stores a byte offset into the access
log; when the file is rotated the offset points into the new, shorter file and
entries can be skipped. Prefer a long rotation interval, or clear
`.logpos` after rotating.
- **The index is a full rescan**, not incremental — cost grows with library size.
+36 -4
View File
@@ -1,7 +1,29 @@
"""Bot-side client for the musician's share-list / share-link endpoints.
This module used to hardcode the musician's LAN address and send no auth
header, which broke the file-share feature in two ways:
* the musician's ``/get_share_list`` and ``/get_share_links`` both call
``_authorize_request()``, so every request 401'd on any deployment that set
``CONJURER_API_KEY`` — which the deployment guides tell you to do; and
* the hardcoded IP ignored ``CONJURER_FILE_SERVICE``, so a musician running on
another host (the Proxmox/docker split) was unreachable.
It now follows the same convention as the other service clients
(``music_functions``): address from ``constants``, shared auth headers, and an
explicit timeout so a wedged musician can't hang the bot.
"""
import requests
# Base URL for Flask service
BASE_URL = 'http://192.168.1.15:5000'
from constants import (
FILE_SERVICE_ADDRESS,
GET_SHARE_LINKS,
GET_SHARE_LIST,
service_headers,
)
SERVICE_HEADERS = service_headers()
REQUEST_TIMEOUT = 60
def find_matches(entries, keywords):
@@ -12,7 +34,12 @@ def find_matches(entries, keywords):
Returns: list of file paths
"""
payload = {'entries': entries, 'keywords': keywords}
response = requests.post(f"{BASE_URL}/get_share_list", json=payload)
response = requests.post(
f"{FILE_SERVICE_ADDRESS}{GET_SHARE_LIST}",
json=payload,
headers=SERVICE_HEADERS,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
data = response.json()
return data.get('files', [])
@@ -25,7 +52,12 @@ def publish(file_paths):
Returns: list of published URLs
"""
payload = {'file_paths': file_paths}
response = requests.post(f"{BASE_URL}/get_share_links", json=payload)
response = requests.post(
f"{FILE_SERVICE_ADDRESS}{GET_SHARE_LINKS}",
json=payload,
headers=SERVICE_HEADERS,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
data = response.json()
return data.get('links', [])