use std::path::{Path, PathBuf};
use arcbox_image::machine_image::MachineImageManifest;
use chrono::Utc;
use super::archive::{self, ArchiveManifest, ArchivedMachine, FORMAT_VERSION};
use super::clone::{DATA_DISK, stopped_data_disk};
use super::{MachineConfig, MachineInfo, MachineManager, clone_file, reserve_hostname};
use crate::error::{EngineError, Result};
pub(super) const EXPORT_STAGING_PREFIX: &str = ".export-";
pub(super) const IMPORT_STAGING_PREFIX: &str = ".import-";
pub(super) enum DataDisk {
Sparse,
Staged(PathBuf),
}
impl MachineManager {
pub fn export(&self, name: &str, path: &Path, image: MachineImageManifest) -> Result<u64> {
let (snapshot, machine) = {
let machines = self
.machines
.write()
.map_err(|_| EngineError::LockPoisoned)?;
let machine = machines
.get(name)
.ok_or_else(|| EngineError::not_found(name.to_owned()))?;
let disk = stopped_data_disk(machine)?;
std::fs::create_dir_all(&self.machines_dir)?;
let snapshot = tempfile::Builder::new()
.prefix(EXPORT_STAGING_PREFIX)
.tempdir_in(&self.machines_dir)?;
clone_file(&disk, &snapshot.path().join(DATA_DISK))?;
(snapshot, ArchivedMachine::from(machine))
};
let manifest = ArchiveManifest {
format_version: FORMAT_VERSION,
arcbox_version: env!("CARGO_PKG_VERSION").to_owned(),
exported_at: Utc::now(),
machine,
image,
};
let size = archive::write(path, &manifest, &snapshot.path().join(DATA_DISK))?;
tracing::info!(machine = name, path = %path.display(), size, "exported machine");
Ok(size)
}
pub fn import(&self, config: MachineConfig, path: &Path) -> Result<String> {
self.ensure_name_available(&config.name)?;
std::fs::create_dir_all(&self.machines_dir)?;
let staging = tempfile::Builder::new()
.prefix(IMPORT_STAGING_PREFIX)
.tempdir_in(&self.machines_dir)?;
let staged = staging.path().join(DATA_DISK);
archive::extract_data_disk(path, &staged)?;
let name = self.create_machine(config, DataDisk::Staged(staged))?;
tracing::info!(machine = %name, path = %path.display(), "imported machine");
Ok(name)
}
pub fn ensure_name_available(&self, name: &str) -> Result<()> {
let machines = self
.machines
.read()
.map_err(|_| EngineError::LockPoisoned)?;
reserve_hostname(&machines, name).map(|_| ())
}
pub(super) fn sweep_staging(machines_dir: &Path) {
let Ok(entries) = std::fs::read_dir(machines_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !(name.starts_with(EXPORT_STAGING_PREFIX) || name.starts_with(IMPORT_STAGING_PREFIX))
{
continue;
}
let path = entry.path();
match std::fs::remove_dir_all(&path) {
Ok(()) => {
tracing::info!(path = %path.display(), "removed a stale staging directory");
}
Err(e) => {
tracing::warn!(path = %path.display(), error = %e, "could not remove a stale staging directory");
}
}
}
}
}
impl From<&MachineInfo> for ArchivedMachine {
fn from(machine: &MachineInfo) -> Self {
Self {
name: machine.name.clone(),
cpus: machine.cpus,
memory_mb: machine.memory_mb,
disk_gb: machine.disk_gb,
distro: machine.distro.clone().unwrap_or_default(),
distro_version: machine.distro_version.clone(),
mounts: machine.mounts.clone(),
}
}
}