viewpoint_core/page/video_io/
mod.rs

1//! Video file I/O operations.
2//!
3//! This module handles saving, copying, and deleting video files.
4
5use std::path::{Path, PathBuf};
6
7use tracing::info;
8
9use crate::error::PageError;
10
11use super::video::Video;
12
13impl Video {
14    /// Save the video to a specific path.
15    ///
16    /// This copies the recorded video to the specified location.
17    ///
18    /// # Example
19    ///
20    /// ```no_run
21    /// use viewpoint_core::page::Video;
22    ///
23    /// # async fn example(video: &Video) -> Result<(), viewpoint_core::CoreError> {
24    /// video.save_as("./test-results/my-test.webm").await?;
25    /// # Ok(())
26    /// # }
27    /// ```
28    pub async fn save_as(&self, path: impl AsRef<Path>) -> Result<(), PageError> {
29        let current_path = self.path().await?;
30        let target_path = path.as_ref();
31
32        // Ensure parent directory exists
33        if let Some(parent) = target_path.parent() {
34            tokio::fs::create_dir_all(parent)
35                .await
36                .map_err(|e| PageError::EvaluationFailed(format!("Failed to create directory: {e}")))?;
37        }
38
39        // Copy the video file
40        tokio::fs::copy(&current_path, target_path)
41            .await
42            .map_err(|e| PageError::EvaluationFailed(format!("Failed to copy video: {e}")))?;
43
44        // Also copy the frames directory if it exists (for jpeg-sequence format)
45        let state = self.state.read().await;
46        if let Some(ref video_path) = state.video_path {
47            // Read the metadata to find frames dir
48            if let Ok(content) = tokio::fs::read_to_string(video_path).await {
49                if let Ok(metadata) = serde_json::from_str::<serde_json::Value>(&content) {
50                    if let Some(frames_dir) = metadata.get("frames_dir").and_then(|v| v.as_str()) {
51                        let source_frames = PathBuf::from(frames_dir);
52                        if source_frames.exists() {
53                            let target_frames = target_path.with_extension("frames");
54                            copy_dir_recursive(&source_frames, &target_frames).await?;
55                        }
56                    }
57                }
58            }
59        }
60
61        info!("Video saved to {:?}", target_path);
62        Ok(())
63    }
64
65    /// Delete the recorded video.
66    ///
67    /// This removes the video file and any associated frame data.
68    ///
69    /// # Example
70    ///
71    /// ```no_run
72    /// use viewpoint_core::page::Video;
73    ///
74    /// # async fn example(video: &Video) -> Result<(), viewpoint_core::CoreError> {
75    /// video.delete().await?;
76    /// # Ok(())
77    /// # }
78    /// ```
79    pub async fn delete(&self) -> Result<(), PageError> {
80        let state = self.state.read().await;
81
82        if let Some(ref video_path) = state.video_path {
83            // Read the metadata to find frames dir
84            if let Ok(content) = tokio::fs::read_to_string(video_path).await {
85                if let Ok(metadata) = serde_json::from_str::<serde_json::Value>(&content) {
86                    if let Some(frames_dir) = metadata.get("frames_dir").and_then(|v| v.as_str()) {
87                        let frames_path = PathBuf::from(frames_dir);
88                        if frames_path.exists() {
89                            let _ = tokio::fs::remove_dir_all(&frames_path).await;
90                        }
91                    }
92                }
93            }
94
95            // Remove the video file
96            tokio::fs::remove_file(video_path)
97                .await
98                .map_err(|e| PageError::EvaluationFailed(format!("Failed to delete video: {e}")))?;
99
100            info!("Video deleted");
101        }
102
103        Ok(())
104    }
105}
106
107/// Recursively copy a directory.
108pub(super) async fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), PageError> {
109    tokio::fs::create_dir_all(dst)
110        .await
111        .map_err(|e| PageError::EvaluationFailed(format!("Failed to create directory: {e}")))?;
112
113    let mut entries = tokio::fs::read_dir(src)
114        .await
115        .map_err(|e| PageError::EvaluationFailed(format!("Failed to read directory: {e}")))?;
116
117    while let Some(entry) = entries.next_entry().await.map_err(|e| {
118        PageError::EvaluationFailed(format!("Failed to read directory entry: {e}"))
119    })? {
120        let src_path = entry.path();
121        let dst_path = dst.join(entry.file_name());
122
123        if src_path.is_dir() {
124            Box::pin(copy_dir_recursive(&src_path, &dst_path)).await?;
125        } else {
126            tokio::fs::copy(&src_path, &dst_path)
127                .await
128                .map_err(|e| PageError::EvaluationFailed(format!("Failed to copy file: {e}")))?;
129        }
130    }
131
132    Ok(())
133}