cartridge_rs/lib.rs
1//! # Cartridge - High-Performance Mutable Archive Format
2//!
3//! `cartridge-rs` provides a high-level, easy-to-use API for working with Cartridge archives.
4//! Cartridge is a mutable archive format optimized for embedded systems with features like:
5//!
6//! - **Mutable archives** with in-place modifications
7//! - **SQLite VFS integration** for running databases directly inside archives
8//! - **Advanced features**: compression, encryption, snapshots, IAM policies
9//! - **Engram integration**: freeze to immutable, signed archives
10//!
11//! ## Quick Start
12//!
13//! ```rust,no_run
14//! use cartridge_rs::{Cartridge, Result};
15//!
16//! # fn main() -> Result<()> {
17//! // Create a new archive - auto-grows from 12KB as needed!
18//! let mut cart = Cartridge::create("my-data", "My Data Container")?;
19//!
20//! // Write files
21//! cart.write("documents/report.txt", b"Hello, World!")?;
22//!
23//! // Read files
24//! let content = cart.read("documents/report.txt")?;
25//!
26//! // List directory
27//! let files = cart.list("documents")?;
28//!
29//! // Automatic cleanup on drop
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Advanced Usage
35//!
36//! ```rust,no_run
37//! use cartridge_rs::{CartridgeBuilder, Result};
38//!
39//! # fn main() -> Result<()> {
40//! // Use builder for custom configuration
41//! let mut cart = CartridgeBuilder::new()
42//! .slug("my-data")
43//! .title("My Data Container")
44//! .path("/data/my-container") // Custom path
45//! .with_audit_logging()
46//! .build()?;
47//!
48//! cart.write("data.txt", b"content")?;
49//! # Ok(())
50//! # }
51//! ```
52
53// Core implementation (merged from cartridge-core)
54pub mod core;
55
56// Re-export core modules internally so crate:: paths in core still work
57#[allow(unused_imports)]
58pub(crate) use core::{
59 allocator, audit, buffer_pool, catalog, compression, encryption, engram_integration, error,
60 header, iam, io, manifest, page, snapshot, validation, vfs,
61};
62
63// Re-export core types that users need
64pub use crate::core::{
65 catalog::{FileMetadata, FileType},
66 encryption::EncryptionConfig,
67 error::{CartridgeError, Result},
68 header::{Header, S3AclMode, S3FeatureFuses, S3SseMode, S3VersioningMode, PAGE_SIZE},
69 iam::{Action, Effect, Policy, PolicyEngine, Statement},
70 manifest::Manifest,
71 snapshot::{SnapshotManager, SnapshotMetadata},
72 validation::ContainerSlug,
73};
74
75use crate::core::Cartridge as CoreCartridge;
76use serde::{Deserialize, Serialize};
77use std::collections::HashSet;
78use std::path::Path;
79use tracing::{debug, info};
80
81/// Rich metadata about a file or directory in the archive
82///
83/// Entry provides a convenient view of files and directories with parsed metadata,
84/// eliminating the need for consumers to manually parse paths and build hierarchies.
85///
86/// # Examples
87///
88/// ```rust,no_run
89/// use cartridge_rs::Cartridge;
90///
91/// # fn main() -> cartridge_rs::Result<()> {
92/// let cart = Cartridge::open("data.cart")?;
93/// let entries = cart.list_entries("documents")?;
94///
95/// for entry in entries {
96/// if entry.is_dir {
97/// println!("📁 {} ({})", entry.name, entry.path);
98/// } else {
99/// println!("📄 {} ({} bytes)", entry.name, entry.size.unwrap_or(0));
100/// }
101/// }
102/// # Ok(())
103/// # }
104/// ```
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct Entry {
107 /// Full path in the archive (e.g., "research/notes/overview.cml")
108 pub path: String,
109
110 /// Just the name (e.g., "overview.cml" or "notes")
111 pub name: String,
112
113 /// Parent directory path (e.g., "research/notes")
114 /// Empty string for root-level entries
115 pub parent: String,
116
117 /// True if this is a directory (has children under this prefix)
118 pub is_dir: bool,
119
120 /// File size in bytes (None for directories or if unavailable)
121 pub size: Option<u64>,
122
123 /// Creation timestamp as Unix epoch seconds (None if unavailable)
124 pub created: Option<u64>,
125
126 /// Last modification timestamp as Unix epoch seconds (None if unavailable)
127 pub modified: Option<u64>,
128
129 /// MIME type or content type (None if unavailable)
130 pub content_type: Option<String>,
131
132 /// File type (File, Directory, or Symlink)
133 pub file_type: FileType,
134
135 /// Compressed size on disk in bytes (None for directories or if unavailable)
136 /// This is the actual space used in the container, which may be less than
137 /// `size` when compression is enabled.
138 pub compressed_size: Option<u64>,
139}
140
141/// Convert flat paths to Entry objects with rich metadata
142///
143/// This helper parses paths, infers directory structure, and fetches metadata
144/// from the cartridge for each entry. Internal .cartridge/ files are filtered out.
145fn paths_to_entries(cart: &CoreCartridge, paths: &[String], _prefix: &str) -> Result<Vec<Entry>> {
146 let mut entries = Vec::new();
147 let mut seen_dirs: HashSet<String> = HashSet::new();
148
149 // Process each file path
150 for path in paths {
151 // Skip internal .cartridge directory (with or without leading slash)
152 if path.starts_with(".cartridge/")
153 || path == ".cartridge"
154 || path.starts_with("/.cartridge")
155 {
156 continue;
157 }
158 // Extract name and parent from path
159 let name = path.rsplit('/').next().unwrap_or(path).to_string();
160 let parent = if let Some(idx) = path.rfind('/') {
161 if idx == 0 {
162 // Root level: "/file.txt" -> parent is "/"
163 "/".to_string()
164 } else {
165 path[..idx].to_string()
166 }
167 } else {
168 String::new()
169 };
170
171 // Fetch metadata for the file
172 let metadata = cart.metadata(path).ok();
173
174 // Create entry for the file
175 entries.push(Entry {
176 path: path.clone(),
177 name,
178 parent: parent.clone(),
179 is_dir: false,
180 size: metadata.as_ref().map(|m| m.size),
181 created: metadata.as_ref().map(|m| m.created_at),
182 modified: metadata.as_ref().map(|m| m.modified_at),
183 content_type: metadata.as_ref().and_then(|m| m.content_type.clone()),
184 file_type: metadata
185 .as_ref()
186 .map(|m| m.file_type)
187 .unwrap_or(FileType::File),
188 compressed_size: metadata
189 .as_ref()
190 .map(|m| (m.blocks.len() as u64) * PAGE_SIZE as u64),
191 });
192
193 // Add parent directories (if not already seen)
194 let mut current_parent = parent.as_str();
195 while !current_parent.is_empty() && current_parent != "/" {
196 if seen_dirs.insert(current_parent.to_string()) {
197 let parent_name = current_parent
198 .rsplit('/')
199 .next()
200 .unwrap_or(current_parent)
201 .to_string();
202 let grandparent = if let Some(idx) = current_parent.rfind('/') {
203 if idx == 0 {
204 "/".to_string()
205 } else {
206 current_parent[..idx].to_string()
207 }
208 } else {
209 String::new()
210 };
211
212 entries.push(Entry {
213 path: current_parent.to_string(),
214 name: parent_name,
215 parent: grandparent,
216 is_dir: true,
217 size: None,
218 created: None,
219 modified: None,
220 content_type: None,
221 file_type: FileType::Directory,
222 compressed_size: None,
223 });
224 }
225
226 // Move up the tree
227 if let Some(idx) = current_parent.rfind('/') {
228 current_parent = ¤t_parent[..idx];
229 } else {
230 break;
231 }
232 }
233 }
234
235 // Sort: directories first, then alphabetically by name
236 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
237 (true, false) => std::cmp::Ordering::Less,
238 (false, true) => std::cmp::Ordering::Greater,
239 _ => a.name.cmp(&b.name),
240 });
241
242 Ok(entries)
243}
244
245/// High-level Cartridge archive API
246///
247/// This is a wrapper around `cartridge_core::Cartridge` that provides:
248/// - Sensible defaults
249/// - Simpler method names
250/// - Automatic resource management
251/// - Better error messages
252///
253/// # Examples
254///
255/// ```rust,no_run
256/// use cartridge_rs::{Cartridge, Result};
257///
258/// # fn main() -> Result<()> {
259/// let mut cart = Cartridge::create("my-data", "My Data")?;
260/// cart.write("file.txt", b"content")?;
261/// let data = cart.read("file.txt")?;
262/// # Ok(())
263/// # }
264/// ```
265pub struct Cartridge {
266 inner: CoreCartridge,
267}
268
269impl Cartridge {
270 /// Create a new Cartridge archive with auto-growth
271 ///
272 /// Creates a container with the given slug and title.
273 /// - Starts at 12KB and grows automatically as needed
274 /// - Slug is used as the filename (kebab-case, becomes `{slug}.cart`)
275 /// - Title is the human-readable display name
276 ///
277 /// # Examples
278 ///
279 /// ```rust,no_run
280 /// use cartridge_rs::Cartridge;
281 ///
282 /// // Creates "my-data.cart" in current directory
283 /// let mut cart = Cartridge::create("my-data", "My Data Container")?;
284 /// # Ok::<(), cartridge_rs::CartridgeError>(())
285 /// ```
286 pub fn create(slug: &str, title: &str) -> Result<Self> {
287 info!("Creating cartridge with slug '{}', title '{}'", slug, title);
288 let inner = CoreCartridge::create(slug, title)?;
289 Ok(Cartridge { inner })
290 }
291
292 /// Create a new Cartridge archive at a specific path
293 ///
294 /// Use this when you need to specify a custom directory or path.
295 ///
296 /// # Examples
297 ///
298 /// ```rust,no_run
299 /// use cartridge_rs::Cartridge;
300 ///
301 /// // Creates "/data/my-container.cart"
302 /// let mut cart = Cartridge::create_at("/data/my-container", "my-container", "My Container")?;
303 /// # Ok::<(), cartridge_rs::CartridgeError>(())
304 /// ```
305 pub fn create_at<P: AsRef<Path>>(path: P, slug: &str, title: &str) -> Result<Self> {
306 info!(
307 "Creating cartridge at {:?} with slug '{}', title '{}'",
308 path.as_ref(),
309 slug,
310 title
311 );
312 let inner = CoreCartridge::create_at(path, slug, title)?;
313 Ok(Cartridge { inner })
314 }
315
316 /// Open an existing Cartridge archive
317 ///
318 /// # Examples
319 ///
320 /// ```rust,no_run
321 /// use cartridge_rs::Cartridge;
322 ///
323 /// let mut cart = Cartridge::open("existing.cart")?;
324 /// # Ok::<(), cartridge_rs::CartridgeError>(())
325 /// ```
326 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
327 info!("Opening cartridge at {:?}", path.as_ref());
328 let inner = CoreCartridge::open(path)?;
329 Ok(Cartridge { inner })
330 }
331
332 /// Write data to a file in the archive
333 ///
334 /// Creates the file if it doesn't exist, updates it if it does.
335 /// Automatically creates parent directories.
336 ///
337 /// # Examples
338 ///
339 /// ```rust,no_run
340 /// # use cartridge_rs::Cartridge;
341 /// # let mut cart = Cartridge::create("my-data", "My Data")?;
342 /// cart.write("documents/report.txt", b"Hello, World!")?;
343 /// # Ok::<(), cartridge_rs::CartridgeError>(())
344 /// ```
345 pub fn write<P: AsRef<str>>(&mut self, path: P, content: &[u8]) -> Result<()> {
346 let path = path.as_ref();
347 debug!("Writing {} bytes to {}", content.len(), path);
348
349 // Check if file exists, create or update accordingly
350 if self.inner.exists(path)? {
351 self.inner.write_file(path, content)
352 } else {
353 self.inner.create_file(path, content)
354 }
355 }
356
357 /// Read data from a file in the archive
358 ///
359 /// # Examples
360 ///
361 /// ```rust,no_run
362 /// # use cartridge_rs::Cartridge;
363 /// # let cart = Cartridge::create("my-data", "My Data")?;
364 /// let content = cart.read("documents/report.txt")?;
365 /// println!("Content: {}", String::from_utf8_lossy(&content));
366 /// # Ok::<(), cartridge_rs::CartridgeError>(())
367 /// ```
368 pub fn read<P: AsRef<str>>(&self, path: P) -> Result<Vec<u8>> {
369 let path = path.as_ref();
370 debug!("Reading {}", path);
371 self.inner.read_file(path)
372 }
373
374 /// Delete a file from the archive
375 ///
376 /// # Examples
377 ///
378 /// ```rust,no_run
379 /// # use cartridge_rs::Cartridge;
380 /// # let mut cart = Cartridge::create("my-data", "My Data")?;
381 /// cart.delete("old_file.txt")?;
382 /// # Ok::<(), cartridge_rs::CartridgeError>(())
383 /// ```
384 pub fn delete<P: AsRef<str>>(&mut self, path: P) -> Result<()> {
385 let path = path.as_ref();
386 debug!("Deleting {}", path);
387 self.inner.delete_file(path)
388 }
389
390 /// List all entries in a directory
391 ///
392 /// # Examples
393 ///
394 /// ```rust,no_run
395 /// # use cartridge_rs::Cartridge;
396 /// # let cart = Cartridge::create("my-data", "My Data")?;
397 /// let files = cart.list("documents")?;
398 /// for file in files {
399 /// println!("Found: {}", file);
400 /// }
401 /// # Ok::<(), cartridge_rs::CartridgeError>(())
402 /// ```
403 pub fn list<P: AsRef<str>>(&self, path: P) -> Result<Vec<String>> {
404 let path = path.as_ref();
405 debug!("Listing directory {}", path);
406 self.inner.list_dir(path)
407 }
408
409 /// List all entries with rich metadata under a given prefix
410 ///
411 /// Returns Entry objects with parsed path components, file metadata,
412 /// and inferred directory information. This eliminates the need to
413 /// manually parse paths and build hierarchies.
414 ///
415 /// # Examples
416 ///
417 /// ```rust,no_run
418 /// # use cartridge_rs::Cartridge;
419 /// # let cart = Cartridge::create("my-data", "My Data")?;
420 /// let entries = cart.list_entries("documents")?;
421 /// for entry in entries {
422 /// if entry.is_dir {
423 /// println!("📁 {}", entry.name);
424 /// } else {
425 /// println!("📄 {} ({} bytes)", entry.name, entry.size.unwrap_or(0));
426 /// }
427 /// }
428 /// # Ok::<(), cartridge_rs::CartridgeError>(())
429 /// ```
430 pub fn list_entries<P: AsRef<str>>(&self, prefix: P) -> Result<Vec<Entry>> {
431 let prefix = prefix.as_ref();
432 debug!("Listing entries under prefix {}", prefix);
433 let paths = self.inner.list_dir(prefix)?;
434 paths_to_entries(&self.inner, &paths, prefix)
435 }
436
437 /// List immediate children of a directory
438 ///
439 /// Like `list_entries()` but filters to only direct children,
440 /// providing a traditional directory-style listing.
441 ///
442 /// # Examples
443 ///
444 /// ```rust,no_run
445 /// # use cartridge_rs::Cartridge;
446 /// # let cart = Cartridge::create("my-data", "My Data")?;
447 /// // List only immediate children of "documents"
448 /// let children = cart.list_children("documents")?;
449 /// for child in children {
450 /// println!("{} - parent: {}", child.name, child.parent);
451 /// }
452 /// # Ok::<(), cartridge_rs::CartridgeError>(())
453 /// ```
454 pub fn list_children<P: AsRef<str>>(&self, parent: P) -> Result<Vec<Entry>> {
455 let parent = parent.as_ref();
456 debug!("Listing immediate children of {}", parent);
457 let all_entries = self.list_entries(parent)?;
458
459 // Filter to immediate children only
460 Ok(all_entries
461 .into_iter()
462 .filter(|e| e.parent == parent)
463 .collect())
464 }
465
466 /// Check if a path is a directory
467 ///
468 /// Returns true if the path has children (i.e., is a directory),
469 /// false otherwise.
470 ///
471 /// # Examples
472 ///
473 /// ```rust,no_run
474 /// # use cartridge_rs::Cartridge;
475 /// # let cart = Cartridge::create("my-data", "My Data")?;
476 /// if cart.is_dir("documents")? {
477 /// println!("documents is a directory");
478 /// }
479 /// # Ok::<(), cartridge_rs::CartridgeError>(())
480 /// ```
481 pub fn is_dir<P: AsRef<str>>(&self, path: P) -> Result<bool> {
482 let path = path.as_ref();
483 debug!("Checking if {} is a directory", path);
484
485 // A path is a directory if it has children
486 let prefix = if path.is_empty() {
487 String::new()
488 } else {
489 format!("{}/", path)
490 };
491
492 let paths = self.inner.list_dir(&prefix)?;
493 Ok(!paths.is_empty())
494 }
495
496 /// Check if a file or directory exists
497 ///
498 /// # Examples
499 ///
500 /// ```rust,no_run
501 /// # use cartridge_rs::Cartridge;
502 /// # let cart = Cartridge::create("my-data", "My Data")?;
503 /// if cart.exists("config.json")? {
504 /// println!("Config file found!");
505 /// }
506 /// # Ok::<(), cartridge_rs::CartridgeError>(())
507 /// ```
508 pub fn exists<P: AsRef<str>>(&self, path: P) -> Result<bool> {
509 self.inner.exists(path.as_ref())
510 }
511
512 /// Get metadata for a file or directory
513 ///
514 /// # Examples
515 ///
516 /// ```rust,no_run
517 /// # use cartridge_rs::Cartridge;
518 /// # let cart = Cartridge::create("my-data", "My Data")?;
519 /// let meta = cart.metadata("file.txt")?;
520 /// println!("Size: {} bytes", meta.size);
521 /// # Ok::<(), cartridge_rs::CartridgeError>(())
522 /// ```
523 pub fn metadata<P: AsRef<str>>(&self, path: P) -> Result<FileMetadata> {
524 self.inner.metadata(path.as_ref())
525 }
526
527 /// Create a directory
528 ///
529 /// Automatically creates parent directories if needed.
530 ///
531 /// # Examples
532 ///
533 /// ```rust,no_run
534 /// # use cartridge_rs::Cartridge;
535 /// # let mut cart = Cartridge::create("my-data", "My Data")?;
536 /// cart.create_dir("documents/reports/2025")?;
537 /// # Ok::<(), cartridge_rs::CartridgeError>(())
538 /// ```
539 pub fn create_dir<P: AsRef<str>>(&mut self, path: P) -> Result<()> {
540 self.inner.create_dir(path.as_ref())
541 }
542
543 /// Flush all pending changes to disk
544 ///
545 /// # Examples
546 ///
547 /// ```rust,no_run
548 /// # use cartridge_rs::Cartridge;
549 /// # let mut cart = Cartridge::create("my-data", "My Data")?;
550 /// cart.write("file.txt", b"data")?;
551 /// cart.flush()?; // Ensure changes are persisted
552 /// # Ok::<(), cartridge_rs::CartridgeError>(())
553 /// ```
554 pub fn flush(&mut self) -> Result<()> {
555 debug!("Flushing cartridge to disk");
556 self.inner.flush()
557 }
558
559 /// Get the container slug
560 ///
561 /// # Examples
562 ///
563 /// ```rust,no_run
564 /// # use cartridge_rs::Cartridge;
565 /// # let cart = Cartridge::create("my-data", "My Data")?;
566 /// assert_eq!(cart.slug()?, "my-data");
567 /// # Ok::<(), cartridge_rs::CartridgeError>(())
568 /// ```
569 pub fn slug(&self) -> Result<String> {
570 self.inner.slug()
571 }
572
573 /// Get the container title
574 ///
575 /// # Examples
576 ///
577 /// ```rust,no_run
578 /// # use cartridge_rs::Cartridge;
579 /// # let cart = Cartridge::create("my-data", "My Container")?;
580 /// assert_eq!(cart.title()?, "My Container");
581 /// # Ok::<(), cartridge_rs::CartridgeError>(())
582 /// ```
583 pub fn title(&self) -> Result<String> {
584 self.inner.title()
585 }
586
587 /// Read the container manifest
588 ///
589 /// # Examples
590 ///
591 /// ```rust,no_run
592 /// # use cartridge_rs::Cartridge;
593 /// # let cart = Cartridge::create("my-data", "My Container")?;
594 /// let manifest = cart.read_manifest()?;
595 /// println!("Version: {}", manifest.version);
596 /// # Ok::<(), cartridge_rs::CartridgeError>(())
597 /// ```
598 pub fn read_manifest(&self) -> Result<Manifest> {
599 self.inner.read_manifest()
600 }
601
602 /// Update the container manifest
603 ///
604 /// # Examples
605 ///
606 /// ```rust,no_run
607 /// # use cartridge_rs::Cartridge;
608 /// # let mut cart = Cartridge::create("my-data", "My Container")?;
609 /// cart.update_manifest(|manifest| {
610 /// manifest.description = Some("Updated description".to_string());
611 /// })?;
612 /// # Ok::<(), cartridge_rs::CartridgeError>(())
613 /// ```
614 pub fn update_manifest<F>(&mut self, f: F) -> Result<()>
615 where
616 F: FnOnce(&mut Manifest),
617 {
618 self.inner.update_manifest(f)
619 }
620
621 /// Get the cartridge header with S3 fuses and metadata
622 ///
623 /// # Examples
624 ///
625 /// ```rust,no_run
626 /// # use cartridge_rs::Cartridge;
627 /// # fn main() -> cartridge_rs::Result<()> {
628 /// let cart = Cartridge::create("data", "My Data")?;
629 /// let fuses = cart.header().get_s3_fuses();
630 /// println!("ACL mode: {:?}", fuses.acl_mode);
631 /// # Ok(())
632 /// # }
633 /// ```
634 pub fn header(&self) -> &Header {
635 self.inner.header()
636 }
637
638 /// Get mutable access to the cartridge header
639 pub fn header_mut(&mut self) -> &mut Header {
640 self.inner.header_mut()
641 }
642
643 /// Update user-defined metadata for a file
644 ///
645 /// Stores custom key-value pairs in file metadata without modifying content.
646 /// Useful for S3-compatible metadata, ACLs, SSE headers, etc.
647 ///
648 /// # Examples
649 ///
650 /// ```rust,no_run
651 /// # use cartridge_rs::Cartridge;
652 /// # fn main() -> cartridge_rs::Result<()> {
653 /// let mut cart = Cartridge::create("data", "My Data")?;
654 /// cart.write("file.txt", b"content")?;
655 /// cart.update_user_metadata("file.txt", "s3:acl", r#"{"grants":[]}"#)?;
656 /// # Ok(())
657 /// # }
658 /// ```
659 pub fn update_user_metadata<P: AsRef<str>>(
660 &mut self,
661 path: P,
662 key: impl Into<String>,
663 value: impl Into<String>,
664 ) -> Result<()> {
665 self.inner.update_user_metadata(path.as_ref(), key, value)
666 }
667
668 /// Create a snapshot of the current cartridge state
669 ///
670 /// # Arguments
671 /// * `name` - Snapshot name
672 /// * `description` - Snapshot description
673 /// * `snapshot_dir` - Directory to store snapshot files
674 ///
675 /// # Returns
676 /// Snapshot ID (timestamp in microseconds)
677 ///
678 /// # Examples
679 ///
680 /// ```rust,no_run
681 /// # use cartridge_rs::Cartridge;
682 /// # use std::path::Path;
683 /// # fn main() -> cartridge_rs::Result<()> {
684 /// let mut cart = Cartridge::create("data", "My Data")?;
685 /// cart.write("file.txt", b"version 1")?;
686 /// let snapshot_id = cart.create_snapshot(
687 /// "v1".to_string(),
688 /// "First version".to_string(),
689 /// Path::new("./snapshots")
690 /// )?;
691 /// # Ok(())
692 /// # }
693 /// ```
694 pub fn create_snapshot(
695 &self,
696 name: String,
697 description: String,
698 snapshot_dir: &std::path::Path,
699 ) -> Result<u64> {
700 self.inner.create_snapshot(name, description, snapshot_dir)
701 }
702
703 /// Restore cartridge state from a snapshot
704 ///
705 /// # Arguments
706 /// * `snapshot_id` - Snapshot ID to restore
707 /// * `snapshot_dir` - Directory containing snapshot files
708 ///
709 /// # Examples
710 ///
711 /// ```rust,no_run
712 /// # use cartridge_rs::Cartridge;
713 /// # use std::path::Path;
714 /// # fn main() -> cartridge_rs::Result<()> {
715 /// let mut cart = Cartridge::create("data", "My Data")?;
716 /// let snapshot_id = 1234567890000000u64;
717 /// cart.restore_snapshot(snapshot_id, Path::new("./snapshots"))?;
718 /// # Ok(())
719 /// # }
720 /// ```
721 pub fn restore_snapshot(&mut self, snapshot_id: u64, snapshot_dir: &std::path::Path) -> Result<()> {
722 self.inner.restore_snapshot(snapshot_id, snapshot_dir)
723 }
724
725 /// Enable encryption for all new files written to the cartridge
726 ///
727 /// Once enabled, all new files created or updated will be encrypted using AES-256-GCM.
728 /// Existing files are not automatically encrypted - they remain as-is.
729 ///
730 /// # Arguments
731 /// * `key` - 32-byte AES-256 encryption key
732 ///
733 /// # Examples
734 ///
735 /// ```rust,no_run
736 /// # use cartridge_rs::{Cartridge, EncryptionConfig};
737 /// # fn main() -> cartridge_rs::Result<()> {
738 /// let mut cart = Cartridge::create("secure-data", "Secure Data")?;
739 ///
740 /// // Generate a random encryption key
741 /// let key = EncryptionConfig::generate_key();
742 ///
743 /// // Enable encryption
744 /// cart.enable_encryption(&key)?;
745 ///
746 /// // All new files will be encrypted
747 /// cart.write("sensitive.txt", b"confidential data")?;
748 /// # Ok(())
749 /// # }
750 /// ```
751 pub fn enable_encryption(&mut self, key: &[u8; 32]) -> Result<()> {
752 self.inner.enable_encryption(key)
753 }
754
755 /// Disable encryption
756 ///
757 /// New files written after disabling encryption will not be encrypted.
758 /// Files that were already encrypted remain encrypted and require the key to read.
759 ///
760 /// # Examples
761 ///
762 /// ```rust,no_run
763 /// # use cartridge_rs::Cartridge;
764 /// # fn main() -> cartridge_rs::Result<()> {
765 /// let mut cart = Cartridge::create("data", "My Data")?;
766 /// cart.disable_encryption()?;
767 /// # Ok(())
768 /// # }
769 /// ```
770 pub fn disable_encryption(&mut self) -> Result<()> {
771 self.inner.disable_encryption()
772 }
773
774 /// Check if encryption is currently enabled
775 ///
776 /// Returns `true` if new files will be encrypted, `false` otherwise.
777 ///
778 /// # Examples
779 ///
780 /// ```rust,no_run
781 /// # use cartridge_rs::{Cartridge, EncryptionConfig};
782 /// # fn main() -> cartridge_rs::Result<()> {
783 /// let mut cart = Cartridge::create("data", "My Data")?;
784 /// assert!(!cart.is_encrypted());
785 ///
786 /// let key = EncryptionConfig::generate_key();
787 /// cart.enable_encryption(&key)?;
788 /// assert!(cart.is_encrypted());
789 /// # Ok(())
790 /// # }
791 /// ```
792 pub fn is_encrypted(&self) -> bool {
793 self.inner.is_encrypted()
794 }
795
796 /// Get access to the underlying core Cartridge for advanced operations
797 ///
798 /// Use this when you need features not exposed by the high-level API:
799 /// - IAM policies
800 /// - Snapshots
801 /// - Audit logging
802 /// - Custom allocator settings
803 ///
804 /// # Examples
805 ///
806 /// ```rust,ignore
807 /// use cartridge_rs::{Cartridge, Policy};
808 ///
809 /// let mut cart = Cartridge::create("data.cart")?;
810 ///
811 /// // Access advanced features
812 /// let policy = Policy::new("my-policy", vec![]);
813 /// cart.inner_mut().set_policy(policy);
814 /// ```
815 pub fn inner(&self) -> &CoreCartridge {
816 &self.inner
817 }
818
819 /// Get mutable access to the underlying core Cartridge
820 pub fn inner_mut(&mut self) -> &mut CoreCartridge {
821 &mut self.inner
822 }
823}
824
825/// Builder for customizing Cartridge creation
826///
827/// Provides a fluent API for configuring advanced options.
828///
829/// # Examples
830///
831/// ```rust,no_run
832/// use cartridge_rs::CartridgeBuilder;
833///
834/// # fn main() -> cartridge_rs::Result<()> {
835/// let cart = CartridgeBuilder::new()
836/// .slug("my-data")
837/// .title("My Data Container")
838/// .path("/data/my-container") // Optional: custom path
839/// .with_audit_logging()
840/// .build()?;
841/// # Ok(())
842/// # }
843/// ```
844pub struct CartridgeBuilder {
845 path: Option<String>,
846 slug: Option<String>,
847 title: Option<String>,
848 enable_audit: bool,
849}
850
851impl CartridgeBuilder {
852 /// Create a new CartridgeBuilder with default settings
853 pub fn new() -> Self {
854 CartridgeBuilder {
855 path: None,
856 slug: None,
857 title: None,
858 enable_audit: false,
859 }
860 }
861
862 /// Set the slug (kebab-case identifier)
863 pub fn slug<S: Into<String>>(mut self, slug: S) -> Self {
864 self.slug = Some(slug.into());
865 self
866 }
867
868 /// Set the title (human-readable display name)
869 pub fn title<S: Into<String>>(mut self, title: S) -> Self {
870 self.title = Some(title.into());
871 self
872 }
873
874 /// Set a custom path (optional, defaults to slug in current directory)
875 pub fn path<P: Into<String>>(mut self, path: P) -> Self {
876 self.path = Some(path.into());
877 self
878 }
879
880 /// Enable audit logging for all operations
881 pub fn with_audit_logging(mut self) -> Self {
882 self.enable_audit = true;
883 self
884 }
885
886 /// Build the Cartridge instance
887 pub fn build(self) -> Result<Cartridge> {
888 let slug = self.slug.ok_or_else(|| {
889 CartridgeError::Io(std::io::Error::new(
890 std::io::ErrorKind::InvalidInput,
891 "slug must be set",
892 ))
893 })?;
894
895 let title = self.title.ok_or_else(|| {
896 CartridgeError::Io(std::io::Error::new(
897 std::io::ErrorKind::InvalidInput,
898 "title must be set",
899 ))
900 })?;
901
902 info!("Building cartridge with slug '{}', title '{}'", slug, title);
903
904 let mut inner = if let Some(path) = self.path {
905 CoreCartridge::create_at(&path, &slug, &title)?
906 } else {
907 CoreCartridge::create(&slug, &title)?
908 };
909
910 if self.enable_audit {
911 use crate::core::audit::AuditLogger;
912 use std::sync::Arc;
913 use std::time::Duration;
914
915 let logger = Arc::new(AuditLogger::new(1000, Duration::from_secs(60)));
916 inner.set_audit_logger(logger);
917 debug!("Audit logging enabled");
918 }
919
920 Ok(Cartridge { inner })
921 }
922}
923
924impl Default for CartridgeBuilder {
925 fn default() -> Self {
926 Self::new()
927 }
928}
929
930/// Virtual Filesystem trait for unified storage interface
931///
932/// Provides a common interface that can be implemented by different storage backends:
933/// - Cartridge (mutable containers)
934/// - Engram (immutable archives)
935/// - ZipVfs, TarVfs (other archive formats)
936/// - S3Vfs, LocalVfs (remote/local filesystems)
937///
938/// This allows applications to work with any storage backend using the same API.
939///
940/// # Examples
941///
942/// ```rust,no_run
943/// use cartridge_rs::{Cartridge, Vfs};
944///
945/// fn process_storage<V: Vfs>(vfs: &V, path: &str) -> Result<(), Box<dyn std::error::Error>> {
946/// // Works with any VFS implementation
947/// let entries = vfs.list_entries(path)?;
948/// for entry in entries {
949/// if !entry.is_dir {
950/// let content = vfs.read(&entry.path)?;
951/// println!("File: {} ({} bytes)", entry.name, content.len());
952/// }
953/// }
954/// Ok(())
955/// }
956///
957/// // Use with Cartridge
958/// let cart = Cartridge::create("my-data", "My Data")?;
959/// process_storage(&cart, "documents")?;
960/// # Ok::<(), Box<dyn std::error::Error>>(())
961/// ```
962pub trait Vfs {
963 /// List all entries under a given prefix with rich metadata
964 ///
965 /// Returns Entry objects with parsed path components, file metadata,
966 /// and inferred directory information.
967 fn list_entries(&self, prefix: &str) -> Result<Vec<Entry>>;
968
969 /// List immediate children of a directory (non-recursive)
970 ///
971 /// Returns only entries whose parent matches the given path,
972 /// providing a directory-style listing view.
973 fn list_children(&self, parent: &str) -> Result<Vec<Entry>>;
974
975 /// Read the contents of a file
976 ///
977 /// Returns the full file contents as a byte vector.
978 fn read(&self, path: &str) -> Result<Vec<u8>>;
979
980 /// Write or update a file
981 ///
982 /// Creates the file if it doesn't exist, updates it if it does.
983 /// Automatically creates parent directories as needed.
984 fn write(&mut self, path: &str, data: &[u8]) -> Result<()>;
985
986 /// Delete a file or directory
987 ///
988 /// For directories, this typically deletes all contents recursively.
989 fn delete(&mut self, path: &str) -> Result<()>;
990
991 /// Check if a path exists
992 fn exists(&self, path: &str) -> Result<bool>;
993
994 /// Check if a path is a directory
995 fn is_dir(&self, path: &str) -> Result<bool>;
996
997 /// Get metadata for a path
998 fn metadata(&self, path: &str) -> Result<FileMetadata>;
999}
1000
1001/// Implement VFS trait for Cartridge
1002impl Vfs for Cartridge {
1003 fn list_entries(&self, prefix: &str) -> Result<Vec<Entry>> {
1004 self.list_entries(prefix)
1005 }
1006
1007 fn list_children(&self, parent: &str) -> Result<Vec<Entry>> {
1008 self.list_children(parent)
1009 }
1010
1011 fn read(&self, path: &str) -> Result<Vec<u8>> {
1012 self.read(path)
1013 }
1014
1015 fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
1016 self.write(path, data)
1017 }
1018
1019 fn delete(&mut self, path: &str) -> Result<()> {
1020 self.delete(path)
1021 }
1022
1023 fn exists(&self, path: &str) -> Result<bool> {
1024 self.exists(path)
1025 }
1026
1027 fn is_dir(&self, path: &str) -> Result<bool> {
1028 self.is_dir(path)
1029 }
1030
1031 fn metadata(&self, path: &str) -> Result<FileMetadata> {
1032 self.metadata(path)
1033 }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::*;
1039
1040 #[test]
1041 fn test_create_and_write() -> Result<()> {
1042 let temp_dir = tempfile::TempDir::new().unwrap();
1043 let path = temp_dir.path().join("test-cart");
1044
1045 let mut cart = Cartridge::create_at(&path, "test-cart", "Test Cartridge")?;
1046 cart.write("test.txt", b"hello")?;
1047 cart.flush()?; // Ensure write is persisted
1048
1049 let content = cart.read("test.txt")?;
1050 assert_eq!(content, b"hello");
1051
1052 Ok(())
1053 }
1054
1055 #[test]
1056 fn test_builder() -> Result<()> {
1057 let temp_dir = tempfile::TempDir::new().unwrap();
1058 let path = temp_dir.path().join("builder-cart");
1059
1060 let cart = CartridgeBuilder::new()
1061 .slug("builder-cart")
1062 .title("Builder Cartridge")
1063 .path(path.to_str().unwrap())
1064 .build()?;
1065
1066 // Starts small with auto-growth
1067 assert!(cart.inner().stats().total_blocks >= 3);
1068
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn test_list_entries_flat_structure() -> Result<()> {
1074 let temp_dir = tempfile::TempDir::new().unwrap();
1075 let path = temp_dir.path().join("flat-cart");
1076
1077 let mut cart = Cartridge::create_at(&path, "flat-cart", "Flat Cartridge")?;
1078
1079 // Create flat structure (no nesting)
1080 cart.write("/file1.txt", b"content1")?;
1081 cart.write("/file2.txt", b"content2")?;
1082 cart.write("/file3.txt", b"content3")?;
1083 cart.flush()?;
1084
1085 let entries = cart.list_entries("/")?;
1086
1087 // Should have 3 files
1088 assert_eq!(entries.len(), 3);
1089 assert!(entries.iter().all(|e| !e.is_dir));
1090 assert!(entries.iter().all(|e| e.parent == "/"));
1091
1092 Ok(())
1093 }
1094
1095 #[test]
1096 fn test_list_entries_nested_structure() -> Result<()> {
1097 let temp_dir = tempfile::TempDir::new().unwrap();
1098 let path = temp_dir.path().join("nested-cart");
1099
1100 let mut cart = Cartridge::create_at(&path, "nested-cart", "Nested Cartridge")?;
1101
1102 // Create nested structure (3+ levels)
1103 cart.write("/docs/guides/getting-started.md", b"# Getting Started")?;
1104 cart.write("/docs/guides/advanced.md", b"# Advanced")?;
1105 cart.write("/docs/api/reference.md", b"# API Reference")?;
1106 cart.write("/src/main.rs", b"fn main() {}")?;
1107 cart.flush()?;
1108
1109 let entries = cart.list_entries("/")?;
1110
1111 // Should have: /docs, /docs/guides, /docs/api, /src (4 dirs) + 4 files = 8 entries
1112 assert_eq!(entries.len(), 8);
1113
1114 // Count directories and files
1115 let dirs: Vec<_> = entries.iter().filter(|e| e.is_dir).collect();
1116 let files: Vec<_> = entries.iter().filter(|e| !e.is_dir).collect();
1117
1118 assert_eq!(dirs.len(), 4);
1119 assert_eq!(files.len(), 4);
1120
1121 // Verify directory names
1122 let dir_names: Vec<_> = dirs.iter().map(|e| e.name.as_str()).collect();
1123 assert!(dir_names.contains(&"docs"));
1124 assert!(dir_names.contains(&"guides"));
1125 assert!(dir_names.contains(&"api"));
1126 assert!(dir_names.contains(&"src"));
1127
1128 Ok(())
1129 }
1130
1131 #[test]
1132 fn test_list_entries_empty() -> Result<()> {
1133 let temp_dir = tempfile::TempDir::new().unwrap();
1134 let path = temp_dir.path().join("empty-cart");
1135
1136 let cart = Cartridge::create_at(&path, "empty-cart", "Empty Cartridge")?;
1137
1138 let entries = cart.list_entries("/")?;
1139
1140 // Empty cartridge should return empty list (excluding internal .cartridge/)
1141 assert_eq!(entries.len(), 0);
1142
1143 Ok(())
1144 }
1145
1146 #[test]
1147 fn test_list_children_root_level() -> Result<()> {
1148 let temp_dir = tempfile::TempDir::new().unwrap();
1149 let path = temp_dir.path().join("children-cart");
1150
1151 let mut cart = Cartridge::create_at(&path, "children-cart", "Children Cartridge")?;
1152
1153 // Create structure with nested files
1154 cart.write("/root1.txt", b"root file 1")?;
1155 cart.write("/root2.txt", b"root file 2")?;
1156 cart.write("/docs/nested.md", b"nested file")?;
1157 cart.write("/docs/deep/very-nested.md", b"very nested")?;
1158 cart.flush()?;
1159
1160 let children = cart.list_children("/")?;
1161
1162 // Should only have root-level entries: root1.txt, root2.txt, docs/
1163 assert_eq!(children.len(), 3);
1164 assert!(children.iter().all(|e| e.parent == "/"));
1165
1166 // Count root files and directories
1167 let files: Vec<_> = children.iter().filter(|e| !e.is_dir).collect();
1168 let dirs: Vec<_> = children.iter().filter(|e| e.is_dir).collect();
1169
1170 assert_eq!(files.len(), 2);
1171 assert_eq!(dirs.len(), 1);
1172 assert_eq!(dirs[0].name, "docs");
1173
1174 Ok(())
1175 }
1176
1177 #[test]
1178 fn test_list_children_nested_directory() -> Result<()> {
1179 let temp_dir = tempfile::TempDir::new().unwrap();
1180 let path = temp_dir.path().join("nested-children-cart");
1181
1182 let mut cart = Cartridge::create_at(&path, "nested-children-cart", "Nested Children")?;
1183
1184 cart.write("/docs/readme.md", b"readme")?;
1185 cart.write("/docs/guides/tutorial.md", b"tutorial")?;
1186 cart.write("/docs/api/reference.md", b"reference")?;
1187 cart.flush()?;
1188
1189 let children = cart.list_children("/docs")?;
1190
1191 // Should have: readme.md, guides/, api/ (3 immediate children)
1192 assert_eq!(children.len(), 3);
1193 assert!(children.iter().all(|e| e.parent == "/docs"));
1194
1195 Ok(())
1196 }
1197
1198 #[test]
1199 fn test_list_children_only_subdirectories() -> Result<()> {
1200 let temp_dir = tempfile::TempDir::new().unwrap();
1201 let path = temp_dir.path().join("subdirs-cart");
1202
1203 let mut cart = Cartridge::create_at(&path, "subdirs-cart", "Subdirs")?;
1204
1205 // Create directories with no files at this level
1206 cart.write("/parent/child1/file.txt", b"file1")?;
1207 cart.write("/parent/child2/file.txt", b"file2")?;
1208 cart.flush()?;
1209
1210 let children = cart.list_children("/parent")?;
1211
1212 // Should only have child1/ and child2/ directories
1213 assert_eq!(children.len(), 2);
1214 assert!(children.iter().all(|e| e.is_dir));
1215 assert!(children.iter().all(|e| e.parent == "/parent"));
1216
1217 Ok(())
1218 }
1219
1220 #[test]
1221 fn test_list_children_only_files() -> Result<()> {
1222 let temp_dir = tempfile::TempDir::new().unwrap();
1223 let path = temp_dir.path().join("files-cart");
1224
1225 let mut cart = Cartridge::create_at(&path, "files-cart", "Files")?;
1226
1227 // Create directory with only files
1228 cart.write("/data/file1.dat", b"data1")?;
1229 cart.write("/data/file2.dat", b"data2")?;
1230 cart.write("/data/file3.dat", b"data3")?;
1231 cart.flush()?;
1232
1233 let children = cart.list_children("/data")?;
1234
1235 // Should only have 3 files
1236 assert_eq!(children.len(), 3);
1237 assert!(children.iter().all(|e| !e.is_dir));
1238 assert!(children.iter().all(|e| e.parent == "/data"));
1239
1240 Ok(())
1241 }
1242
1243 #[test]
1244 fn test_is_dir_known_directory() -> Result<()> {
1245 let temp_dir = tempfile::TempDir::new().unwrap();
1246 let path = temp_dir.path().join("isdir-cart");
1247
1248 let mut cart = Cartridge::create_at(&path, "isdir-cart", "IsDir")?;
1249
1250 cart.write("/documents/report.txt", b"report")?;
1251 cart.flush()?;
1252
1253 // "/documents" should be a directory
1254 assert!(cart.is_dir("/documents")?);
1255
1256 Ok(())
1257 }
1258
1259 #[test]
1260 fn test_is_dir_known_file() -> Result<()> {
1261 let temp_dir = tempfile::TempDir::new().unwrap();
1262 let path = temp_dir.path().join("isfile-cart");
1263
1264 let mut cart = Cartridge::create_at(&path, "isfile-cart", "IsFile")?;
1265
1266 cart.write("/documents/report.txt", b"report")?;
1267 cart.flush()?;
1268
1269 // "/documents/report.txt" should NOT be a directory
1270 assert!(!cart.is_dir("/documents/report.txt")?);
1271
1272 Ok(())
1273 }
1274
1275 #[test]
1276 fn test_is_dir_nonexistent() -> Result<()> {
1277 let temp_dir = tempfile::TempDir::new().unwrap();
1278 let path = temp_dir.path().join("nonexistent-cart");
1279
1280 let cart = Cartridge::create_at(&path, "nonexistent-cart", "NonExistent")?;
1281
1282 // Non-existent path should not be a directory
1283 assert!(!cart.is_dir("/does-not-exist")?);
1284
1285 Ok(())
1286 }
1287
1288 #[test]
1289 fn test_entry_metadata_fields() -> Result<()> {
1290 let temp_dir = tempfile::TempDir::new().unwrap();
1291 let path = temp_dir.path().join("metadata-cart");
1292
1293 let mut cart = Cartridge::create_at(&path, "metadata-cart", "Metadata")?;
1294
1295 cart.write("/test.txt", b"hello world")?;
1296 cart.flush()?;
1297
1298 let entries = cart.list_entries("/")?;
1299
1300 assert_eq!(entries.len(), 1);
1301 let entry = &entries[0];
1302
1303 // Verify basic fields
1304 assert_eq!(entry.path, "/test.txt");
1305 assert_eq!(entry.name, "test.txt");
1306 assert_eq!(entry.parent, "/");
1307 assert!(!entry.is_dir);
1308
1309 // Verify metadata fields are populated
1310 assert!(entry.size.is_some());
1311 assert_eq!(entry.size.unwrap(), 11); // "hello world" is 11 bytes
1312 assert!(entry.created.is_some());
1313 assert!(entry.modified.is_some());
1314
1315 Ok(())
1316 }
1317}