Mountain/Environment/FileSystemProvider/
write_operations.rs

1//! # FileSystemProvider - Write Operations
2//!
3//! Implementation of
4//! [`FileSystemWriter`](CommonLibrary::FileSystem::FileSystemWriter) for
5//! [`MountainEnvironment`]
6//!
7//! Provides secure, validated filesystem write access with workspace trust
8//! enforcement.
9
10use std::path::PathBuf;
11
12use CommonLibrary::{Error::CommonError::CommonError, FileSystem::DTO::FileTypeDTO::FileTypeDTO};
13use tokio::fs;
14
15use super::super::{MountainEnvironment::MountainEnvironment, Utility};
16
17/// Write operations implementation for MountainEnvironment
18pub(super) async fn write_file_impl(
19	env:&MountainEnvironment,
20	path:&PathBuf,
21	content:Vec<u8>,
22	create:bool,
23	overwrite:bool,
24) -> Result<(), CommonError> {
25	Utility::IsPathAllowedForAccess(&env.ApplicationState, path)?;
26
27	// Validate that Content is not excessively large to prevent memory issues
28	if content.len() > 1024 * 1024 * 1024 {
29		// 1 GB limit
30		return Err(CommonError::InvalidArgument {
31			ArgumentName:"Content".to_string(),
32			Reason:"Content exceeds maximum size limit of 1GB".to_string(),
33		});
34	}
35
36	let path_exists = fs::try_exists(path).await.unwrap_or(false);
37
38	if path_exists && !overwrite {
39		return Err(CommonError::FileSystemFileExists(path.clone()));
40	}
41
42	if !path_exists && !create {
43		return Err(CommonError::FileSystemNotFound(path.clone()));
44	}
45
46	// Create parent directories if they don't exist
47	if let Some(parent_directory) = path.parent() {
48		if !fs::try_exists(parent_directory).await.unwrap_or(false) {
49			fs::create_dir_all(parent_directory).await.map_err(|error| {
50				CommonError::FromStandardIOError(error, parent_directory.to_path_buf(), "WriteFile.CreateParent")
51			})?;
52		}
53	}
54
55	fs::write(path, &content)
56		.await
57		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "WriteFile"))?;
58
59	// Implement atomic write pattern to prevent partial writes and data corruption
60	// on crashes or interrupts. The current implementation writes directly to the
61	// target file, which can leave corrupted files if the operation is interrupted.
62	// A robust implementation: 1) writes content to a temporary file in the same
63	// directory (ensuring same filesystem for atomic rename), 2) flushes and syncs
64	// the temporary file to disk (fsync), 3) atomically renames the temporary file
65	// to the target path using fs::rename (POSIX rename is atomic within a
66	// filesystem), 4) deletes old file if replacing, or handles temp cleanup on
67	// failure. This pattern ensures the target file is either fully written or
68	// unchanged.
69	Ok(())
70}
71
72/// CreateDirectory operations implementation for MountainEnvironment
73pub(super) async fn create_directory_impl(
74	env:&MountainEnvironment,
75	path:&PathBuf,
76	recursive:bool,
77) -> Result<(), CommonError> {
78	Utility::IsPathAllowedForAccess(&env.ApplicationState, path)?;
79
80	// Validate that parent path doesn't point to a file
81	if let Some(parent_path) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
82		if fs::try_exists(parent_path).await.unwrap_or(false) {
83			let parent_metadata = fs::metadata(parent_path).await.map_err(|error| {
84				CommonError::FromStandardIOError(error, parent_path.to_path_buf(), "CreateDirectory.ParentStat")
85			})?;
86
87			if parent_metadata.is_file() {
88				return Err(CommonError::InvalidArgument {
89					ArgumentName:"Path".to_string(),
90					Reason:format!("Cannot create directory: parent path is a file: {}", parent_path.display()),
91				});
92			}
93		}
94	}
95
96	let operation = if recursive {
97		fs::create_dir_all(path).await
98	} else {
99		fs::create_dir(path).await
100	};
101
102	operation.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "CreateDirectory"))
103}
104
105/// Delete operations implementation for MountainEnvironment
106pub(super) async fn delete_impl(
107	env:&MountainEnvironment,
108	path:&PathBuf,
109	recursive:bool,
110	_use_trash:bool,
111) -> Result<(), CommonError> {
112	Utility::IsPathAllowedForAccess(&env.ApplicationState, path)?;
113
114	// A full implementation would use the `trash` crate if `UseTrash` is true.
115	match fs::metadata(path).await {
116		Ok(metadata) => {
117			let operation = if metadata.is_dir() {
118				if recursive {
119					fs::remove_dir_all(path).await
120				} else {
121					fs::remove_dir(path).await
122				}
123			} else {
124				fs::remove_file(path).await
125			};
126
127			operation.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "Delete"))
128		},
129
130		// Idempotent success
131		Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
132
133		Err(error) => Err(CommonError::FromStandardIOError(error, path.clone(), "Delete.Stat")),
134	}
135}
136
137/// Rename operations implementation for MountainEnvironment
138pub(super) async fn rename_impl(
139	env:&MountainEnvironment,
140	source:&PathBuf,
141	target:&PathBuf,
142	overwrite:bool,
143) -> Result<(), CommonError> {
144	Utility::IsPathAllowedForAccess(&env.ApplicationState, source)?;
145
146	Utility::IsPathAllowedForAccess(&env.ApplicationState, target)?;
147
148	if !overwrite && fs::try_exists(target).await.unwrap_or(false) {
149		return Err(CommonError::FileSystemFileExists(target.clone()));
150	}
151
152	fs::rename(source, target)
153		.await
154		.map_err(|error| CommonError::FromStandardIOError(error, source.clone(), "Rename"))
155}
156
157/// Copy operations implementation for MountainEnvironment
158pub(super) async fn copy_impl(
159	env:&MountainEnvironment,
160	source:&PathBuf,
161	target:&PathBuf,
162	overwrite:bool,
163) -> Result<(), CommonError> {
164	Utility::IsPathAllowedForAccess(&env.ApplicationState, source)?;
165
166	Utility::IsPathAllowedForAccess(&env.ApplicationState, target)?;
167
168	// Validate that source exists
169	if !fs::try_exists(source).await.unwrap_or(false) {
170		return Err(CommonError::FileSystemNotFound(source.clone()));
171	}
172
173	// Call stat_file_impl from the read_operations module
174	let source_metadata = super::read_operations::stat_file_impl(env, source).await?;
175
176	if (source_metadata.FileType & FileTypeDTO::Directory as u8) != 0 {
177		return Err(CommonError::NotImplemented { FeatureName:"Recursive directory copy".to_string() });
178	}
179
180	// Prevent copying file to itself (which would truncate it)
181	if fs::canonicalize(source).await.ok().as_ref() == fs::canonicalize(target).await.ok().as_ref() {
182		return Err(CommonError::InvalidArgument {
183			ArgumentName:"Target".to_string(),
184			Reason:"Cannot copy file to itself".to_string(),
185		});
186	}
187
188	if !overwrite && fs::try_exists(target).await.unwrap_or(false) {
189		return Err(CommonError::FileSystemFileExists(target.clone()));
190	}
191
192	// Create target parent directory if needed
193	if let Some(target_parent) = target.parent() {
194		if !fs::try_exists(target_parent).await.unwrap_or(false) {
195			fs::create_dir_all(target_parent).await.map_err(|error| {
196				CommonError::FromStandardIOError(error, target_parent.to_path_buf(), "Copy.CreateTargetParent")
197			})?;
198		}
199	}
200
201	fs::copy(source, target)
202		.await
203		.map(|_| ())
204		.map_err(|error| CommonError::FromStandardIOError(error, source.clone(), "Copy"))
205}
206
207/// CreateFile operations implementation for MountainEnvironment
208pub(super) async fn create_file_impl(env:&MountainEnvironment, path:&PathBuf) -> Result<(), CommonError> {
209	// Use WriteFile with an empty Vec, ensuring creation without overwrite.
210	// This ensures proper parent directory creation and path validation.
211	write_file_impl(env, path, vec![], true, false).await
212}