diff --git a/pytorrent/services/rtorrent/files.py b/pytorrent/services/rtorrent/files.py index f9ac10f..e483c32 100644 --- a/pytorrent/services/rtorrent/files.py +++ b/pytorrent/services/rtorrent/files.py @@ -541,17 +541,31 @@ def torrent_download_zip_items(profile: dict, torrent_hash: str, indexes: list[i return items +def _remote_file_exists(c: ScgiRtorrentClient, source_path: str) -> bool: + # Note: Export fallback checks candidate .torrent files on the rTorrent host before staging, avoiding stale tied-file paths. + clean = _remote_clean_path(source_path) + if not clean: + return False + script = 'p=$1; [ -f "$p" ] && [ -r "$p" ] && printf OK || true' + try: + return str(_rt_execute(c, "execute.capture", "sh", "-c", script, "pytorrent-file-exists", clean) or "").strip() == "OK" + except Exception: + return False + + def _remote_stage_path(c: ScgiRtorrentClient, source_path: str, suffix: str = "") -> str: token = uuid.uuid4().hex safe_suffix = ''.join(ch if ch.isalnum() or ch in '.-_' else '_' for ch in str(suffix or ''))[:80] target = f"{download_tmp_dir().rstrip('/')}/pytorrent-download-{token}{safe_suffix}" script = ( 'src=$1; dst=$2; ' - 'if [ ! -f "$src" ]; then echo "ERR\tmissing source"; exit 0; fi; ' + 'if [ ! -f "$src" ]; then printf "ERR\tmissing source: %s\n" "$src"; exit 0; fi; ' + 'if [ ! -r "$src" ]; then printf "ERR\tsource is not readable: %s\n" "$src"; exit 0; fi; ' 'cp -- "$src" "$dst" 2>/tmp/pytorrent-cp-err-$$ || { rc=$?; err=$(cat /tmp/pytorrent-cp-err-$$ 2>/dev/null); rm -f /tmp/pytorrent-cp-err-$$; printf "ERR\t%s\t%s\n" "$rc" "$err"; exit 0; }; ' 'rm -f /tmp/pytorrent-cp-err-$$; chmod 0644 "$dst" 2>/dev/null || true; printf "OK\t%s\n" "$dst"' ) - output = str(_rt_execute(c, "execute.capture", "sh", "-c", script, "pytorrent-stage-file", source_path, target) or "").strip() + clean_source = _remote_clean_path(source_path) + output = str(_rt_execute(c, "execute.capture", "sh", "-c", script, "pytorrent-stage-file", clean_source, target) or "").strip() parts = (output.splitlines()[0] if output else "").split("\t", 2) if len(parts) >= 2 and parts[0] == "OK": return parts[1] @@ -643,14 +657,48 @@ def _torrent_raw_from_method(c: ScgiRtorrentClient, torrent_hash: str) -> bytes return None -def _torrent_source_file(c: ScgiRtorrentClient, torrent_hash: str) -> str: +def _rtorrent_session_path(c: ScgiRtorrentClient) -> str: + for method in ("session.path", "get_session"): + try: + value = str(c.call(method) or "").strip() + except Exception: + continue + if value: + return _remote_clean_path(value) + return "" + + +def _torrent_source_file_candidates(c: ScgiRtorrentClient, torrent_hash: str) -> list[str]: + # Note: rTorrent may keep stale watch/tied paths; session candidates preserve .torrent export when the original source was moved. + candidates: list[str] = [] for method in ("d.tied_to_file", "d.get_tied_to_file", "d.loaded_file", "d.get_loaded_file", "d.session_file", "d.get_session_file"): try: value = str(c.call(method, torrent_hash) or "").strip() except Exception: continue if value: - return value + candidates.append(value) + session_path = _rtorrent_session_path(c) + hash_values = [] + clean_hash = str(torrent_hash or "").strip() + if clean_hash: + hash_values.extend([clean_hash, clean_hash.upper(), clean_hash.lower()]) + for h in dict.fromkeys(hash_values): + if session_path: + candidates.append(_remote_join(session_path, f"{h}.torrent")) + candidates.append(f"/tmp/{h}.torrent") + result = [] + for item in candidates: + clean = _remote_clean_path(item) + if clean and clean not in result: + result.append(clean) + return result + + +def _torrent_source_file(c: ScgiRtorrentClient, torrent_hash: str) -> str: + for source in _torrent_source_file_candidates(c, torrent_hash): + if _remote_file_exists(c, source): + return source return ""