1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! Atomic File Operations for SQLiteGraph V2
//!
//! Database-grade filesystem operations with atomic semantics and fsync discipline.
//! Provides guaranteed atomic copy operations suitable for crash-safe snapshot management.
use crate::backend::native::{NativeBackendError, NativeResult};
use std::path::{Path, PathBuf};
/// Atomic file operations handler
pub struct AtomicFileOperations;
/// Error types for atomic operations
#[derive(Debug)]
pub enum AtomicOperationError {
/// Source path does not exist or is inaccessible
SourceInaccessible { path: PathBuf, error: String },
/// Destination path exists and overwrite protection is enabled
DestinationExists { path: PathBuf },
/// Source path is a directory when file expected
SourceIsDirectory { path: PathBuf },
/// Destination parent directory does not exist
ParentDirectoryMissing { parent: PathBuf },
/// Filesystem I/O error during operation
IoError { context: String, error: String },
/// Temporary file cleanup failed
CleanupFailed { temp_path: PathBuf, error: String },
}
impl AtomicFileOperations {
/// Create new atomic file operations instance
pub fn new() -> Self {
Self
}
/// Perform atomic file copy from source to destination
///
/// Requirements:
/// - Source must exist and be a file (not directory)
/// - Destination must not exist (overwrite protection)
/// - Uses temporary file + rename for atomicity
/// - Full fsync discipline for crash safety
/// - Cleanup on any failure
///
/// # Failing TDD Tests Expected:
/// - test_atomic_copy_file_to_new_location
/// - test_atomic_copy_rejects_directory
/// - test_atomic_copy_overwrite_protection
/// - test_atomic_copy_crash_safety_simulation
pub fn atomic_copy_file(&self, source: &Path, destination: &Path) -> NativeResult<()> {
// Step 1: Validate preconditions
self.validate_preconditions(source, destination)?;
// Step 2: Create temporary file path
let temp_path = self.create_temp_path(destination);
// Step 3: Ensure temp path doesn't exist from previous operations
if temp_path.exists() {
let _ = self.cleanup_temp_file(&temp_path);
}
// Step 4: Perform copy with proper error handling
let copy_result = std::fs::copy(source, &temp_path);
if let Err(e) = copy_result {
let _ = self.cleanup_temp_file(&temp_path);
return Err(NativeBackendError::Io(e));
}
// Step 5: Verify the temp file was created as a file (not directory)
if !temp_path.is_file() {
let _ = self.cleanup_temp_file(&temp_path);
return Err(NativeBackendError::IoError {
context: format!("Temporary path was not created as a file: {:?}", temp_path),
source: std::io::Error::new(std::io::ErrorKind::Other, "File creation failed"),
});
}
// Step 6: Sync temporary file to ensure data is durable
if let Err(e) = self.sync_file(&temp_path) {
let _ = self.cleanup_temp_file(&temp_path);
return Err(e);
}
// Step 7: Atomic rename to final destination
if let Err(e) = std::fs::rename(&temp_path, destination) {
let _ = self.cleanup_temp_file(&temp_path);
return Err(NativeBackendError::IoError {
context: "Failed to rename temporary file".to_string(),
source: e,
});
}
// Step 8: Sync parent directory to make rename durable
if let Some(parent) = destination.parent() {
if let Err(e) = self.sync_directory(parent) {
return Err(e);
}
}
Ok(())
}
/// Validate file copy preconditions before performing operation
fn validate_preconditions(&self, source: &Path, destination: &Path) -> NativeResult<()> {
// Check source exists and is a file
if !source.exists() {
return Err(NativeBackendError::InvalidParameter {
context: format!("Source file does not exist: {:?}", source),
source: None,
});
}
// Check source is explicitly a file (not directory)
if !source.is_file() {
return Err(NativeBackendError::InvalidParameter {
context: format!(
"Source path is not a file: {:?} (is_directory: {})",
source,
source.is_dir()
),
source: None,
});
}
// Check destination does not exist (overwrite protection)
if destination.exists() {
return Err(NativeBackendError::InvalidParameter {
context: format!(
"Destination already exists, overwrite protection enabled: {:?}",
destination
),
source: None,
});
}
// Check parent directory exists and is actually a directory
if let Some(parent) = destination.parent() {
if !parent.exists() {
return Err(NativeBackendError::InvalidParameter {
context: format!("Destination parent directory does not exist: {:?}", parent),
source: None,
});
}
if !parent.is_dir() {
return Err(NativeBackendError::InvalidParameter {
context: format!(
"Destination parent is not a directory: {:?} (is_file: {})",
parent,
parent.is_file()
),
source: None,
});
}
}
Ok(())
}
/// Create temporary file path for atomic operation
fn create_temp_path(&self, destination: &Path) -> PathBuf {
// Generate unique temporary file path
let file_stem = destination
.file_stem()
.unwrap_or_else(|| std::ffi::OsStr::new("temp"));
let parent = destination.parent().unwrap_or_else(|| Path::new("."));
// Simple format that guarantees file semantics
parent.join(format!(
"{}.tmp.{}",
file_stem.to_string_lossy(),
std::process::id()
))
}
/// Perform cleanup of temporary files on failure
fn cleanup_temp_file(&self, temp_path: &Path) -> NativeResult<()> {
if temp_path.exists() {
// Remove only if it's a file, not a directory
if temp_path.is_file() {
match std::fs::remove_file(temp_path) {
Ok(()) => {
// Successfully cleaned up temp file
}
Err(e) => {
// Log warning but don't fail the operation if cleanup fails
eprintln!(
"Warning: Failed to cleanup temporary file {:?}: {}",
temp_path, e
);
}
}
} else {
// Unexpected: temp path exists but is a directory
eprintln!(
"Warning: Temporary path exists as directory, attempting to remove: {:?}",
temp_path
);
match std::fs::remove_dir_all(temp_path) {
Ok(()) => {
// Successfully removed directory
}
Err(e) => {
eprintln!(
"Warning: Failed to cleanup temporary directory {:?}: {}",
temp_path, e
);
}
}
}
}
Ok(())
}
/// Sync file data to disk for durability
fn sync_file(&self, file_path: &Path) -> NativeResult<()> {
use std::fs::OpenOptions;
let file = OpenOptions::new().write(true).open(file_path)?;
file.sync_all().map_err(|e| NativeBackendError::IoError {
context: format!("Failed to sync file: {:?}", file_path),
source: e,
})
}
/// Sync directory metadata to disk for rename durability
fn sync_directory(&self, dir_path: &Path) -> NativeResult<()> {
use std::fs::OpenOptions;
// Try to open directory for syncing, but don't fail if unsupported
match OpenOptions::new().read(true).write(true).open(dir_path) {
Ok(dir) => dir.sync_all().map_err(|e| NativeBackendError::IoError {
context: format!("Failed to sync directory: {:?}", dir_path),
source: e,
}),
Err(e) => {
// Directory sync not supported on this filesystem, log warning but continue
eprintln!(
"Warning: Directory sync not supported: {:?} (error: {})",
dir_path, e
);
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::{NamedTempFile, TempDir};
#[test]
fn test_atomic_copy_file_to_new_location() {
// This test should FAIL initially - create failing TDD test
let temp_dir = TempDir::new().unwrap();
let atomic_ops = AtomicFileOperations::new();
// Create source file with test data
let source_file = temp_dir.path().join("source.txt");
let mut source = fs::File::create(&source_file).unwrap();
source.write_all(b"Hello, SQLiteGraph V2!").unwrap();
source.sync_all().unwrap();
// Debug: Verify source file was created correctly
assert!(source_file.exists(), "Source file should exist");
assert!(source_file.is_file(), "Source should be a file");
assert!(!source_file.is_dir(), "Source should not be a directory");
// Destination path (must not exist)
let dest_file = temp_dir.path().join("destination.txt");
// Debug: Verify destination doesn't exist initially
assert!(
!dest_file.exists(),
"Destination should not exist initially"
);
// Perform atomic copy - should succeed when implemented
match atomic_ops.atomic_copy_file(&source_file, &dest_file) {
Ok(()) => {
// Success case
}
Err(e) => {
// Debug: Print error details
panic!("Atomic copy failed with error: {:?}", e);
}
}
// Verify destination exists and has correct content
assert!(dest_file.exists());
assert!(dest_file.is_file(), "Destination should be a file");
let dest_content = fs::read_to_string(&dest_file).unwrap();
assert_eq!(dest_content, "Hello, SQLiteGraph V2!");
// Verify source still exists (copy, not move)
assert!(source_file.exists());
let source_content = fs::read_to_string(&source_file).unwrap();
assert_eq!(source_content, "Hello, SQLiteGraph V2!");
}
#[test]
fn test_atomic_copy_rejects_directory() {
// Test case: Source is directory, should reject
let temp_dir = TempDir::new().unwrap();
let atomic_ops = AtomicFileOperations::new();
// Create a directory as "source"
let source_dir = temp_dir.path().join("source_dir");
fs::create_dir(&source_dir).unwrap();
// Destination file path
let dest_file = temp_dir.path().join("destination.txt");
// Should reject directory source with appropriate error
let result = atomic_ops.atomic_copy_file(&source_dir, &dest_file);
assert!(result.is_err(), "Should reject directory source");
// When implemented, should return specific error type
// match result {
// Err(NativeBackendError::InvalidOperation { context, .. }) => {
// assert!(context.contains("directory"));
// }
// _ => panic!("Expected directory rejection error"),
// }
}
#[test]
fn test_atomic_copy_overwrite_protection() {
// Test case: Destination exists, should reject
let temp_dir = TempDir::new().unwrap();
let atomic_ops = AtomicFileOperations::new();
// Create source file
let source_file = temp_dir.path().join("source.txt");
let mut source = fs::File::create(&source_file).unwrap();
source.write_all(b"Source content").unwrap();
source.sync_all().unwrap();
// Create destination file that already exists
let dest_file = temp_dir.path().join("destination.txt");
let mut dest = fs::File::create(&dest_file).unwrap();
dest.write_all(b"Existing content").unwrap();
dest.sync_all().unwrap();
// Should reject overwriting existing file
let result = atomic_ops.atomic_copy_file(&source_file, &dest_file);
assert!(result.is_err(), "Should reject overwriting existing file");
// Verify destination content unchanged
let dest_content = fs::read_to_string(&dest_file).unwrap();
assert_eq!(dest_content, "Existing content");
}
#[test]
fn test_atomic_copy_crash_safety_simulation() {
// Test case: Simulate crash during copy operation
let temp_dir = TempDir::new().unwrap();
let atomic_ops = AtomicFileOperations::new();
// Create source file with known content
let source_file = temp_dir.path().join("source.txt");
let test_content = "Important SQLiteGraph data that must be crash-safe";
let mut source = fs::File::create(&source_file).unwrap();
source.write_all(test_content.as_bytes()).unwrap();
source.sync_all().unwrap();
// Destination path
let dest_file = temp_dir.path().join("destination.txt");
// When implemented, this should handle crash scenarios:
// 1. If copy fails after temp file creation, temp file should be cleaned up
// 2. Destination should never be partially written
// 3. Operation should be atomic (either fully succeeds or fully fails)
// Test that operation either succeeds completely or fails cleanly
let result = atomic_ops.atomic_copy_file(&source_file, &dest_file);
if result.is_ok() {
// If succeeded, destination should have complete, correct content
assert!(dest_file.exists());
let dest_content = fs::read_to_string(&dest_file).unwrap();
assert_eq!(dest_content, test_content);
// No temporary files should remain
let temp_file = dest_file.with_extension("tmp");
assert!(!temp_file.exists(), "Temporary file should be cleaned up");
} else {
// If failed, destination should not exist and no temp files should remain
assert!(
!dest_file.exists(),
"Destination should not exist on failed copy"
);
let temp_file = dest_file.with_extension("tmp");
assert!(
!temp_file.exists(),
"Temporary file should be cleaned up on failure"
);
}
}
#[test]
fn test_atomic_copy_missing_parent_directory() {
// Test case: Destination parent directory doesn't exist
let temp_dir = TempDir::new().unwrap();
let atomic_ops = AtomicFileOperations::new();
// Create source file
let source_file = temp_dir.path().join("source.txt");
let mut source = fs::File::create(&source_file).unwrap();
source.write_all(b"Test content").unwrap();
source.sync_all().unwrap();
// Destination in non-existent parent directory
let dest_file = temp_dir.path().join("nonexistent").join("destination.txt");
// Should fail due to missing parent directory
let result = atomic_ops.atomic_copy_file(&source_file, &dest_file);
assert!(result.is_err(), "Should fail when parent directory missing");
}
#[test]
fn test_atomic_copy_missing_source() {
// Test case: Source file doesn't exist
let temp_dir = TempDir::new().unwrap();
let atomic_ops = AtomicFileOperations::new();
// Non-existent source file
let source_file = temp_dir.path().join("nonexistent.txt");
let dest_file = temp_dir.path().join("destination.txt");
// Should fail due to missing source file
let result = atomic_ops.atomic_copy_file(&source_file, &dest_file);
assert!(result.is_err(), "Should fail when source file missing");
}
}