Skip to main content

phemios_core/
network_export.rs

1//! Account-authorized original streaming into native-owned private staging.
2use super::*;
3impl Client {
4    /// Explicit bounded original export; binary bodies never cross the webview.
5    pub fn export_original(
6        &self,
7        reference: &str,
8        stamp: &str,
9        expected: u64,
10        path: &std::path::Path,
11        valid: &dyn Fn() -> Result<(), String>,
12        progress: &dyn Fn(u64),
13    ) -> Result<(), String> {
14        use std::io::{Read, Write};
15        if !reference.starts_with("track:")
16            || reference.len() > 4096
17            || stamp.len() > 128
18            || expected == 0
19            || expected > 64 * 1024 * 1024 * 1024
20        {
21            return Err("Original audio invalide ou trop volumineux.".into());
22        }
23        valid()?;
24        self.require_unrestricted_download_admitted(valid)?;
25        let mut response = self
26            .agent
27            .get(format!("{}/api/me/v1/exports/original", self.server))
28            .header("Authorization", format!("Bearer {}", self.token))
29            .header("Accept-Encoding", "identity")
30            .query("ref", reference)
31            .query("stamp", stamp)
32            .config()
33            .timeout_global(Some(std::time::Duration::from_secs(3600)))
34            .timeout_resolve(Some(std::time::Duration::from_secs(15)))
35            .timeout_connect(Some(std::time::Duration::from_secs(15)))
36            // Ureq carries the preceding stage budget forward: SendRequest bounds
37            // header reception, whereas RecvResponse would cap the whole body.
38            .timeout_send_request(Some(std::time::Duration::from_secs(15)))
39            .timeout_recv_response(None)
40            .timeout_recv_body(Some(std::time::Duration::from_secs(15)))
41            .build()
42            .call()
43            .map_err(|_| "Téléchargement de l’original interrompu.")?;
44        if response.status() != ureq::http::StatusCode::OK
45            || response
46                .headers()
47                .get("content-length")
48                .and_then(|v| v.to_str().ok())
49                .and_then(|v| v.parse::<u64>().ok())
50                != Some(expected)
51            || response
52                .headers()
53                .get("content-encoding")
54                .is_some_and(|v| v != "identity")
55        {
56            return Err("Original indisponible, modifié ou export non autorisé. Actualisez Aède et réessayez.".into());
57        }
58        let mut options = std::fs::OpenOptions::new();
59        options.write(true).create_new(true);
60        #[cfg(unix)]
61        {
62            use std::os::unix::fs::OpenOptionsExt;
63            options.mode(0o600);
64        }
65        let mut file = options
66            .open(path)
67            .map_err(|_| "Fichier temporaire indisponible.")?;
68        let mut reader = response.body_mut().as_reader();
69        let mut buffer = vec![0; 64 * 1024];
70        let mut count = 0u64;
71        loop {
72            valid()?;
73            let n = reader
74                .read(&mut buffer)
75                .map_err(|_| "Téléchargement de l’original interrompu.")?;
76            if n == 0 {
77                break;
78            }
79            count = count
80                .checked_add(n as u64)
81                .ok_or("Original trop volumineux.")?;
82            if count > expected {
83                return Err("L’original dépasse sa taille annoncée.".into());
84            }
85            file.write_all(&buffer[..n])
86                .map_err(|_| "Espace temporaire insuffisant ou écriture impossible.")?;
87            progress(count);
88        }
89        if count != expected {
90            return Err("L’original téléchargé est incomplet.".into());
91        }
92        file.sync_all()
93            .map_err(|_| "Écriture temporaire impossible.")?;
94        valid()
95    }
96}
97#[cfg(test)]
98#[path = "network_export_tests.rs"]
99mod tests;