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!(
32 "📁 Found {} mount directories to verify",
33 mount_directories.len()
34 );
35
36 for mount_info in mount_directories {
37 if let Some(host_path) = &mount_info.host_path
38 && mount_info.is_bind_mount
39 {
40 self.create_host_directory_if_not_exists(host_path)?;
41 }
42 }
43
44 info!("✅ Host mount directory verification completed");
45 Ok(())
46 }
47
48 pub fn extract_mount_directories(&self, compose: &dct::Compose) -> Result<Vec<MountInfo>> {
50 let mut mount_infos = Vec::new();
51
52 for (service_name, service_opt) in &compose.services.0 {
53 if let Some(service) = service_opt {
54 let volumes = &service.volumes;
55 for volume in volumes {
56 if let Some(mount_info) = self.parse_volume_spec(service_name, volume) {
57 mount_infos.push(mount_info);
58 }
59 }
60 }
61 }
62
63 Ok(mount_infos)
64 }
65
66 fn parse_volume_spec(&self, service_name: &str, volume: &dct::Volumes) -> Option<MountInfo> {
68 match volume {
69 dct::Volumes::Simple(volume_str) => {
70 let parts: Vec<&str> = volume_str.split(':').collect();
71
72 match parts.len() {
73 2 | 3 => {
74 let host_path = parts[0];
76 let container_path = parts[1];
77
78 let is_bind = self.is_bind_mount_path(host_path);
79
80 if is_bind {
81 let normalized_host_path = match self.normalize_path(host_path) {
83 Ok(path) => path,
84 Err(e) => {
85 warn!("Path normalization failed: {}", e);
86 return None;
87 }
88 };
89
90 let host_path_buf = std::path::PathBuf::from(&normalized_host_path);
92 let absolute_host_path = if host_path_buf.is_absolute() {
93 normalized_host_path
94 } else {
95 match self.get_working_directory() {
96 Some(compose_dir) => compose_dir
97 .join(&normalized_host_path)
98 .to_string_lossy()
99 .to_string(),
100 None => {
101 return None;
102 }
103 }
104 };
105 Some(MountInfo {
106 service_name: service_name.to_string(),
107 container_path: container_path.to_string(),
108 host_path: Some(absolute_host_path),
109 is_bind_mount: true,
110 })
111 } else {
112 None
113 }
114 }
115 _ => None,
116 }
117 }
118 dct::Volumes::Advanced(volume_def) => {
119 if let Some(source) = &volume_def.source {
121 let is_bind = self.is_bind_mount_path(source);
122
123 if is_bind {
124 let container_path = &volume_def.target;
125 let normalized_source = match self.normalize_path(source) {
127 Ok(path) => path,
128 Err(e) => {
129 warn!("Path normalization failed: {}", e);
130 return None;
131 }
132 };
133
134 let source_path_buf = std::path::PathBuf::from(&normalized_source);
136 let absolute_host_path = if source_path_buf.is_absolute() {
137 normalized_source
138 } else {
139 match self.get_working_directory() {
140 Some(compose_dir) => compose_dir
141 .join(&normalized_source)
142 .to_string_lossy()
143 .to_string(),
144 None => {
145 return None;
146 }
147 }
148 };
149 return Some(MountInfo {
150 service_name: service_name.to_string(),
151 container_path: container_path.to_string(),
152 host_path: Some(absolute_host_path),
153 is_bind_mount: true,
154 });
155 }
156 }
157 None
158 }
159 }
160 }
161
162 fn normalize_path(&self, path: &str) -> Result<String, PathUtilsError> {
164 let path_processor = PathProcessor::new(
165 self.runtime_env.host_os.clone(),
166 self.runtime_env.path_format.clone(),
167 );
168 path_processor.normalize_path(path)
169 }
170
171 fn is_bind_mount_path(&self, path: &str) -> bool {
173 let path_processor = PathProcessor::new(
174 self.runtime_env.host_os.clone(),
175 self.runtime_env.path_format.clone(),
176 );
177 path_processor.is_bind_mount_path(path)
178 }
179
180 fn create_host_directory_if_not_exists(&self, path_str: &str) -> Result<()> {
182 let path = Path::new(path_str);
183
184 if path.exists() {
185 if path.is_dir() {
186 debug!("✅ Directory already exists: {}", path.display());
187 return Ok(());
188 } else {
189 debug!("✅ File already exists: {}", path.display());
190 return Ok(());
191 }
192 }
193
194 let is_likely_file = path.extension().is_some();
196
197 let dir_to_create = if is_likely_file {
198 path.parent()
200 } else {
201 Some(path)
203 };
204
205 if let Some(dir_path) = dir_to_create {
206 match std::fs::create_dir_all(dir_path) {
207 Ok(_) => {
208 info!("📂 Creating directory: {}", dir_path.display());
209 Ok(())
210 }
211 Err(e) => {
212 let error_msg =
213 format!("Failed to create directory {}: {}", dir_path.display(), e);
214 warn!("❌ {}", error_msg);
215 Err(DuckError::Docker(error_msg).into())
216 }
217 }
218 } else {
219 Ok(())
220 }
221 }
222}