olai_stack_state/
transfer.rs1use std::collections::BTreeMap;
5use std::path::{Component, Path};
6
7use olai_stack_topology::{EnvManifest, MAX_SLOT, WORKSPACE_FILE, WorkspaceEnv, WorkspaceManifest};
8
9use crate::error::{Error, Result};
10use crate::model::{Draft, EnvironmentId, InstanceId, Revision, Workspace, WorkspaceId};
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<Workspace> = 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 first = workspace.get_or_insert_with(|| ws.clone());
56 if first.id != ws.id {
57 return Err(Error::Export(format!(
58 "the instances belong to two workspaces, `{}` and `{}`",
59 first.name, ws.name
60 )));
61 }
62 let slot = instance.slot.ok_or_else(|| {
63 Error::Export(format!("instance {id} holds no slot; lease one first"))
64 })?;
65 let revision = self.revision(&instance.environment, instance.revision)?;
66 members.push((env.name, slot, revision));
67 }
68 let Some(workspace) = workspace else {
69 return Err(Error::Export("no instances given".into()));
70 };
71 write_workspace(&workspace, &members, dir)
72 }
73
74 pub fn export_workspace(&self, workspace: &WorkspaceId, dir: &Path) -> Result<()> {
79 let ws = self.workspace_by_id(workspace)?;
80 let mut members = Vec::new();
81 let mut unslotted = Vec::new();
82 for env in self.environments(workspace)? {
83 let head = env.head.ok_or_else(|| {
84 Error::Export(format!(
85 "environment `{}` has no revision; commit it first",
86 env.name
87 ))
88 })?;
89 let revision = self.revision(&env.id, head)?;
90 let slot = self
91 .instances_of(&env.id)?
92 .into_iter()
93 .filter(|i| i.revision == head)
94 .find_map(|i| i.slot);
95 match slot {
96 Some(slot) => members.push((env.name, slot, revision)),
97 None => unslotted.push((env.name, revision)),
98 }
99 }
100 for (name, revision) in unslotted {
101 let slot = (0..=MAX_SLOT)
102 .find(|s| members.iter().all(|(_, taken, _)| taken != s))
103 .ok_or(Error::SlotsExhausted)?;
104 members.push((name, slot, revision));
105 }
106 members.sort_by(|a, b| a.0.cmp(&b.0));
107 write_workspace(&ws, &members, dir)
108 }
109
110 pub fn export_revision(&self, env: &EnvironmentId, number: u32, dir: &Path) -> Result<()> {
114 write_env_dir(&self.revision(env, number)?, dir)
115 }
116}
117
118fn write_workspace(ws: &Workspace, members: &[(String, u16, Revision)], dir: &Path) -> Result<()> {
121 let manifest = WorkspaceManifest {
122 version: WorkspaceManifest::current_version(),
123 name: ws.name.clone(),
124 mesh: ws.mesh,
125 envs: members
126 .iter()
127 .map(|(name, slot, _)| WorkspaceEnv {
128 name: name.clone(),
129 path: name.clone(),
130 slot: *slot,
131 })
132 .collect(),
133 };
134 manifest.validate()?;
136 for (name, _, revision) in members {
137 write_env_dir(revision, &dir.join(name))?;
138 }
139 write(&dir.join(WORKSPACE_FILE), &manifest.to_toml()?)
140}
141
142fn write_env_dir(revision: &Revision, dir: &Path) -> Result<()> {
144 for (rel, text) in revision.modules() {
145 write(&dir.join(safe_relative(rel)?), text)?;
146 }
147 write(&dir.join(ENV_FILE), revision.manifest_toml())
148}
149
150pub fn read_env_dir(dir: &Path) -> Result<Draft> {
152 let manifest = EnvManifest::read_from(&dir.join(ENV_FILE))?;
153 let mut modules = BTreeMap::new();
154 for rel in &manifest.catalog.manifests {
155 let path = dir.join(rel);
156 let text = std::fs::read_to_string(&path).map_err(|e| Error::io(&path, e))?;
157 modules.insert(rel.clone(), text);
158 }
159 Ok(Draft {
160 manifest,
161 modules,
162 vars: Default::default(),
163 })
164}
165
166fn safe_relative(rel: &str) -> Result<&Path> {
168 let path = Path::new(rel);
169 if path
170 .components()
171 .all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
172 {
173 Ok(path)
174 } else {
175 Err(Error::Export(format!(
176 "module manifest path `{rel}` leaves the environment directory"
177 )))
178 }
179}
180
181fn write(path: &Path, contents: &str) -> Result<()> {
182 if let Some(parent) = path.parent() {
183 std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
184 }
185 std::fs::write(path, contents).map_err(|e| Error::io(path, e))
186}