1use anyhow::{Context, Result};
2use borsh::BorshSerialize;
3use sha2::{Digest, Sha256};
4
5use crate::api::ApiClient;
6use crate::config::{self, NetworkConfig};
7use crate::near::NearSigner;
8
9const CHUNK_SIZE: usize = 1 << 20; #[derive(BorshSerialize)]
21struct FastfsFileContent {
22 mime_type: String,
23 content: Vec<u8>,
24}
25
26#[derive(BorshSerialize)]
27struct SimpleFastfs {
28 relative_path: String,
29 content: Option<FastfsFileContent>,
30}
31
32#[derive(BorshSerialize)]
33struct PartialFastfs {
34 relative_path: String,
35 offset: u32,
36 full_size: u32,
37 mime_type: String,
38 content_chunk: Vec<u8>,
39 nonce: u32,
40}
41
42fn encode_simple(relative_path: &str, mime_type: &str, content: &[u8]) -> Vec<u8> {
44 let simple = SimpleFastfs {
45 relative_path: relative_path.to_string(),
46 content: Some(FastfsFileContent {
47 mime_type: mime_type.to_string(),
48 content: content.to_vec(),
49 }),
50 };
51
52 let mut data = vec![0u8];
54 data.extend(borsh::to_vec(&simple).expect("borsh serialization"));
55 data
56}
57
58fn encode_partial(
60 relative_path: &str,
61 offset: u32,
62 full_size: u32,
63 mime_type: &str,
64 chunk: &[u8],
65 nonce: u32,
66) -> Vec<u8> {
67 let partial = PartialFastfs {
68 relative_path: relative_path.to_string(),
69 offset,
70 full_size,
71 mime_type: mime_type.to_string(),
72 content_chunk: chunk.to_vec(),
73 nonce,
74 };
75
76 let mut data = vec![1u8];
78 data.extend(borsh::to_vec(&partial).expect("borsh serialization"));
79 data
80}
81
82fn prepare_fastfs_payloads(relative_path: &str, mime_type: &str, content: &[u8]) -> Vec<Vec<u8>> {
84 if content.len() <= CHUNK_SIZE {
85 return vec![encode_simple(relative_path, mime_type, content)];
86 }
87
88 let nonce = (std::time::SystemTime::now()
89 .duration_since(std::time::UNIX_EPOCH)
90 .unwrap()
91 .as_secs()
92 - 1_769_376_240) as u32;
93
94 let full_size = content.len() as u32;
95 let mut payloads = Vec::new();
96
97 let mut offset = 0usize;
98 while offset < content.len() {
99 let end = (offset + CHUNK_SIZE).min(content.len());
100 let chunk = &content[offset..end];
101 payloads.push(encode_partial(
102 relative_path,
103 offset as u32,
104 full_size,
105 mime_type,
106 chunk,
107 nonce,
108 ));
109 offset = end;
110 }
111
112 payloads
113}
114
115pub async fn upload(
117 network: &NetworkConfig,
118 file_path: &str,
119 receiver: Option<String>,
120 mime_type: Option<String>,
121) -> Result<()> {
122 let creds = config::load_credentials(network)?;
123
124 let content = std::fs::read(file_path)
126 .with_context(|| format!("Failed to read file: {file_path}"))?;
127
128 let hash = hex::encode(Sha256::digest(&content));
130
131 let extension = std::path::Path::new(file_path)
133 .extension()
134 .and_then(|e| e.to_str())
135 .unwrap_or("bin");
136 let mime = mime_type.unwrap_or_else(|| match extension {
137 "wasm" => "application/wasm".to_string(),
138 "json" => "application/json".to_string(),
139 "html" => "text/html".to_string(),
140 "js" => "application/javascript".to_string(),
141 "css" => "text/css".to_string(),
142 "png" => "image/png".to_string(),
143 "jpg" | "jpeg" => "image/jpeg".to_string(),
144 "svg" => "image/svg+xml".to_string(),
145 _ => "application/octet-stream".to_string(),
146 });
147
148 let relative_path = format!("{hash}.{extension}");
149 let receiver_id = receiver.unwrap_or_else(|| network.contract_id.clone());
150
151 eprintln!("Uploading to FastFS...");
152 eprintln!(" File: {file_path}");
153 eprintln!(" Size: {} bytes", content.len());
154 eprintln!(" SHA256: {hash}");
155 eprintln!(" Sender: {}", creds.account_id);
156 eprintln!(" Receiver: {receiver_id}");
157
158 let payloads = prepare_fastfs_payloads(&relative_path, &mime, &content);
159 let num_chunks = payloads.len();
160
161 if num_chunks > 1 {
162 eprintln!(" Chunks: {num_chunks} x {}KB max", CHUNK_SIZE / 1024);
163 }
164 eprintln!();
165
166 if creds.is_wallet_key() {
167 upload_via_wallet(network, &creds, &receiver_id, &payloads).await?;
168 } else {
169 upload_via_near_key(network, &creds, &receiver_id, &payloads).await?;
170 }
171
172 let fastfs_url = format!(
173 "https://main.fastfs.io/{}/{}/{}",
174 creds.account_id, receiver_id, relative_path
175 );
176
177 eprintln!();
178 eprintln!("Upload complete!");
179 eprintln!();
180 eprintln!("FastFS URL: {fastfs_url}");
181 eprintln!();
182
183 if extension == "wasm" {
184 eprintln!("Run directly:");
185 eprintln!(" outlayer run --wasm {fastfs_url} '{{}}' ");
186 eprintln!();
187 eprintln!("Or deploy as project version:");
188 eprintln!(" outlayer deploy <name> --wasm-url {fastfs_url}");
189 }
190
191 Ok(())
192}
193
194async fn upload_via_near_key(
196 network: &NetworkConfig,
197 creds: &config::Credentials,
198 receiver_id: &str,
199 payloads: &[Vec<u8>],
200) -> Result<()> {
201 let private_key = config::load_private_key(&network.network_id, &creds.account_id, creds)?;
202 let receiver_account_id: near_primitives::types::AccountId = receiver_id
203 .parse()
204 .context("Invalid receiver account ID")?;
205 let signer = NearSigner::new(network, &creds.account_id, &private_key)?;
206 let (current_nonce, block_hash) = signer.get_tx_context().await?;
207 let num_chunks = payloads.len();
208
209 for (i, payload) in payloads.iter().enumerate() {
210 if num_chunks > 1 {
211 eprint!(" Uploading chunk {}/{}... ", i + 1, num_chunks);
212 } else {
213 eprint!(" Uploading... ");
214 }
215
216 let tx_hash = signer
217 .send_function_call_async(
218 &receiver_account_id,
219 "__fastdata_fastfs",
220 payload.clone(),
221 1, 0, current_nonce + 1 + i as u64,
224 block_hash,
225 )
226 .await
227 .with_context(|| format!("Failed to upload chunk {}/{}", i + 1, num_chunks))?;
228
229 eprintln!("tx: {tx_hash}");
230 }
231
232 Ok(())
233}
234
235async fn upload_via_wallet(
237 network: &NetworkConfig,
238 creds: &config::Credentials,
239 receiver_id: &str,
240 payloads: &[Vec<u8>],
241) -> Result<()> {
242 let wallet_key = creds
243 .wallet_key
244 .as_ref()
245 .context("Missing wallet_key in credentials")?;
246 let api = ApiClient::new(network);
247 let num_chunks = payloads.len();
248
249 for (i, payload) in payloads.iter().enumerate() {
250 if num_chunks > 1 {
251 eprint!(" Uploading chunk {}/{}... ", i + 1, num_chunks);
252 } else {
253 eprint!(" Uploading... ");
254 }
255
256 let resp = api
257 .wallet_call_raw(
258 wallet_key,
259 receiver_id,
260 "__fastdata_fastfs",
261 payload,
262 1, 0, )
265 .await
266 .with_context(|| format!("Failed to upload chunk {}/{}", i + 1, num_chunks))?;
267
268 if let Some(tx_hash) = &resp.tx_hash {
269 eprintln!("tx: {tx_hash}");
270 } else {
271 eprintln!("request: {}", resp.request_id);
272 }
273 }
274
275 Ok(())
276}