- Add missing modules to lib.rs (apt, package_manager, ostree, test_support, etc.) - Fix DebPackageMetadata type compatibility with PackageInfo - Add missing description and scripts fields to DebPackageMetadata - Create apt.rs module alias for apt_compat - Create minimal apt_ostree_integration.rs with required types - Fix type conversions in package_manager.rs resolve_dependencies - Update all test instances of DebPackageMetadata with new fields - All Rust code now compiles successfully - All tests now pass successfully
420 lines
No EOL
15 KiB
Rust
420 lines
No EOL
15 KiB
Rust
//! Filesystem Assembly for APT-OSTree
|
|
//!
|
|
//! This module implements the filesystem assembly process that combines base filesystem
|
|
//! with layered packages using hardlink optimization for efficient storage and proper
|
|
//! layering order.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::fs;
|
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
|
use std::collections::HashMap;
|
|
use tracing::{info, warn, debug};
|
|
use serde::{Serialize, Deserialize};
|
|
use std::pin::Pin;
|
|
use std::future::Future;
|
|
|
|
use crate::error::AptOstreeResult;
|
|
use crate::dependency_resolver::DebPackageMetadata;
|
|
|
|
/// Filesystem assembly manager
|
|
pub struct FilesystemAssembler {
|
|
base_path: PathBuf,
|
|
staging_path: PathBuf,
|
|
final_path: PathBuf,
|
|
}
|
|
|
|
/// File metadata for deduplication
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
pub struct FileMetadata {
|
|
pub size: u64,
|
|
pub mode: u32,
|
|
pub mtime: i64,
|
|
pub inode: u64,
|
|
pub device: u64,
|
|
}
|
|
|
|
/// Assembly configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AssemblyConfig {
|
|
pub base_filesystem_path: PathBuf,
|
|
pub staging_directory: PathBuf,
|
|
pub final_deployment_path: PathBuf,
|
|
pub enable_hardlinks: bool,
|
|
pub preserve_permissions: bool,
|
|
pub preserve_timestamps: bool,
|
|
}
|
|
|
|
impl Default for AssemblyConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
base_filesystem_path: PathBuf::from("/var/lib/apt-ostree/base"),
|
|
staging_directory: PathBuf::from("/var/lib/apt-ostree/staging"),
|
|
final_deployment_path: PathBuf::from("/var/lib/apt-ostree/deployments"),
|
|
enable_hardlinks: true,
|
|
preserve_permissions: true,
|
|
preserve_timestamps: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FilesystemAssembler {
|
|
/// Create a new filesystem assembler
|
|
pub fn new(config: AssemblyConfig) -> AptOstreeResult<Self> {
|
|
info!("Creating filesystem assembler with config: {:?}", config);
|
|
|
|
// Create directories if they don't exist
|
|
fs::create_dir_all(&config.base_filesystem_path)?;
|
|
fs::create_dir_all(&config.staging_directory)?;
|
|
fs::create_dir_all(&config.final_deployment_path)?;
|
|
|
|
Ok(Self {
|
|
base_path: config.base_filesystem_path,
|
|
staging_path: config.staging_directory,
|
|
final_path: config.final_deployment_path,
|
|
})
|
|
}
|
|
|
|
/// Assemble filesystem from base and package layers
|
|
pub async fn assemble_filesystem(
|
|
&self,
|
|
base_commit: &str,
|
|
package_commits: &[String],
|
|
target_deployment: &str,
|
|
) -> AptOstreeResult<()> {
|
|
info!("Assembling filesystem from base {} and {} packages", base_commit, package_commits.len());
|
|
|
|
// Create staging directory for this assembly
|
|
let staging_dir = self.staging_path.join(target_deployment);
|
|
if staging_dir.exists() {
|
|
fs::remove_dir_all(&staging_dir)?;
|
|
}
|
|
fs::create_dir_all(&staging_dir)?;
|
|
|
|
// Step 1: Checkout base filesystem with hardlinks
|
|
self.checkout_base_filesystem(base_commit, &staging_dir).await?;
|
|
|
|
// Step 2: Layer packages in order
|
|
for (index, package_commit) in package_commits.iter().enumerate() {
|
|
info!("Layering package {} ({}/{})", package_commit, index + 1, package_commits.len());
|
|
self.layer_package(package_commit, &staging_dir).await?;
|
|
}
|
|
|
|
// Step 3: Optimize hardlinks
|
|
if self.should_optimize_hardlinks() {
|
|
self.optimize_hardlinks(&staging_dir).await?;
|
|
}
|
|
|
|
// Step 4: Create final deployment
|
|
let final_deployment = self.final_path.join(target_deployment);
|
|
if final_deployment.exists() {
|
|
fs::remove_dir_all(&final_deployment)?;
|
|
}
|
|
|
|
self.create_final_deployment(&staging_dir, &final_deployment).await?;
|
|
|
|
// Clean up staging
|
|
fs::remove_dir_all(&staging_dir)?;
|
|
|
|
info!("Filesystem assembly completed: {}", target_deployment);
|
|
Ok(())
|
|
}
|
|
|
|
/// Checkout base filesystem using hardlinks for efficiency
|
|
async fn checkout_base_filesystem(&self, base_commit: &str, staging_dir: &Path) -> AptOstreeResult<()> {
|
|
info!("Checking out base filesystem from commit: {}", base_commit);
|
|
|
|
// TODO: Implement actual OSTree checkout
|
|
// For now, create a placeholder base filesystem
|
|
let base_commit_path = self.base_path.join(base_commit);
|
|
|
|
if base_commit_path.exists() {
|
|
// Copy base filesystem using hardlinks where possible
|
|
self.copy_with_hardlinks(&base_commit_path, staging_dir).await?;
|
|
} else {
|
|
// Create minimal base filesystem structure
|
|
self.create_minimal_base_filesystem(staging_dir).await?;
|
|
}
|
|
|
|
info!("Base filesystem checkout completed");
|
|
Ok(())
|
|
}
|
|
|
|
/// Layer a package on top of the current filesystem
|
|
async fn layer_package(&self, package_commit: &str, staging_dir: &Path) -> AptOstreeResult<()> {
|
|
info!("Layering package commit: {}", package_commit);
|
|
|
|
// TODO: Implement actual package commit checkout
|
|
// For now, simulate package layering
|
|
let package_path = self.staging_path.join("packages").join(package_commit);
|
|
|
|
if package_path.exists() {
|
|
// Apply package files on top of current filesystem
|
|
self.apply_package_files(&package_path, staging_dir).await?;
|
|
} else {
|
|
warn!("Package commit not found: {}", package_commit);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Copy directory using hardlinks where possible
|
|
fn copy_with_hardlinks<'a>(&'a self, src: &'a Path, dst: &'a Path) -> Pin<Box<dyn Future<Output = AptOstreeResult<()>> + 'a>> {
|
|
Box::pin(async move {
|
|
debug!("Copying with hardlinks: {} -> {}", src.display(), dst.display());
|
|
|
|
if src.is_file() {
|
|
// For files, try to create hardlink, fallback to copy
|
|
if let Err(_) = fs::hard_link(src, dst) {
|
|
fs::copy(src, dst)?;
|
|
}
|
|
} else if src.is_dir() {
|
|
fs::create_dir_all(dst)?;
|
|
|
|
for entry in fs::read_dir(src)? {
|
|
let entry = entry?;
|
|
let src_path = entry.path();
|
|
let dst_path = dst.join(entry.file_name());
|
|
|
|
self.copy_with_hardlinks(&src_path, &dst_path).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Create minimal base filesystem structure
|
|
pub async fn create_minimal_base_filesystem(&self, staging_dir: &Path) -> AptOstreeResult<()> {
|
|
info!("Creating minimal base filesystem structure");
|
|
|
|
let dirs = [
|
|
"bin", "boot", "dev", "etc", "home", "lib", "lib64", "media",
|
|
"mnt", "opt", "proc", "root", "run", "sbin", "srv", "sys",
|
|
"tmp", "usr", "var"
|
|
];
|
|
|
|
for dir in &dirs {
|
|
fs::create_dir_all(staging_dir.join(dir))?;
|
|
}
|
|
|
|
// Create essential files
|
|
let etc_dir = staging_dir.join("etc");
|
|
fs::write(etc_dir.join("hostname"), "localhost\n")?;
|
|
fs::write(etc_dir.join("hosts"), "127.0.0.1 localhost\n::1 localhost\n")?;
|
|
|
|
info!("Minimal base filesystem created");
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply package files to the filesystem
|
|
async fn apply_package_files(&self, package_path: &Path, staging_dir: &Path) -> AptOstreeResult<()> {
|
|
debug!("Applying package files: {} -> {}", package_path.display(), staging_dir.display());
|
|
|
|
// Read package metadata
|
|
let metadata_path = package_path.join("metadata.json");
|
|
if metadata_path.exists() {
|
|
let metadata_content = fs::read_to_string(&metadata_path)?;
|
|
let metadata: DebPackageMetadata = serde_json::from_str(&metadata_content)?;
|
|
|
|
info!("Applying package: {} {}", metadata.name, metadata.version);
|
|
}
|
|
|
|
// Apply files from package
|
|
let files_dir = package_path.join("files");
|
|
if files_dir.exists() {
|
|
self.copy_with_hardlinks(&files_dir, staging_dir).await?;
|
|
}
|
|
|
|
// Apply scripts if they exist
|
|
let scripts_dir = package_path.join("scripts");
|
|
if scripts_dir.exists() {
|
|
// TODO: Execute scripts in proper order
|
|
info!("Package scripts found, would execute in proper order");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Optimize hardlinks for identical files
|
|
async fn optimize_hardlinks(&self, staging_dir: &Path) -> AptOstreeResult<()> {
|
|
info!("Optimizing hardlinks in: {}", staging_dir.display());
|
|
|
|
let mut file_map: HashMap<FileMetadata, Vec<PathBuf>> = HashMap::new();
|
|
|
|
// Scan all files and group by metadata
|
|
self.scan_files_for_deduplication(staging_dir, &mut file_map).await?;
|
|
|
|
// Create hardlinks for identical files
|
|
let mut hardlink_count = 0;
|
|
for (metadata, paths) in file_map {
|
|
if paths.len() > 1 {
|
|
// Use the first path as the source for hardlinks
|
|
let source = &paths[0];
|
|
for target in &paths[1..] {
|
|
if let Err(_) = fs::hard_link(source, target) {
|
|
warn!("Failed to create hardlink: {} -> {}", source.display(), target.display());
|
|
} else {
|
|
hardlink_count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
info!("Hardlink optimization completed: {} hardlinks created", hardlink_count);
|
|
Ok(())
|
|
}
|
|
|
|
/// Scan files for deduplication
|
|
fn scan_files_for_deduplication<'a>(
|
|
&'a self,
|
|
dir: &'a Path,
|
|
file_map: &'a mut HashMap<FileMetadata, Vec<PathBuf>>,
|
|
) -> Pin<Box<dyn Future<Output = AptOstreeResult<()>> + 'a>> {
|
|
Box::pin(async move {
|
|
for entry in fs::read_dir(dir)? {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
|
|
if path.is_file() {
|
|
let metadata = fs::metadata(&path)?;
|
|
let file_metadata = FileMetadata {
|
|
size: metadata.size(),
|
|
mode: metadata.mode(),
|
|
mtime: metadata.mtime(),
|
|
inode: metadata.ino(),
|
|
device: metadata.dev(),
|
|
};
|
|
|
|
file_map.entry(file_metadata).or_insert_with(Vec::new).push(path);
|
|
} else if path.is_dir() {
|
|
self.scan_files_for_deduplication(&path, file_map).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Create final deployment
|
|
async fn create_final_deployment(&self, staging_dir: &Path, final_dir: &Path) -> AptOstreeResult<()> {
|
|
info!("Creating final deployment: {} -> {}", staging_dir.display(), final_dir.display());
|
|
|
|
// Copy staging to final location
|
|
self.copy_with_hardlinks(staging_dir, final_dir).await?;
|
|
|
|
// Set proper permissions
|
|
self.set_deployment_permissions(final_dir).await?;
|
|
|
|
info!("Final deployment created: {}", final_dir.display());
|
|
Ok(())
|
|
}
|
|
|
|
/// Set proper permissions for deployment
|
|
async fn set_deployment_permissions(&self, deployment_dir: &Path) -> AptOstreeResult<()> {
|
|
debug!("Setting deployment permissions: {}", deployment_dir.display());
|
|
|
|
// Set directory permissions
|
|
let metadata = fs::metadata(deployment_dir)?;
|
|
let mut permissions = metadata.permissions();
|
|
permissions.set_mode(0o755);
|
|
fs::set_permissions(deployment_dir, permissions)?;
|
|
|
|
// Recursively set permissions for subdirectories
|
|
self.set_recursive_permissions(deployment_dir).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Set recursive permissions
|
|
fn set_recursive_permissions<'a>(&'a self, dir: &'a Path) -> Pin<Box<dyn Future<Output = AptOstreeResult<()>> + 'a>> {
|
|
Box::pin(async move {
|
|
for entry in fs::read_dir(dir)? {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
|
|
let metadata = fs::metadata(&path)?;
|
|
let mut permissions = metadata.permissions();
|
|
|
|
if path.is_dir() {
|
|
permissions.set_mode(0o755);
|
|
fs::set_permissions(&path, permissions)?;
|
|
self.set_recursive_permissions(&path).await?;
|
|
} else if path.is_file() {
|
|
// Check if file is executable
|
|
let mode = metadata.mode();
|
|
if mode & 0o111 != 0 {
|
|
permissions.set_mode(0o755);
|
|
} else {
|
|
permissions.set_mode(0o644);
|
|
}
|
|
fs::set_permissions(&path, permissions)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Check if hardlink optimization should be enabled
|
|
fn should_optimize_hardlinks(&self) -> bool {
|
|
// TODO: Make this configurable
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Package layering order manager
|
|
pub struct PackageLayeringManager {
|
|
assembler: FilesystemAssembler,
|
|
}
|
|
|
|
impl PackageLayeringManager {
|
|
/// Create a new package layering manager
|
|
pub fn new(assembler: FilesystemAssembler) -> Self {
|
|
Self { assembler }
|
|
}
|
|
|
|
/// Determine optimal layering order for packages
|
|
pub fn determine_layering_order(&self, packages: &[DebPackageMetadata]) -> Vec<String> {
|
|
info!("Determining layering order for {} packages", packages.len());
|
|
|
|
// Simple dependency-based ordering
|
|
// TODO: Implement proper dependency resolution
|
|
let mut ordered_packages = Vec::new();
|
|
let mut processed = std::collections::HashSet::new();
|
|
|
|
for package in packages {
|
|
if !processed.contains(&package.name) {
|
|
ordered_packages.push(package.name.clone());
|
|
processed.insert(package.name.clone());
|
|
}
|
|
}
|
|
|
|
info!("Layering order determined: {:?}", ordered_packages);
|
|
ordered_packages
|
|
}
|
|
|
|
/// Assemble filesystem with proper package ordering
|
|
pub async fn assemble_with_ordering(
|
|
&self,
|
|
base_commit: &str,
|
|
packages: &[DebPackageMetadata],
|
|
target_deployment: &str,
|
|
) -> AptOstreeResult<()> {
|
|
info!("Assembling filesystem with proper package ordering");
|
|
|
|
// Determine layering order
|
|
let ordered_package_names = self.determine_layering_order(packages);
|
|
|
|
// Convert package names to commit IDs (simplified)
|
|
let package_commits: Vec<String> = ordered_package_names
|
|
.iter()
|
|
.map(|name| format!("pkg_{}", name.replace("-", "_")))
|
|
.collect();
|
|
|
|
// Assemble filesystem
|
|
self.assembler.assemble_filesystem(base_commit, &package_commits, target_deployment).await?;
|
|
|
|
info!("Filesystem assembly with ordering completed");
|
|
Ok(())
|
|
}
|
|
}
|