conduit_cli/core/engine/
store.rs1use sha1::Sha1;
2use sha2::{Digest, Sha256, Sha512};
3use std::path::{Path, PathBuf};
4use tokio::fs;
5use tokio::io::AsyncReadExt;
6
7use crate::core::schemas::lock::HashKind;
8use crate::errors::ConduitResult;
9
10#[derive(Clone, Debug)]
11pub struct Store {
12 root: PathBuf,
13}
14
15impl Store {
16 pub fn new(root: PathBuf) -> Self {
17 Self { root }
18 }
19
20 pub fn object_path(&self, hash: &str, kind: HashKind) -> PathBuf {
21 let prefix = match kind {
22 HashKind::Sha1 => "sha1",
23 HashKind::Sha256 => "sha256",
24 HashKind::Sha512 => "sha512",
25 };
26 self.root
27 .join("objects")
28 .join(prefix)
29 .join(&hash[..2])
30 .join(hash)
31 }
32
33 pub async fn calculate_hash<P: AsRef<Path>>(
34 &self,
35 path: P,
36 kind: HashKind,
37 ) -> ConduitResult<String> {
38 let mut file = fs::File::open(path).await?;
39 let mut buffer = vec![0u8; 65536].into_boxed_slice();
40
41 match kind {
42 HashKind::Sha1 => {
43 let mut hasher = Sha1::new();
44 loop {
45 let n = file.read(&mut buffer).await?;
46 if n == 0 {
47 break;
48 }
49 hasher.update(&buffer[..n]);
50 }
51 Ok(format!("{:x}", hasher.finalize()))
52 }
53 HashKind::Sha256 => {
54 let mut hasher = Sha256::new();
55 loop {
56 let n = file.read(&mut buffer).await?;
57 if n == 0 {
58 break;
59 }
60 hasher.update(&buffer[..n]);
61 }
62 Ok(format!("{:x}", hasher.finalize()))
63 }
64 HashKind::Sha512 => {
65 let mut hasher = Sha512::new();
66 loop {
67 let n = file.read(&mut buffer).await?;
68 if n == 0 {
69 break;
70 }
71 hasher.update(&buffer[..n]);
72 }
73 Ok(format!("{:x}", hasher.finalize()))
74 }
75 }
76 }
77
78 pub async fn add_file<P: AsRef<Path>>(
79 &self,
80 source: P,
81 hash: &str,
82 kind: HashKind,
83 ) -> ConduitResult<()> {
84 let target = self.object_path(hash, kind);
85
86 if target.exists() {
87 return Ok(());
88 }
89
90 if let Some(parent) = target.parent() {
91 let _ = fs::create_dir_all(parent).await;
92 }
93
94 let temp_target = target.with_extension(format!("{}.tmp", uuid::Uuid::new_v4()));
95
96 fs::copy(&source, &temp_target).await?;
97
98 if let Err(e) = fs::rename(&temp_target, &target).await {
99 let _ = fs::remove_file(&temp_target).await;
100 if !target.exists() {
101 return Err(e.into());
102 }
103 }
104
105 Ok(())
106 }
107
108 pub async fn link_object<P: AsRef<Path>>(
109 &self,
110 hash: &str,
111 kind: HashKind,
112 target: P,
113 ) -> ConduitResult<()> {
114 let source = self.object_path(hash, kind);
115
116 if let Some(parent) = target.as_ref().parent() {
117 fs::create_dir_all(parent).await?;
118 }
119
120 if target.as_ref().exists() {
121 fs::remove_file(&target).await?;
122 }
123
124 fs::hard_link(source, target).await?;
125 Ok(())
126 }
127
128 pub fn get_project_root(&self) -> PathBuf {
129 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
130 }
131
132 pub fn get_mods_path(&self) -> PathBuf {
133 self.get_project_root().join("mods")
134 }
135
136 pub fn get_plugins_path(&self) -> PathBuf {
137 self.get_project_root().join("plugins")
138 }
139
140 pub fn get_world_path(&self) -> PathBuf {
141 self.get_project_root().join("world")
142 }
143
144 pub async fn install_to_project(
145 &self,
146 hash: &str,
147 kind: HashKind,
148 rel_path: PathBuf,
149 ) -> ConduitResult<()> {
150 let target = self.get_project_root().join(rel_path);
151 self.link_object(hash, kind, target).await
152 }
153}