Skip to main content

supreme_fs/game_storage/online/
upload.rs

1//! The unified chunked-upload primitive used for both snapshot and content
2//! uploads, plus the server upload-limit query it depends on.
3
4use super::util::compute_chunk_size;
5use crate::game_storage::GameStorage;
6use crate::{HybridStorageError, SnapshotEvent};
7use sha2::{Digest, Sha256};
8use std::io;
9use std::sync::mpsc::Sender;
10
11impl GameStorage {
12    /// Get maximum upload size from server
13    pub fn get_max_upload_size(&self) -> Result<usize, HybridStorageError> {
14        if let Some(api_url) = &self.api_url {
15            let client = self
16                .client
17                .as_ref()
18                .ok_or_else(|| io::Error::other("Online storage not configured"))?;
19
20            let url = format!("{api_url}?action=get_upload_limits");
21            if let Ok(response) = client.get(&url).call() {
22                #[derive(serde::Deserialize)]
23                struct UploadLimits {
24                    max_upload_size: usize,
25                }
26
27                if let Ok(limits) = response.into_body().read_json::<UploadLimits>() {
28                    return Ok(limits.max_upload_size);
29                }
30            }
31        }
32
33        // Default fallback - assume 8MB limit
34        Ok(8 * 1024 * 1024)
35    }
36
37    /// Unified chunked upload for all files (single chunk for small files, multiple for large)
38    pub fn upload_file_unified(
39        &self,
40        subdirectory: &str,
41        data: &[u8],
42        file_type: &str, // "snapshot" or "content"
43        filename: Option<&str>,
44        tx: Option<Sender<SnapshotEvent>>,
45    ) -> Result<(), HybridStorageError> {
46        let client = self
47            .client
48            .as_ref()
49            .ok_or_else(|| io::Error::other("Online storage not configured"))?;
50        let api_url = self
51            .api_url
52            .as_ref()
53            .ok_or_else(|| io::Error::other("Online storage not configured"))?;
54
55        // Calculate Hash of the data (we need this for the final filename)
56        let hash = {
57            let mut hasher = Sha256::new();
58            hasher.update(data);
59            hex::encode(hasher.finalize())
60        };
61
62        let filename = match filename {
63            Some(f) => f.to_string(),
64            None => {
65                if file_type == "snapshot" {
66                    format!("{hash}.json")
67                } else {
68                    hash.clone()
69                }
70            }
71        };
72
73        // Get server upload limits to determine optimal chunk size
74        let max_upload_size = self.get_max_upload_size()?;
75        let chunk_size = compute_chunk_size(max_upload_size);
76
77        let total_chunks = data.len().div_ceil(chunk_size);
78
79        // Debug logging
80        if let Some(ref sender) = tx {
81            let _ = sender.send(SnapshotEvent::Log(format!(
82                "Data length: {} bytes, chunk size: {} bytes, total chunks: {}",
83                data.len(),
84                chunk_size,
85                total_chunks
86            )));
87        }
88
89        // Use SHA256 of the complete file as upload ID (unique and verifiable)
90        let upload_id = hash.clone();
91
92        // Send progress init if we have a sender
93        if let Some(ref sender) = tx {
94            let _ = sender.send(SnapshotEvent::InitProgress(total_chunks));
95            let _ = sender.send(SnapshotEvent::Log(format!(
96                "Uploading {total_chunks} chunks (chunk size: {chunk_size} bytes)"
97            )));
98        }
99
100        // Upload each chunk
101        for (chunk_index, chunk) in data.chunks(chunk_size).enumerate() {
102            // Calculate chunk hash for integrity verification
103            let chunk_hash = {
104                let mut hasher = Sha256::new();
105                hasher.update(chunk);
106                hex::encode(hasher.finalize())
107            };
108
109            // Send progress update
110            if let Some(ref sender) = tx {
111                let _ = sender.send(SnapshotEvent::SetStatus(format!(
112                    "Uploading chunk {}/{}",
113                    chunk_index + 1,
114                    total_chunks
115                )));
116            }
117
118            use super::multipart::{MultipartForm, SendMultipart};
119
120            let part_filename = format!("chunk_{chunk_index}_{chunk_hash}");
121            let mut form = MultipartForm::new()
122                .bytes(
123                    "chunk_file",
124                    &part_filename,
125                    "application/octet-stream",
126                    chunk,
127                )
128                .text("chunk_index", chunk_index.to_string())
129                .text("total_chunks", total_chunks.to_string())
130                .text("original_filename", filename.clone())
131                .text("upload_id", upload_id.clone())
132                .text("chunk_hash", chunk_hash)
133                .text("file_type", file_type);
134
135            if file_type == "content" {
136                form = form.text("hash", filename.clone());
137            }
138
139            if let Some(token) = &self.token {
140                form = form.text("api_token", token.clone());
141            }
142
143            let response = client.post(api_url).send_multipart(form)?;
144
145            if !response.status().is_success() {
146                return Err(HybridStorageError::UploadFailed(
147                    self.extract_error_message(response),
148                ));
149            }
150
151            // Send progress increment
152            if let Some(ref sender) = tx {
153                let _ = sender.send(SnapshotEvent::IncrementProgress);
154            }
155
156            // Check if this was the last chunk and processing is complete
157            #[derive(serde::Deserialize)]
158            struct ChunkResponse {
159                status: Option<String>,
160                missing_files: Option<Vec<String>>,
161            }
162
163            if let Ok(response_data) = response.into_body().read_json::<ChunkResponse>()
164                && response_data.status.as_deref() != Some("chunk_received")
165            {
166                // This was the last chunk and processing is complete
167                if file_type == "snapshot" {
168                    if let Some(missing) = response_data.missing_files {
169                        let total_missing = missing.len();
170
171                        // Notify user about missing content files
172                        if let Some(ref sender) = tx {
173                            let msg = format!(
174                                "Uploading {} missing content file{}...",
175                                total_missing,
176                                if total_missing == 1 { "" } else { "s" }
177                            );
178                            let _ = sender.send(SnapshotEvent::Log(msg.clone()));
179                            let _ = sender.send(SnapshotEvent::SetStatus(msg));
180                        }
181
182                        for (index, hash) in missing.into_iter().enumerate() {
183                            // Update status for each content file
184                            if let Some(ref sender) = tx {
185                                let _ = sender.send(SnapshotEvent::SetStatus(format!(
186                                    "Uploading content file {}/{}",
187                                    index + 1,
188                                    total_missing
189                                )));
190                            }
191
192                            // Upload missing content
193                            let content_path_xz = self.contents_path().join(format!("{hash}.xz"));
194                            let content_path_raw = self.contents_path().join(&hash);
195
196                            if let Ok(content_data) = std::fs::read(&content_path_xz) {
197                                let filename = format!("{hash}.xz");
198                                if let Some(ref sender) = tx {
199                                    let _ = sender.send(SnapshotEvent::Log(format!(
200                                        "Uploading content file: {} ({} bytes)",
201                                        filename,
202                                        content_data.len()
203                                    )));
204                                }
205                                self.upload_content_file(&filename, &content_data)?;
206                            } else if let Ok(content_data) = std::fs::read(&content_path_raw) {
207                                if let Some(ref sender) = tx {
208                                    let _ = sender.send(SnapshotEvent::Log(format!(
209                                        "Uploading content file: {} ({} bytes)",
210                                        hash,
211                                        content_data.len()
212                                    )));
213                                }
214                                self.upload_content_file(&hash, &content_data)?;
215                            }
216                        }
217
218                        // Log completion of content file uploads
219                        if let Some(ref sender) = tx {
220                            let _ = sender.send(SnapshotEvent::Log(format!(
221                                "Uploaded {} content file{}",
222                                total_missing,
223                                if total_missing == 1 { "" } else { "s" }
224                            )));
225                        }
226                    }
227
228                    // Commit the snapshot
229                    if let Some(ref sender) = tx {
230                        let _ =
231                            sender.send(SnapshotEvent::Log("Committing snapshot...".to_string()));
232                        let _ = sender.send(SnapshotEvent::SetStatus(
233                            "Committing snapshot...".to_string(),
234                        ));
235                    }
236                    self.commit_snapshot_type(subdirectory, &filename)?;
237
238                    // Send completion status
239                    if let Some(ref sender) = tx {
240                        let _ =
241                            sender.send(SnapshotEvent::SetStatus("Upload complete!".to_string()));
242                    }
243                }
244
245                return Ok(());
246            }
247        }
248
249        Ok(())
250    }
251}