use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result, bail};
use suno_core::{Clip, Http, HttpRequest};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub async fn get_bytes(http: &impl Http, url: &str) -> Result<Vec<u8>> {
let response = http
.send(HttpRequest::get(url))
.await
.map_err(|err| anyhow::anyhow!("request failed: {err}"))?;
if !(200..=299).contains(&response.status) {
bail!("download failed for {url}: status {}", response.status);
}
Ok(response.body)
}
pub async fn cover(http: &impl Http, clip: &Clip) -> Option<Vec<u8>> {
let url = clip.selected_image_url()?;
get_bytes(http, url).await.ok()
}
pub fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
let tmp = temp_sibling(path);
let _scratch = Scratch(tmp.clone());
std::fs::write(&tmp, bytes).with_context(|| format!("could not write {}", tmp.display()))?;
replace(&tmp, path).with_context(|| format!("could not finalise {}", path.display()))?;
Ok(())
}
pub(crate) fn replace(from: &Path, to: &Path) -> std::io::Result<()> {
match std::fs::rename(from, to) {
Ok(()) => Ok(()),
Err(_) if to.exists() => {
let backup = to.with_file_name(format!(
".{}.{}.bak",
to.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default(),
unique_stamp()
));
std::fs::rename(to, &backup)?;
match std::fs::rename(from, to) {
Ok(()) => {
let _ = std::fs::remove_file(&backup);
Ok(())
}
Err(err) => {
let _ = std::fs::rename(&backup, to);
Err(err)
}
}
}
Err(err) => Err(err),
}
}
fn temp_sibling(path: &Path) -> PathBuf {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "download".to_owned());
path.with_file_name(format!(".{name}.{}.part", unique_stamp()))
}
fn unique_stamp() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let seq = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{}-{nanos}-{seq}", std::process::id())
}
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_atomic_replaces_and_leaves_no_temp() {
let dir = Path::new("target").join(format!("write-atomic-{}", unique_stamp()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("clip.bin");
write_atomic(&path, b"first").unwrap();
write_atomic(&path, b"second").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"second");
let names: Vec<String> = std::fs::read_dir(&dir)
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(names, vec!["clip.bin".to_owned()]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn replace_overwrites_existing_and_leaves_no_backup() {
let dir = Path::new("target").join(format!("replace-{}", unique_stamp()));
std::fs::create_dir_all(&dir).unwrap();
let to = dir.join("dest.bin");
let from = dir.join("src.bin");
std::fs::write(&to, b"old").unwrap();
std::fs::write(&from, b"new").unwrap();
replace(&from, &to).unwrap();
assert_eq!(std::fs::read(&to).unwrap(), b"new");
let names: Vec<String> = std::fs::read_dir(&dir)
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(names, vec!["dest.bin".to_owned()]);
let _ = std::fs::remove_dir_all(&dir);
}
}