signer_remote/storage/
client.rs1use std::{io::SeekFrom, path::PathBuf};
3
4use base64::Engine as _;
5use futures::stream::{self, StreamExt, TryStreamExt};
6use sha3::Digest;
7use signer_core::{SignerKeys, SignerUser};
8use tokio::io::{AsyncReadExt as _, AsyncSeekExt, AsyncWriteExt, BufReader};
9
10use crate::{
11 error::{RemoteError, RemoteResult},
12 remote::{HttpClient, HttpClientConfig},
13 storage::{
14 StorageBlobsCheckExistenceRequest, StorageBlobsCheckExistenceResponse, StorageItemData,
15 StorageItemResponse,
16 },
17};
18
19pub struct SignerDaemonStorage {
20 addr: String,
21 keys: SignerKeys,
22 user: SignerUser,
23}
24
25#[derive(Debug, Clone)]
26struct ChunkDescriptor {
27 hash: String,
28 offset: u64,
29 length: u64,
30}
31
32impl SignerDaemonStorage {
33 pub fn new(addr: String, keys: SignerKeys, user: SignerUser) -> Self {
34 Self { addr, keys, user }
35 }
36
37 pub async fn upload(&self, path: PathBuf) -> RemoteResult<StorageItemResponse> {
38 let file = tokio::fs::File::open(&path)
39 .await
40 .map_err(RemoteError::Io)?;
41 let mut reader = BufReader::new(file);
42
43 let mut chunk_descriptors = Vec::new();
45 let mut current_offset = 0;
46
47 loop {
48 let mut buffer = vec![0; 16 * 1024 * 1024];
49 let n = reader.read(&mut buffer).await.map_err(RemoteError::Io)?;
50 if n == 0 {
51 break;
52 }
53 buffer.truncate(n);
54
55 let mut hasher = sha3::Sha3_256::new();
56 hasher.update(&buffer);
57 let hash = base64::prelude::BASE64_URL_SAFE.encode(hasher.finalize().to_vec());
58
59 chunk_descriptors.push(ChunkDescriptor {
60 hash,
61 offset: current_offset,
62 length: n as u64,
63 });
64
65 current_offset += n as u64;
66 }
67
68 let blob_hashes: Vec<String> = chunk_descriptors.iter().map(|d| d.hash.clone()).collect();
69
70 let mut item_hasher = sha3::Sha3_256::new();
72 for hash in &blob_hashes {
73 item_hasher.update(hash.as_bytes());
74 }
75 let item_hash = base64::prelude::BASE64_URL_SAFE.encode(item_hasher.finalize().to_vec());
76
77 let config = HttpClientConfig::new(self.keys.clone(), self.user.clone(), self.addr.clone());
78 let client = HttpClient::new(config);
79
80 let head_response = client
82 .head(&format!(
83 "/api/storage/item/{}/{}",
84 &self.user.pub_key, &item_hash
85 ))
86 .await;
87
88 match head_response {
89 Ok(response) => {
90 if response.status().is_success() {
91 return Ok(StorageItemResponse {
92 pubkey: self.user.pub_key.clone(),
93 hash: item_hash,
94 file_name: path.file_name().unwrap().to_str().unwrap().to_string(),
95 });
96 }
97 }
98 Err(_) => {
99 }
101 }
102
103 let create_response = client
105 .post_raw(
106 "/api/storage/item",
107 &StorageItemData {
108 chunks: blob_hashes.clone(),
109 file_name: path.file_name().unwrap().to_str().unwrap().to_string(),
110 },
111 )
112 .await;
113
114 match create_response {
115 Ok(response) => {
116 if response.status() == reqwest::StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS {
117 let item: StorageItemResponse = response
118 .json()
119 .await
120 .map_err(|e| RemoteError::Internal(e.to_string()))?;
121 return Ok(item);
122 }
123 }
124 Err(_) => {
125 }
127 }
128
129 let missing_blobs: StorageBlobsCheckExistenceResponse = client
131 .post(
132 "/api/storage/blobs/check-existence",
133 &StorageBlobsCheckExistenceRequest {
134 hashes: blob_hashes.clone(),
135 },
136 )
137 .await?;
138
139 let missing_hashes: std::collections::HashSet<String> =
140 missing_blobs.missing_hashes.into_iter().collect();
141
142 let descriptors_to_upload: Vec<ChunkDescriptor> = chunk_descriptors
144 .into_iter()
145 .filter(|d| missing_hashes.contains(&d.hash))
146 .collect();
147
148 let upload_futs = descriptors_to_upload.into_iter().map(|desc| {
149 let keys = self.keys.clone();
150 let user = self.user.clone();
151 let addr = self.addr.clone();
152 let path = path.clone();
153 async move {
154 let mut file = tokio::fs::File::open(&path)
155 .await
156 .map_err(RemoteError::Io)?;
157 file.seek(SeekFrom::Start(desc.offset))
158 .await
159 .map_err(RemoteError::Io)?;
160 let mut chunk_reader = file.take(desc.length);
161 let mut chunk_data = Vec::with_capacity(desc.length as usize);
162 chunk_reader
163 .read_to_end(&mut chunk_data)
164 .await
165 .map_err(RemoteError::Io)?;
166
167 let config = HttpClientConfig::new(keys, user, addr.clone());
168 let client = HttpClient::new(config);
169
170 let file_data = reqwest::multipart::Part::bytes(chunk_data);
171 let form_data = reqwest::multipart::Form::new().part("blob", file_data);
172
173 client
174 .post_multipart("/api/storage/blob", form_data)
175 .await?
176 .error_for_status()
177 .map_err(RemoteError::Network)?;
178
179 let result: RemoteResult<()> = Ok(());
180 result
181 }
182 });
183
184 stream::iter(upload_futs)
185 .buffer_unordered(10)
186 .try_collect::<Vec<()>>()
187 .await?;
188
189 let item: StorageItemResponse = client
191 .post(
192 "/api/storage/item",
193 &StorageItemData {
194 chunks: blob_hashes,
195 file_name: path.file_name().unwrap().to_str().unwrap().to_string(),
196 },
197 )
198 .await?;
199
200 Ok(item)
201 }
202
203 pub async fn download(&self, key: StorageItemResponse, path: PathBuf) -> RemoteResult<()> {
204 let config = HttpClientConfig::new(self.keys.clone(), self.user.clone(), self.addr.clone());
205 let client = HttpClient::new(config);
206
207 let r = client
208 .get_raw(&format!("/api/storage/item/{}/{}", &key.pubkey, &key.hash))
209 .await?
210 .error_for_status()
211 .map_err(RemoteError::Network)?;
212
213 let mut f = tokio::fs::File::create(path)
214 .await
215 .map_err(RemoteError::Io)?;
216 let mut response_stream = r.bytes_stream();
217 while let Some(chunk) = response_stream
218 .try_next()
219 .await
220 .map_err(RemoteError::Network)?
221 {
222 f.write_all(&chunk).await.map_err(RemoteError::Io)?;
223 }
224
225 Ok(())
226 }
227}