use crate::error::{Result, StorageError};
use cap_std::ambient_authority;
use cap_std::fs::Dir;
use std::env;
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Storage {
root_dir: Dir,
}
impl Storage {
pub fn open<P: AsRef<Path>>(root: P) -> Result<Self> {
let root_path = root.as_ref();
let root_dir = Dir::open_ambient_dir(root_path, ambient_authority()).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StorageError::RootNotFound(root_path.to_path_buf())
} else {
StorageError::Io(e)
}
})?;
Self::validate_storage(&root_dir)?;
Ok(Self { root_dir })
}
pub fn discover() -> Result<Self> {
let search_paths = Self::default_search_paths();
for path in search_paths {
if path.exists() {
match Self::open(&path) {
Ok(storage) => return Ok(storage),
Err(_) => continue,
}
}
}
Err(StorageError::InvalidStorage(
"No valid storage location found. Searched default locations.".to_string(),
))
}
pub fn discover_all() -> Result<Vec<Self>> {
let mut stores = Vec::new();
if let Ok(primary) = Self::discover() {
stores.push(primary);
}
stores.extend(Self::additional_image_stores_from_env());
if stores.is_empty() {
return Err(StorageError::InvalidStorage(
"No valid storage location found. Searched default locations and $STORAGE_OPTS."
.to_string(),
));
}
Ok(stores)
}
fn additional_image_stores_from_env() -> Vec<Self> {
let opts = match env::var("STORAGE_OPTS") {
Ok(v) => v,
Err(_) => return Vec::new(),
};
Self::parse_additional_image_stores(&opts)
}
fn parse_additional_image_stores(opts: &str) -> Vec<Self> {
let mut stores = Vec::new();
for item in opts.split(',') {
let item = item.trim();
if let Some(path) = item.strip_prefix("additionalimagestore=")
&& let Ok(s) = Self::open(path)
{
stores.push(s);
}
}
stores
}
fn default_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(root) = env::var("CONTAINERS_STORAGE_ROOT") {
paths.push(PathBuf::from(root));
}
if let Ok(home) = env::var("HOME") {
let home_path = PathBuf::from(home);
if let Ok(xdg_data) = env::var("XDG_DATA_HOME") {
paths.push(PathBuf::from(xdg_data).join("containers/storage"));
}
paths.push(home_path.join(".local/share/containers/storage"));
}
paths.push(PathBuf::from("/var/lib/containers/storage"));
paths
}
fn validate_storage(root_dir: &Dir) -> Result<()> {
let required_dirs = ["overlay", "overlay-layers", "overlay-images"];
for dir_name in &required_dirs {
match root_dir.try_exists(dir_name) {
Ok(exists) if !exists => {
return Err(StorageError::InvalidStorage(format!(
"Missing required directory: {}",
dir_name
)));
}
Err(e) => return Err(StorageError::Io(e)),
_ => {}
}
}
Ok(())
}
pub fn from_root_dir(root_dir: Dir) -> Result<Self> {
Self::validate_storage(&root_dir)?;
Ok(Self { root_dir })
}
pub fn root_dir(&self) -> &Dir {
&self.root_dir
}
pub fn resolve_link(&self, link_id: &str) -> Result<String> {
let overlay_dir = self.root_dir.open_dir("overlay")?;
let link_dir = overlay_dir.open_dir("l")?;
let target = link_dir.read_link(link_id).map_err(|e| {
StorageError::LinkReadError(format!("Failed to read link {}: {}", link_id, e))
})?;
Self::extract_layer_id_from_link(&target)
}
fn extract_layer_id_from_link(target: &Path) -> Result<String> {
let target_str = target.to_str().ok_or_else(|| {
StorageError::LinkReadError("Invalid UTF-8 in link target".to_string())
})?;
let components: Vec<&str> = target_str.split('/').collect();
if components.len() >= 2 {
let layer_id = components[components.len() - 2];
if !layer_id.is_empty() && layer_id != ".." {
return Ok(layer_id.to_string());
}
}
Err(StorageError::LinkReadError(format!(
"Invalid link target format: {}",
target_str
)))
}
pub fn list_images(&self) -> Result<Vec<crate::image::Image>> {
use crate::image::Image;
let images_dir = self.root_dir.open_dir("overlay-images")?;
let mut images = Vec::new();
for entry in images_dir.entries()? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let id = entry
.file_name()
.to_str()
.ok_or_else(|| {
StorageError::InvalidStorage(
"Invalid UTF-8 in image directory name".to_string(),
)
})?
.to_string();
images.push(Image::open(self, &id)?);
}
}
Ok(images)
}
pub fn get_image(&self, id: &str) -> Result<crate::image::Image> {
crate::image::Image::open(self, id)
}
pub fn get_image_layers(
&self,
image: &crate::image::Image,
) -> Result<Vec<crate::layer::Layer>> {
use crate::layer::Layer;
let diff_ids = image.layers()?;
let layer_ids: Vec<String> = self
.resolve_diff_ids(&diff_ids)?
.into_iter()
.enumerate()
.map(|(i, opt)| opt.ok_or_else(|| StorageError::LayerNotFound(diff_ids[i].clone())))
.collect::<Result<_>>()?;
layer_ids
.iter()
.map(|layer_id| Layer::open(self, layer_id))
.collect()
}
pub fn find_image_by_name(&self, name: &str) -> Result<crate::image::Image> {
let images_dir = self.root_dir.open_dir("overlay-images")?;
let mut file = images_dir.open("images.json")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let entries: Vec<ImageJsonEntry> = serde_json::from_str(&contents)
.map_err(|e| StorageError::InvalidStorage(format!("Invalid images.json: {}", e)))?;
for entry in &entries {
if let Some(names) = &entry.names {
for image_name in names {
if image_name == name {
return self.get_image(&entry.id);
}
}
}
}
for entry in &entries {
if let Some(names) = &entry.names {
for image_name in names {
if let Some(prefix) = image_name.strip_suffix(name) {
if prefix.is_empty() || prefix.ends_with('/') {
return self.get_image(&entry.id);
}
}
}
}
}
let name_with_tag = if name.contains(':') {
name.to_string()
} else {
format!("{}:latest", name)
};
for entry in &entries {
if let Some(names) = &entry.names {
for image_name in names {
if let Some(prefix) = image_name.strip_suffix(&name_with_tag)
&& (prefix.is_empty() || prefix.ends_with('/'))
{
return self.get_image(&entry.id);
}
}
}
}
Err(StorageError::ImageNotFound(name.to_string()))
}
fn read_layer_entries(&self) -> Result<Vec<LayerEntry>> {
let layers_dir = self.root_dir.open_dir("overlay-layers").map_err(|e| {
StorageError::Io(std::io::Error::new(
e.kind(),
format!("opening overlay-layers/: {e}"),
))
})?;
let mut file = layers_dir.open("layers.json").map_err(|e| {
StorageError::Io(std::io::Error::new(
e.kind(),
format!("opening overlay-layers/layers.json: {e}"),
))
})?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
serde_json::from_str(&contents)
.map_err(|e| StorageError::InvalidStorage(format!("Invalid layers.json: {}", e)))
}
pub fn resolve_diff_ids(&self, diff_digests: &[String]) -> Result<Vec<Option<String>>> {
let entries = self.read_layer_entries()?;
let mut digest_to_id = std::collections::HashMap::with_capacity(entries.len());
for entry in &entries {
if let Some(digest) = &entry.diff_digest {
digest_to_id.insert(digest.as_str(), entry.id.as_str());
}
}
Ok(diff_digests
.iter()
.map(|diff_digest| {
let normalized = if diff_digest.starts_with("sha256:") {
diff_digest.clone()
} else {
format!("sha256:{}", diff_digest)
};
digest_to_id
.get(normalized.as_str())
.map(|id| id.to_string())
})
.collect())
}
pub fn resolve_diff_id(&self, diff_digest: &str) -> Result<String> {
self.resolve_diff_ids(&[diff_digest.to_string()])?
.into_iter()
.next()
.flatten()
.ok_or_else(|| StorageError::LayerNotFound(diff_digest.to_string()))
}
pub fn get_layer_metadata(&self, layer_id: &str) -> Result<LayerMetadata> {
let entries = self.read_layer_entries()?;
for entry in entries {
if entry.id == layer_id {
return Ok(LayerMetadata {
id: entry.id,
parent: entry.parent,
diff_size: entry.diff_size,
compressed_size: entry.compressed_size,
});
}
}
Err(StorageError::LayerNotFound(layer_id.to_string()))
}
pub fn calculate_image_size(&self, image: &crate::image::Image) -> Result<u64> {
let layers = self.get_image_layers(image)?;
let mut total_size: u64 = 0;
for layer in &layers {
let metadata = self.get_layer_metadata(layer.id())?;
if let Some(size) = metadata.diff_size {
total_size = total_size.saturating_add(size);
}
}
Ok(total_size)
}
}
use crate::image::ImageJsonEntry;
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct LayerEntry {
id: String,
parent: Option<String>,
diff_digest: Option<String>,
diff_size: Option<u64>,
compressed_size: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct LayerMetadata {
pub id: String,
pub parent: Option<String>,
pub diff_size: Option<u64>,
pub compressed_size: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_search_paths() {
let paths = Storage::default_search_paths();
assert!(!paths.is_empty(), "Should have at least one search path");
}
#[test]
fn test_storage_validation() {
let dir = tempfile::tempdir().unwrap();
let storage_path = dir.path();
std::fs::create_dir_all(storage_path.join("overlay")).unwrap();
std::fs::create_dir_all(storage_path.join("overlay-layers")).unwrap();
std::fs::create_dir_all(storage_path.join("overlay-images")).unwrap();
let storage = Storage::open(storage_path).unwrap();
assert!(storage.root_dir().try_exists("overlay").unwrap());
}
fn create_mock_storage(path: &Path) {
for d in ["overlay", "overlay-layers", "overlay-images"] {
std::fs::create_dir_all(path.join(d)).unwrap();
}
}
#[test]
fn test_parse_additional_image_stores() {
let dir = tempfile::tempdir().unwrap();
let store_a = dir.path().join("a");
let store_b = dir.path().join("b");
create_mock_storage(&store_a);
create_mock_storage(&store_b);
assert!(Storage::parse_additional_image_stores("").is_empty());
let opts = format!("additionalimagestore={}", store_a.display());
let stores = Storage::parse_additional_image_stores(&opts);
assert_eq!(stores.len(), 1);
let opts = format!(
"additionalimagestore={},additionalimagestore={}",
store_a.display(),
store_b.display()
);
let stores = Storage::parse_additional_image_stores(&opts);
assert_eq!(stores.len(), 2);
assert!(
Storage::parse_additional_image_stores("additionalimagestore=/no/such/path").is_empty()
);
assert!(
Storage::parse_additional_image_stores("overlay.mount_program=/usr/bin/fuse-overlayfs")
.is_empty()
);
}
}