Skip to main content

Mountain/Environment/FileSystemProvider/
WriteOperations.rs

1//! # FileSystemProvider - Write Operations
2//!
3//! Implements [`FileSystemWriter`](CommonLibrary::FileSystem::FileSystemWriter)
4//! for [`MountainEnvironment`]. Every function calls
5//! `Utility::PathSecurity::IsPathAllowedForAccess` before any I/O.
6//!
7//! ## Functions
8//!
9//! - `write_file_impl` - writes bytes to a path. Enforces a 1 GB content guard,
10//!   `create` / `overwrite` flag semantics (mirroring
11//!   `vscode.workspace.fs.writeFile`), and auto-creates missing parent
12//!   directories. Note: currently writes directly; the inline comment documents
13//!   the planned atomic-rename pattern (`write-to-temp → fsync → rename`).
14//! - `create_directory_impl` - creates a directory, optionally recursively.
15//!   Validates that the parent is not a regular file.
16//! - `delete_impl` - removes a file or directory. `recursive` controls
17//!   `remove_dir_all` vs `remove_dir`. `_use_trash` is stubbed; the `trash`
18//!   crate integration is planned. Idempotent: `NotFound` is treated as
19//!   success.
20//! - `rename_impl` - calls `tokio::fs::rename` (POSIX-atomic within a
21//!   filesystem). Both source and target are path-security checked.
22//! - `copy_impl` - copies a file or directory tree. Directories use the private
23//!   `copy_directory_recursive` helper, which walks an explicit stack to avoid
24//!   deep async-recursion stack overflows.
25//! - `create_file_impl` - thin wrapper over `write_file_impl` with empty
26//!   content, `create=true`, `overwrite=false`.
27
28use std::path::PathBuf;
29
30use CommonLibrary::{Error::CommonError::CommonError, FileSystem::DTO::FileTypeDTO::FileTypeDTO};
31use tokio::fs;
32
33use super::super::{MountainEnvironment::MountainEnvironment, Utility};
34
35/// Write operations implementation for MountainEnvironment
36pub(super) async fn write_file_impl(
37	env:&MountainEnvironment,
38
39	path:&PathBuf,
40
41	content:Vec<u8>,
42
43	create:bool,
44
45	overwrite:bool,
46) -> Result<(), CommonError> {
47	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, path)?;
48
49	// Validate that Content is not excessively large to prevent memory issues
50	if content.len() > 1024 * 1024 * 1024 {
51		// 1 GB limit
52		return Err(CommonError::InvalidArgument {
53			ArgumentName:"Content".to_string(),
54			Reason:"Content exceeds maximum size limit of 1GB".to_string(),
55		});
56	}
57
58	let path_exists = fs::try_exists(path).await.unwrap_or(false);
59
60	if path_exists && !overwrite {
61		return Err(CommonError::FileSystemFileExists(path.clone()));
62	}
63
64	if !path_exists && !create {
65		return Err(CommonError::FileSystemNotFound(path.clone()));
66	}
67
68	// Create parent directories if they don't exist
69	if let Some(parent_directory) = path.parent() {
70		if !fs::try_exists(parent_directory).await.unwrap_or(false) {
71			fs::create_dir_all(parent_directory).await.map_err(|error| {
72				CommonError::FromStandardIOError(error, parent_directory.to_path_buf(), "WriteFile.CreateParent")
73			})?;
74		}
75	}
76
77	fs::write(path, &content)
78		.await
79		.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "WriteFile"))?;
80
81	// Implement atomic write pattern to prevent partial writes and data corruption
82	// on crashes or interrupts. The current implementation writes directly to the
83	// target file, which can leave corrupted files if the operation is interrupted.
84	// A robust implementation: 1) writes content to a temporary file in the same
85	// directory (ensuring same filesystem for atomic rename), 2) flushes and syncs
86	// the temporary file to disk (fsync), 3) atomically renames the temporary file
87	// to the target path using fs::rename (POSIX rename is atomic within a
88	// filesystem), 4) deletes old file if replacing, or handles temp cleanup on
89	// failure. This pattern ensures the target file is either fully written or
90	// unchanged.
91	Ok(())
92}
93
94/// CreateDirectory operations implementation for MountainEnvironment
95pub(super) async fn create_directory_impl(
96	env:&MountainEnvironment,
97
98	path:&PathBuf,
99
100	recursive:bool,
101) -> Result<(), CommonError> {
102	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, path)?;
103
104	// Validate that parent path doesn't point to a file
105	if let Some(parent_path) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
106		if fs::try_exists(parent_path).await.unwrap_or(false) {
107			let parent_metadata = fs::metadata(parent_path).await.map_err(|error| {
108				CommonError::FromStandardIOError(error, parent_path.to_path_buf(), "CreateDirectory.ParentStat")
109			})?;
110
111			if parent_metadata.is_file() {
112				return Err(CommonError::InvalidArgument {
113					ArgumentName:"Path".to_string(),
114					Reason:format!("Cannot create directory: parent path is a file: {}", parent_path.display()),
115				});
116			}
117		}
118	}
119
120	let operation = if recursive {
121		fs::create_dir_all(path).await
122	} else {
123		fs::create_dir(path).await
124	};
125
126	operation.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "CreateDirectory"))
127}
128
129/// Delete operations implementation for MountainEnvironment
130pub(super) async fn delete_impl(
131	env:&MountainEnvironment,
132
133	path:&PathBuf,
134
135	recursive:bool,
136
137	_use_trash:bool,
138) -> Result<(), CommonError> {
139	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, path)?;
140
141	// A full implementation would use the `trash` crate if `UseTrash` is true.
142	match fs::metadata(path).await {
143		Ok(metadata) => {
144			let operation = if metadata.is_dir() {
145				if recursive {
146					fs::remove_dir_all(path).await
147				} else {
148					fs::remove_dir(path).await
149				}
150			} else {
151				fs::remove_file(path).await
152			};
153
154			operation.map_err(|error| CommonError::FromStandardIOError(error, path.clone(), "Delete"))
155		},
156
157		// Idempotent success
158		Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
159
160		Err(error) => Err(CommonError::FromStandardIOError(error, path.clone(), "Delete.Stat")),
161	}
162}
163
164/// Rename operations implementation for MountainEnvironment
165pub(super) async fn rename_impl(
166	env:&MountainEnvironment,
167
168	source:&PathBuf,
169
170	target:&PathBuf,
171
172	overwrite:bool,
173) -> Result<(), CommonError> {
174	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, source)?;
175
176	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, target)?;
177
178	if !overwrite && fs::try_exists(target).await.unwrap_or(false) {
179		return Err(CommonError::FileSystemFileExists(target.clone()));
180	}
181
182	fs::rename(source, target)
183		.await
184		.map_err(|error| CommonError::FromStandardIOError(error, source.clone(), "Rename"))
185}
186
187/// Copy operations implementation for MountainEnvironment
188pub(super) async fn copy_impl(
189	env:&MountainEnvironment,
190
191	source:&PathBuf,
192
193	target:&PathBuf,
194
195	overwrite:bool,
196) -> Result<(), CommonError> {
197	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, source)?;
198
199	Utility::PathSecurity::IsPathAllowedForAccess(&env.ApplicationState, target)?;
200
201	// Validate that source exists
202	if !fs::try_exists(source).await.unwrap_or(false) {
203		return Err(CommonError::FileSystemNotFound(source.clone()));
204	}
205
206	// Call stat_file_impl from the ReadOperations module
207	let source_metadata = super::ReadOperations::stat_file_impl(env, source).await?;
208
209	let SourceIsDir = (source_metadata.FileType & FileTypeDTO::Directory as u8) != 0;
210
211	// Prevent copying file/dir to itself (which would truncate or
212	// recursively explode).
213	if fs::canonicalize(source).await.ok().as_ref() == fs::canonicalize(target).await.ok().as_ref() {
214		return Err(CommonError::InvalidArgument {
215			ArgumentName:"Target".to_string(),
216			Reason:"Cannot copy file to itself".to_string(),
217		});
218	}
219
220	if !overwrite && fs::try_exists(target).await.unwrap_or(false) {
221		return Err(CommonError::FileSystemFileExists(target.clone()));
222	}
223
224	// Create target parent directory if needed (works for both file
225	// and directory copies; the directory copy below also creates
226	// the target itself).
227	if let Some(target_parent) = target.parent() {
228		if !fs::try_exists(target_parent).await.unwrap_or(false) {
229			fs::create_dir_all(target_parent).await.map_err(|error| {
230				CommonError::FromStandardIOError(error, target_parent.to_path_buf(), "Copy.CreateTargetParent")
231			})?;
232		}
233	}
234
235	if SourceIsDir {
236		// Recursive directory copy. Walks the source tree iteratively
237		// (avoids deep async recursion blowing the stack on
238		// pathological depths) and re-creates each entry under the
239		// target. Symlinks are followed to keep behaviour consistent
240		// with VS Code's `IFileService.copy` - if you want preserve-
241		// symlinks semantics, use `clone_native` instead which does a
242		// COW reflink on supported filesystems.
243		return copy_directory_recursive(source, target, overwrite).await;
244	}
245
246	fs::copy(source, target)
247		.await
248		.map(|_| ())
249		.map_err(|error| CommonError::FromStandardIOError(error, source.clone(), "Copy"))
250}
251
252/// Recursively copy a directory tree from `source` into `target`.
253/// Iterative (uses an explicit stack of `(SrcDir, DstDir)`) so it
254/// can't blow the Tokio task stack on very deep trees. Files inside
255/// re-use `tokio::fs::copy` for fast path; directories are created
256/// with `create_dir`. Symlinks are dereferenced.
257async fn copy_directory_recursive(source:&PathBuf, target:&PathBuf, overwrite:bool) -> Result<(), CommonError> {
258	// Pre-create the top-level target dir.
259	if !fs::try_exists(target).await.unwrap_or(false) {
260		fs::create_dir(target)
261			.await
262			.map_err(|error| CommonError::FromStandardIOError(error, target.clone(), "Copy.CreateTargetRoot"))?;
263	}
264
265	let mut Stack:Vec<(PathBuf, PathBuf)> = vec![(source.clone(), target.clone())];
266
267	while let Some((SrcDir, DstDir)) = Stack.pop() {
268		let mut Entries = fs::read_dir(&SrcDir)
269			.await
270			.map_err(|error| CommonError::FromStandardIOError(error, SrcDir.clone(), "Copy.ReadDir"))?;
271
272		while let Some(Entry) = Entries
273			.next_entry()
274			.await
275			.map_err(|error| CommonError::FromStandardIOError(error, SrcDir.clone(), "Copy.NextEntry"))?
276		{
277			let Name = Entry.file_name();
278
279			let SrcPath = SrcDir.join(&Name);
280
281			let DstPath = DstDir.join(&Name);
282
283			let FileType = Entry
284				.file_type()
285				.await
286				.map_err(|error| CommonError::FromStandardIOError(error, SrcPath.clone(), "Copy.FileType"))?;
287
288			if FileType.is_dir() {
289				if !fs::try_exists(&DstPath).await.unwrap_or(false) {
290					fs::create_dir(&DstPath).await.map_err(|error| {
291						CommonError::FromStandardIOError(error, DstPath.clone(), "Copy.CreateSubDir")
292					})?;
293				}
294
295				Stack.push((SrcPath, DstPath));
296			} else {
297				if !overwrite && fs::try_exists(&DstPath).await.unwrap_or(false) {
298					return Err(CommonError::FileSystemFileExists(DstPath));
299				}
300
301				fs::copy(&SrcPath, &DstPath)
302					.await
303					.map_err(|error| CommonError::FromStandardIOError(error, SrcPath.clone(), "Copy.CopyFile"))?;
304			}
305		}
306	}
307
308	Ok(())
309}
310
311/// CreateFile operations implementation for MountainEnvironment
312pub(super) async fn create_file_impl(env:&MountainEnvironment, path:&PathBuf) -> Result<(), CommonError> {
313	// Use WriteFile with an empty Vec, ensuring creation without overwrite.
314	// This ensures proper parent directory creation and path validation.
315	write_file_impl(env, path, vec![], true, false).await
316}