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