supreme_fs/game_storage/online/
transfer.rs1use super::util::ensure_json_extension;
5use crate::game_storage::GameStorage;
6use crate::{HybridStorageError, OnlineStorageError, SnapshotEvent};
7use std::fs;
8use std::io;
9use std::sync::mpsc::Sender;
10
11impl GameStorage {
12 pub fn upload_profile(
13 &self,
14 profile_name: &str,
15 data: &[u8],
16 tx: Option<Sender<SnapshotEvent>>,
17 ) -> Result<(), HybridStorageError> {
18 self.save_and_upload_snapshot("profiles", profile_name, data, tx)
19 }
20
21 pub fn upload_replay(
23 &self,
24 snapshot_name: &str,
25 data: &[u8],
26 tx: Option<Sender<SnapshotEvent>>,
27 ) -> Result<(), HybridStorageError> {
28 self.save_and_upload_snapshot("replays", snapshot_name, data, tx)
29 }
30
31 fn save_and_upload_snapshot(
32 &self,
33 subdirectory: &str,
34 name: &str,
35 data: &[u8],
36 tx: Option<Sender<SnapshotEvent>>,
37 ) -> Result<(), HybridStorageError> {
38 let filename = ensure_json_extension(name);
39
40 let local_path = self
42 .root_path
43 .join(".supreme")
44 .join(subdirectory)
45 .join(&filename);
46
47 fs::create_dir_all(
48 local_path
49 .parent()
50 .ok_or_else(|| io::Error::other("Local path has no parent directory"))?,
51 )?;
52 fs::write(&local_path, data)?;
53
54 if self.api_url.is_some() {
55 self.upload_snapshot_generic(subdirectory, name, data, tx)?;
56 }
57
58 Ok(())
59 }
60
61 fn upload_snapshot_generic(
62 &self,
63 subdirectory: &str,
64 _name: &str,
65 data: &[u8],
66 tx: Option<Sender<SnapshotEvent>>,
67 ) -> Result<(), HybridStorageError> {
68 if self.api_url.is_some() {
69 return self.upload_file_unified(subdirectory, data, "snapshot", None, tx);
71 }
72
73 Ok(())
74 }
75
76 pub fn commit_snapshot(&self, snapshot_name: &str) -> Result<(), HybridStorageError> {
78 self.commit_snapshot_type("replays", snapshot_name)
79 }
80
81 pub fn commit_snapshot_type(
82 &self,
83 subdirectory: &str,
84 snapshot_filename_with_ext: &str,
85 ) -> Result<(), HybridStorageError> {
86 use super::multipart::{MultipartForm, SendMultipart};
87 let client = self
88 .client
89 .as_ref()
90 .ok_or_else(|| io::Error::other("Online storage not configured"))?;
91 let api_url = self
92 .api_url
93 .as_ref()
94 .ok_or_else(|| io::Error::other("Online storage not configured"))?;
95
96 let mut form = MultipartForm::new()
97 .text("action", "commit_snapshot")
98 .text("name", snapshot_filename_with_ext)
99 .text("type", subdirectory);
100
101 if let Some(token) = &self.token {
102 form = form.text("api_token", token.clone());
103 }
104
105 let mut request = client.post(api_url);
106 if let Some(ref token) = self.token {
107 request = request.header("X-SSBZ-Token", token);
108 }
109 let response = request.send_multipart(form)?;
110
111 if !response.status().is_success() {
112 return Err(HybridStorageError::UploadFailed(
113 self.extract_error_message(response),
114 ));
115 }
116 Ok(())
117 }
118
119 pub fn fetch_replay(&self, snapshot_name: &str) -> Result<Vec<u8>, HybridStorageError> {
122 self.fetch_and_cache_snapshot("replays", snapshot_name)
123 }
124
125 pub(crate) fn fetch_and_cache_snapshot(
126 &self,
127 subdirectory: &str,
128 name: &str,
129 ) -> Result<Vec<u8>, HybridStorageError> {
130 let filename = ensure_json_extension(name);
131
132 let local_path = self
134 .root_path
135 .join(".supreme")
136 .join(subdirectory)
137 .join(&filename);
138
139 if local_path.exists() {
140 return Ok(fs::read(&local_path)?);
141 }
142
143 if self.api_url.is_some() {
144 let data = self.fetch_snapshot(subdirectory, &filename)?;
145 fs::create_dir_all(
147 local_path
148 .parent()
149 .ok_or_else(|| io::Error::other("Local path has no parent directory"))?,
150 )?;
151 fs::write(&local_path, &data)?;
152 Ok(data)
153 } else {
154 Err(HybridStorageError::NotFound(filename))
155 }
156 }
157
158 fn fetch_snapshot(
159 &self,
160 subdirectory: &str,
161 filename: &str,
162 ) -> Result<Vec<u8>, HybridStorageError> {
163 let client = self
164 .client
165 .as_ref()
166 .ok_or_else(|| io::Error::other("Online storage not configured"))?;
167 let base_url = self
168 .api_url
169 .as_ref()
170 .ok_or_else(|| io::Error::other("Online storage not configured"))?
171 .trim_end_matches('/');
172
173 let filename_json = ensure_json_extension(filename);
174 let url = format!("{base_url}/{subdirectory}/{filename_json}");
175
176 let response = client.get(&url).call()?;
177 if response.status().is_success() {
178 let mut bytes = Vec::new();
179 use std::io::Read;
180 response.into_body().into_reader().read_to_end(&mut bytes)?;
181 Ok(bytes)
182 } else {
183 Err(OnlineStorageError::NotFound(format!("URL: {subdirectory}/{filename_json}")).into())
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use tempfile::tempdir;
192
193 #[test]
194 fn fetch_and_cache_snapshot_reads_from_local_cache_first() {
195 let dir = tempdir().unwrap();
196 let storage = GameStorage::new(dir.path());
197 let cache_dir = dir.path().join(".supreme").join("replays");
198 fs::create_dir_all(&cache_dir).unwrap();
199 fs::write(cache_dir.join("my_replay.json"), b"cached data").unwrap();
200
201 let data = storage
202 .fetch_and_cache_snapshot("replays", "my_replay")
203 .expect("fetch_and_cache_snapshot");
204 assert_eq!(data, b"cached data");
205 }
206
207 #[test]
208 fn fetch_and_cache_snapshot_returns_not_found_when_offline_and_uncached() {
209 let dir = tempdir().unwrap();
210 let storage = GameStorage::new(dir.path());
211 let result = storage.fetch_and_cache_snapshot("replays", "missing");
212 assert!(matches!(result, Err(HybridStorageError::NotFound(_))));
213 }
214
215 #[test]
216 fn save_and_upload_snapshot_writes_local_cache_even_when_offline() {
217 let dir = tempdir().unwrap();
218 let storage = GameStorage::new(dir.path());
219
220 storage
221 .upload_profile("my_profile", b"profile-bytes", None)
222 .expect("upload_profile should succeed locally when offline");
223
224 let cached = dir
225 .path()
226 .join(".supreme")
227 .join("profiles")
228 .join("my_profile.json");
229 assert!(cached.exists());
230 assert_eq!(fs::read(cached).unwrap(), b"profile-bytes");
231 }
232}