olai_stack_state/
transfer.rs1use std::collections::BTreeMap;
5use std::path::{Component, Path};
6
7use olai_stack_topology::{EnvManifest, WORKSPACE_FILE, WorkspaceEnv, WorkspaceManifest};
8
9use crate::error::{Error, Result};
10use crate::model::{Draft, InstanceId, Workspace};
11use crate::store::Store;
12
13pub const ENV_FILE: &str = "env.toml";
15
16impl Store {
17 pub fn import_workspace(&self, dir: &Path) -> Result<Workspace> {
24 let path = dir.join(WORKSPACE_FILE);
25 let manifest = WorkspaceManifest::read_from(&path)?;
26 let workspace = self.create_workspace(&manifest.name, manifest.mesh)?;
27 let imported = (|| {
28 for member in &manifest.envs {
29 let draft = read_env_dir(&dir.join(&member.path))?;
30 let env = self.create_environment(&workspace.id, &member.name, draft)?;
31 self.commit_draft(&env.id)?;
32 }
33 Ok(())
34 })();
35 if let Err(e) = imported {
36 let _ = self.delete_workspace(&workspace.id);
38 return Err(e);
39 }
40 Ok(workspace)
41 }
42
43 pub fn export_instances(&self, instances: &[InstanceId], dir: &Path) -> Result<()> {
49 let mut workspace: Option<WorkspaceManifest> = None;
50 let mut members = Vec::new();
51 for id in instances {
52 let instance = self.instance(id)?;
53 let env = self.environment(&instance.environment)?;
54 let ws = self.workspace_by_id(&env.workspace)?;
55 let manifest = workspace.get_or_insert_with(|| WorkspaceManifest {
56 version: WorkspaceManifest::current_version(),
57 name: ws.name.clone(),
58 mesh: ws.mesh,
59 envs: Vec::new(),
60 });
61 if manifest.name != ws.name {
62 return Err(Error::Export(format!(
63 "the instances belong to two workspaces, `{}` and `{}`",
64 manifest.name, ws.name
65 )));
66 }
67 let slot = instance.slot.ok_or_else(|| {
68 Error::Export(format!("instance {id} holds no slot; lease one first"))
69 })?;
70 manifest.envs.push(WorkspaceEnv {
71 name: env.name.clone(),
72 path: env.name.clone(),
73 slot,
74 });
75 members.push((
76 env.name,
77 self.revision(&instance.environment, instance.revision)?,
78 ));
79 }
80 let Some(workspace) = workspace else {
81 return Err(Error::Export("no instances given".into()));
82 };
83 workspace.validate()?;
85
86 for (name, revision) in &members {
87 let env_dir = dir.join(name);
88 for (rel, text) in revision.modules() {
89 write(&env_dir.join(safe_relative(rel)?), text)?;
90 }
91 write(&env_dir.join(ENV_FILE), revision.manifest_toml())?;
92 }
93 write(&dir.join(WORKSPACE_FILE), &workspace.to_toml()?)
94 }
95}
96
97pub fn read_env_dir(dir: &Path) -> Result<Draft> {
99 let manifest = EnvManifest::read_from(&dir.join(ENV_FILE))?;
100 let mut modules = BTreeMap::new();
101 for rel in &manifest.catalog.manifests {
102 let path = dir.join(rel);
103 let text = std::fs::read_to_string(&path).map_err(|e| Error::io(&path, e))?;
104 modules.insert(rel.clone(), text);
105 }
106 Ok(Draft {
107 manifest,
108 modules,
109 vars: Default::default(),
110 })
111}
112
113fn safe_relative(rel: &str) -> Result<&Path> {
115 let path = Path::new(rel);
116 if path
117 .components()
118 .all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
119 {
120 Ok(path)
121 } else {
122 Err(Error::Export(format!(
123 "module manifest path `{rel}` leaves the environment directory"
124 )))
125 }
126}
127
128fn write(path: &Path, contents: &str) -> Result<()> {
129 if let Some(parent) = path.parent() {
130 std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
131 }
132 std::fs::write(path, contents).map_err(|e| Error::io(path, e))
133}