Skip to main content

Mountain/Environment/FileSystemProvider/
WriteOperations.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::PathSecurity::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::PathSecurity::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::PathSecurity::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::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, source)?;
145
146	Utility::PathSecurity::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::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, source)?;
165
166	Utility::PathSecurity::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 ReadOperations module
174	let source_metadata = super::ReadOperations::stat_file_impl(env, source).await?;
175
176	let SourceIsDir = (source_metadata.FileType & FileTypeDTO::Directory as u8) != 0;
177
178	// Prevent copying file/dir to itself (which would truncate or
179	// recursively explode).
180	if fs::canonicalize(source).await.ok().as_ref() == fs::canonicalize(target).await.ok().as_ref() {
181		return Err(CommonError::InvalidArgument {
182			ArgumentName:"Target".to_string(),
183			Reason:"Cannot copy file to itself".to_string(),
184		});
185	}
186
187	if !overwrite && fs::try_exists(target).await.unwrap_or(false) {
188		return Err(CommonError::FileSystemFileExists(target.clone()));
189	}
190
191	// Create target parent directory if needed (works for both file
192	// and directory copies; the directory copy below also creates
193	// the target itself).
194	if let Some(target_parent) = target.parent() {
195		if !fs::try_exists(target_parent).await.unwrap_or(false) {
196			fs::create_dir_all(target_parent).await.map_err(|error| {
197				CommonError::FromStandardIOError(error, target_parent.to_path_buf(), "Copy.CreateTargetParent")
198			})?;
199		}
200	}
201
202	if SourceIsDir {
203		// Recursive directory copy. Walks the source tree iteratively
204		// (avoids deep async recursion blowing the stack on
205		// pathological depths) and re-creates each entry under the
206		// target. Symlinks are followed to keep behaviour consistent
207		// with VS Code's `IFileService.copy` - if you want preserve-
208		// symlinks semantics, use `clone_native` instead which does a
209		// COW reflink on supported filesystems.
210		return copy_directory_recursive(source, target, overwrite).await;
211	}
212
213	fs::copy(source, target)
214		.await
215		.map(|_| ())
216		.map_err(|error| CommonError::FromStandardIOError(error, source.clone(), "Copy"))
217}
218
219/// Recursively copy a directory tree from `source` into `target`.
220/// Iterative (uses an explicit stack of `(SrcDir, DstDir)`) so it
221/// can't blow the Tokio task stack on very deep trees. Files inside
222/// re-use `tokio::fs::copy` for fast path; directories are created
223/// with `create_dir`. Symlinks are dereferenced.
224async fn copy_directory_recursive(source:&PathBuf, target:&PathBuf, overwrite:bool) -> Result<(), CommonError> {
225	// Pre-create the top-level target dir.
226	if !fs::try_exists(target).await.unwrap_or(false) {
227		fs::create_dir(target)
228			.await
229			.map_err(|error| CommonError::FromStandardIOError(error, target.clone(), "Copy.CreateTargetRoot"))?;
230	}
231
232	let mut Stack:Vec<(PathBuf, PathBuf)> = vec![(source.clone(), target.clone())];
233	while let Some((SrcDir, DstDir)) = Stack.pop() {
234		let mut Entries = fs::read_dir(&SrcDir)
235			.await
236			.map_err(|error| CommonError::FromStandardIOError(error, SrcDir.clone(), "Copy.ReadDir"))?;
237		while let Some(Entry) = Entries
238			.next_entry()
239			.await
240			.map_err(|error| CommonError::FromStandardIOError(error, SrcDir.clone(), "Copy.NextEntry"))?
241		{
242			let Name = Entry.file_name();
243			let SrcPath = SrcDir.join(&Name);
244			let DstPath = DstDir.join(&Name);
245			let FileType = Entry
246				.file_type()
247				.await
248				.map_err(|error| CommonError::FromStandardIOError(error, SrcPath.clone(), "Copy.FileType"))?;
249
250			if FileType.is_dir() {
251				if !fs::try_exists(&DstPath).await.unwrap_or(false) {
252					fs::create_dir(&DstPath).await.map_err(|error| {
253						CommonError::FromStandardIOError(error, DstPath.clone(), "Copy.CreateSubDir")
254					})?;
255				}
256				Stack.push((SrcPath, DstPath));
257			} else {
258				if !overwrite && fs::try_exists(&DstPath).await.unwrap_or(false) {
259					return Err(CommonError::FileSystemFileExists(DstPath));
260				}
261				fs::copy(&SrcPath, &DstPath)
262					.await
263					.map_err(|error| CommonError::FromStandardIOError(error, SrcPath.clone(), "Copy.CopyFile"))?;
264			}
265		}
266	}
267	Ok(())
268}
269
270/// CreateFile operations implementation for MountainEnvironment
271pub(super) async fn create_file_impl(env:&MountainEnvironment, path:&PathBuf) -> Result<(), CommonError> {
272	// Use WriteFile with an empty Vec, ensuring creation without overwrite.
273	// This ensures proper parent directory creation and path validation.
274	write_file_impl(env, path, vec![], true, false).await
275}