client_core/container/
volumes.rs1use super::types::DockerManager;
2use crate::DuckError;
3use crate::container::path_utils::{PathProcessor, PathUtilsError};
4use anyhow::Result;
5use docker_compose_types as dct;
6use std::path::Path;
7use tracing::{debug, info, warn};
8
9#[derive(Debug, Clone)]
11pub struct MountInfo {
12 pub service_name: String,
13 pub container_path: String,
14 pub host_path: Option<String>,
15 pub is_bind_mount: bool,
16}
17
18impl DockerManager {
19 pub async fn ensure_host_volumes_exist(&self) -> Result<()> {
21 info!("🔍 Checking and creating host mount directories...");
22
23 let compose_config = self.load_compose_config()?;
24 let mount_directories = self.extract_mount_directories(&compose_config)?;
25
26 if mount_directories.is_empty() {
27 info!("✅ No host mount directories need to be created");
28 return Ok(());
29 }
30
31 info!("📁 Found {} mount directories to verify", mount_directories.len());
32
33 for mount_info in mount_directories {
34 if let Some(host_path) = &mount_info.host_path {
35 if mount_info.is_bind_mount {
36 self.create_host_directory_if_not_exists(host_path)?;
37 }
38 }
39 }
40
41 info!("✅ Host mount directory verification completed");
42 Ok(())
43 }
44
45 pub fn extract_mount_directories(&self, compose: &dct::Compose) -> Result<Vec<MountInfo>> {
47 let mut mount_infos = Vec::new();
48
49 for (service_name, service_opt) in &compose.services.0 {
50 if let Some(service) = service_opt {
51 let volumes = &service.volumes;
52 for volume in volumes {
53 if let Some(mount_info) = self.parse_volume_spec(service_name, volume) {
54 mount_infos.push(mount_info);
55 }
56 }
57 }
58 }
59
60 Ok(mount_infos)
61 }
62
63 fn parse_volume_spec(&self, service_name: &str, volume: &dct::Volumes) -> Option<MountInfo> {
65 match volume {
66 dct::Volumes::Simple(volume_str) => {
67 let parts: Vec<&str> = volume_str.split(':').collect();
68
69 match parts.len() {
70 2 | 3 => {
71 let host_path = parts[0];
73 let container_path = parts[1];
74
75 let is_bind = self.is_bind_mount_path(host_path);
76
77 if is_bind {
78 let normalized_host_path = match self.normalize_path(host_path) {
80 Ok(path) => path,
81 Err(e) => {
82 warn!("Path normalization failed: {}", e);
83 return None;
84 }
85 };
86
87 let host_path_buf = std::path::PathBuf::from(&normalized_host_path);
89 let absolute_host_path = if host_path_buf.is_absolute() {
90 normalized_host_path
91 } else {
92 match self.get_working_directory() {
93 Some(compose_dir) => compose_dir
94 .join(&normalized_host_path)
95 .to_string_lossy()
96 .to_string(),
97 None => {
98 return None;
99 }
100 }
101 };
102 Some(MountInfo {
103 service_name: service_name.to_string(),
104 container_path: container_path.to_string(),
105 host_path: Some(absolute_host_path),
106 is_bind_mount: true,
107 })
108 } else {
109 None
110 }
111 }
112 _ => None,
113 }
114 }
115 dct::Volumes::Advanced(volume_def) => {
116 if let Some(source) = &volume_def.source {
118 let is_bind = self.is_bind_mount_path(source);
119
120 if is_bind {
121 let container_path = &volume_def.target;
122 let normalized_source = match self.normalize_path(source) {
124 Ok(path) => path,
125 Err(e) => {
126 warn!("Path normalization failed: {}", e);
127 return None;
128 }
129 };
130
131 let source_path_buf = std::path::PathBuf::from(&normalized_source);
133 let absolute_host_path = if source_path_buf.is_absolute() {
134 normalized_source
135 } else {
136 match self.get_working_directory() {
137 Some(compose_dir) => compose_dir
138 .join(&normalized_source)
139 .to_string_lossy()
140 .to_string(),
141 None => {
142 return None;
143 }
144 }
145 };
146 return Some(MountInfo {
147 service_name: service_name.to_string(),
148 container_path: container_path.to_string(),
149 host_path: Some(absolute_host_path),
150 is_bind_mount: true,
151 });
152 }
153 }
154 None
155 }
156 }
157 }
158
159 fn normalize_path(&self, path: &str) -> Result<String, PathUtilsError> {
161 let path_processor = PathProcessor::new(
162 self.runtime_env.host_os.clone(),
163 self.runtime_env.path_format.clone(),
164 );
165 path_processor.normalize_path(path)
166 }
167
168 fn is_bind_mount_path(&self, path: &str) -> bool {
170 let path_processor = PathProcessor::new(
171 self.runtime_env.host_os.clone(),
172 self.runtime_env.path_format.clone(),
173 );
174 path_processor.is_bind_mount_path(path)
175 }
176
177 fn create_host_directory_if_not_exists(&self, path_str: &str) -> Result<()> {
179 let path = Path::new(path_str);
180
181 if path.exists() {
182 if path.is_dir() {
183 debug!("✅ Directory already exists: {}", path.display());
184 return Ok(());
185 } else {
186 debug!("✅ File already exists: {}", path.display());
187 return Ok(());
188 }
189 }
190
191 let is_likely_file = path.extension().is_some();
193
194 let dir_to_create = if is_likely_file {
195 path.parent()
197 } else {
198 Some(path)
200 };
201
202 if let Some(dir_path) = dir_to_create {
203 match std::fs::create_dir_all(dir_path) {
204 Ok(_) => {
205 info!("📂 Creating directory: {}", dir_path.display());
206 Ok(())
207 }
208 Err(e) => {
209 let error_msg = format!("Failed to create directory {}: {}", dir_path.display(), e);
210 warn!("❌ {}", error_msg);
211 Err(DuckError::Docker(error_msg).into())
212 }
213 }
214 } else {
215 Ok(())
216 }
217 }
218}