temporary_link feature

This commit is contained in:
Mateusz Gruszczyński
2026-05-21 22:05:08 +02:00
parent cb48735178
commit b772c97d50
10 changed files with 844 additions and 41 deletions

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from ._shared import *
from ..services import torrent_creator
from ..services import pdf_preview_links, torrent_creator
from ..services.reverse_dns import attach_reverse_dns
@bp.get("/torrents")
@@ -105,7 +105,18 @@ def torrent_file_media_info(torrent_hash: str, file_index: int):
return jsonify({"ok": False, "error": "No profile"}), 400
try:
# Note: The route is additive and keeps all existing file endpoints unchanged.
return ok({"media_info": rtorrent.torrent_file_media_info(profile, torrent_hash, file_index)})
media_info = rtorrent.torrent_file_media_info(profile, torrent_hash, file_index)
if media_info.get("kind") == "pdf":
link = pdf_preview_links.create_pdf_preview_link(
torrent_hash,
file_index,
int(profile.get("id") or 0),
int(default_user_id() or 0),
)
# Note: The frontend receives an in-app temporary URL instead of exposing the API download endpoint in the new-tab action.
media_info["preview_url"] = url_for("main.pdf_preview", token=link["token"])
media_info["preview_expires_in"] = link["expires_in"]
return ok({"media_info": media_info})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
@@ -199,6 +210,87 @@ def _send_staged_file(profile: dict, path: str, download_name: str, local: bool
@bp.post("/torrents/<torrent_hash>/files/<int:file_index>/download-link")
def torrent_file_download_link(torrent_hash: str, file_index: int):
profile = preferences.active_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
# Note: The API validates the file selection before returning a short-lived in-app /download URL to the UI.
rtorrent.torrent_download_file_info(profile, torrent_hash, file_index)
link = pdf_preview_links.create_file_download_link(torrent_hash, file_index, int(profile.get("id") or 0), int(default_user_id() or 0))
return ok({"url": url_for("main.temporary_download", token=link["token"]), "expires_in": link["expires_in"]})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
@bp.post("/torrents/<torrent_hash>/files/download-link")
def torrent_file_download_link_from_body(torrent_hash: str):
data = request.get_json(silent=True) or {}
try:
file_index = int(data.get("file_index"))
except Exception:
return jsonify({"ok": False, "error": "file_index is required"}), 400
return torrent_file_download_link(torrent_hash, file_index)
@bp.post("/torrents/<torrent_hash>/files/download.zip/link")
def torrent_files_download_zip_link(torrent_hash: str):
profile = preferences.active_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}
try:
indexes = data.get("indexes") or None
# Note: ZIP link creation validates the requested files through the same service used by the direct download endpoint.
rtorrent.torrent_download_zip_items(profile, torrent_hash, indexes)
link = pdf_preview_links.create_file_zip_download_link(torrent_hash, indexes, int(profile.get("id") or 0), int(default_user_id() or 0))
return ok({"url": url_for("main.temporary_download", token=link["token"]), "expires_in": link["expires_in"]})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
@bp.get("/torrents/<torrent_hash>/torrent-file/link")
def torrent_file_export_link(torrent_hash: str):
profile = preferences.active_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
# Note: Export availability is checked before the UI receives a temporary /download URL.
item = rtorrent.export_torrent_file(profile, torrent_hash)
_cleanup_staged_file(profile, item["path"], bool(item.get("local")))
link = pdf_preview_links.create_torrent_file_download_link(torrent_hash, int(profile.get("id") or 0), int(default_user_id() or 0))
return ok({"url": url_for("main.temporary_download", token=link["token"]), "expires_in": link["expires_in"]})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
@bp.post("/torrents/torrent-files.zip/link")
def torrent_files_export_zip_link():
profile = preferences.active_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}
hashes = [str(h) for h in (data.get("hashes") or []) if str(h).strip()]
if not hashes:
return jsonify({"ok": False, "error": "No torrents selected"}), 400
try:
# Note: Each hash is checked before the temporary ZIP export link is returned to the UI.
staged_paths = []
try:
for h in hashes:
item = rtorrent.export_torrent_file(profile, h)
staged_paths.append((item["path"], bool(item.get("local"))))
finally:
for path, is_local in staged_paths:
_cleanup_staged_file(profile, path, is_local)
link = pdf_preview_links.create_torrent_files_zip_download_link(hashes, int(profile.get("id") or 0), int(default_user_id() or 0))
return ok({"url": url_for("main.temporary_download", token=link["token"]), "expires_in": link["expires_in"]})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
@bp.get("/torrents/<torrent_hash>/files/<int:file_index>/download")
def torrent_file_download(torrent_hash: str, file_index: int):
profile = preferences.active_profile()