steamworks/
remote_storage.rs

1use super::*;
2#[cfg(test)]
3use serial_test::serial;
4
5/// Access to the steam remote storage interface
6pub struct RemoteStorage {
7    pub(crate) rs: *mut sys::ISteamRemoteStorage,
8    pub(crate) util: *mut sys::ISteamUtils,
9    pub(crate) inner: Arc<Inner>,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum PublishedFileVisibility {
14    Public,
15    FriendsOnly,
16    Private,
17    Unlisted,
18}
19
20impl From<sys::ERemoteStoragePublishedFileVisibility> for PublishedFileVisibility {
21    fn from(visibility: sys::ERemoteStoragePublishedFileVisibility) -> Self {
22        match visibility {
23            sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityPublic => PublishedFileVisibility::Public,
24            sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityFriendsOnly => PublishedFileVisibility::FriendsOnly,
25            sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityPrivate => PublishedFileVisibility::Private,
26            sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityUnlisted => PublishedFileVisibility::Unlisted,
27            _ => unreachable!(),
28        }
29    }
30}
31
32impl Into<sys::ERemoteStoragePublishedFileVisibility> for PublishedFileVisibility {
33    fn into(self) -> sys::ERemoteStoragePublishedFileVisibility {
34        match self {
35            PublishedFileVisibility::Public => sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityPublic,
36            PublishedFileVisibility::FriendsOnly => sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityFriendsOnly,
37            PublishedFileVisibility::Private => sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityPrivate,
38            PublishedFileVisibility::Unlisted => sys::ERemoteStoragePublishedFileVisibility::k_ERemoteStoragePublishedFileVisibilityUnlisted,
39        }
40    }
41}
42
43impl Clone for RemoteStorage {
44    fn clone(&self) -> Self {
45        RemoteStorage {
46            inner: self.inner.clone(),
47            rs: self.rs,
48            util: self.util,
49        }
50    }
51}
52
53impl RemoteStorage {
54    /// Toggles whether the steam cloud is enabled for the application
55    pub fn set_cloud_enabled_for_app(&self, enabled: bool) {
56        unsafe {
57            sys::SteamAPI_ISteamRemoteStorage_SetCloudEnabledForApp(self.rs, enabled);
58        }
59    }
60
61    /// Returns whether the steam cloud is enabled for the application
62    ///
63    /// # Note
64    ///
65    /// This is independent from the account wide setting
66    pub fn is_cloud_enabled_for_app(&self) -> bool {
67        unsafe { sys::SteamAPI_ISteamRemoteStorage_IsCloudEnabledForApp(self.rs) }
68    }
69
70    /// Returns whether the steam cloud is enabled for the account
71    ///
72    /// # Note
73    ///
74    /// This is independent from the application setting
75    pub fn is_cloud_enabled_for_account(&self) -> bool {
76        unsafe { sys::SteamAPI_ISteamRemoteStorage_IsCloudEnabledForAccount(self.rs) }
77    }
78
79    /// Returns information about all files in the cloud storage
80    pub fn files(&self) -> Vec<SteamFileInfo> {
81        unsafe {
82            let count = sys::SteamAPI_ISteamRemoteStorage_GetFileCount(self.rs);
83            if count == -1 {
84                return Vec::new();
85            }
86            let mut files = Vec::with_capacity(count as usize);
87            for idx in 0..count {
88                let mut size = 0;
89                let name = CStr::from_ptr(sys::SteamAPI_ISteamRemoteStorage_GetFileNameAndSize(
90                    self.rs, idx, &mut size,
91                ));
92                files.push(SteamFileInfo {
93                    name: name.to_string_lossy().into_owned(),
94                    size: size as u64,
95                })
96            }
97
98            files
99        }
100    }
101
102    /// Returns a handle to a steam cloud file
103    ///
104    /// The file does not have to exist.
105    pub fn file(&self, name: &str) -> SteamFile {
106        SteamFile {
107            rs: self.rs,
108            util: self.util,
109            _inner: self.inner.clone(),
110            name: CString::new(name).unwrap(),
111        }
112    }
113}
114
115bitflags! {
116    /// Platform flags used with [`SteamFile::set_sync_platforms`] to restrict
117    /// syncing to specific operating systems.
118    #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
119    #[repr(C)]
120    #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
121    pub struct RemoteStoragePlatforms: u32 {
122        const WINDOWS = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformWindows.0 as _;
123        const MACOS = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformOSX.0 as _;
124        const PS3 = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformPS3.0 as _;
125        const LINUX = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformLinux.0 as _;
126        const SWITCH = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformSwitch.0 as _;
127        const ANDROID = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformAndroid.0 as _;
128        const IOS = sys::ERemoteStoragePlatform::k_ERemoteStoragePlatformIOS.0 as _;
129    }
130}
131
132impl From<RemoteStoragePlatforms> for sys::ERemoteStoragePlatform {
133    fn from(platforms: RemoteStoragePlatforms) -> Self {
134        sys::ERemoteStoragePlatform(platforms.bits() as _)
135    }
136}
137
138/// A handle for a possible steam cloud file
139pub struct SteamFile {
140    pub(crate) rs: *mut sys::ISteamRemoteStorage,
141    pub(crate) util: *mut sys::ISteamUtils,
142    pub(crate) _inner: Arc<Inner>,
143    name: CString,
144}
145
146impl SteamFile {
147    /// Deletes the file locally and remotely.
148    ///
149    /// Returns whether a file was actually deleted
150    pub fn delete(&self) -> bool {
151        unsafe { sys::SteamAPI_ISteamRemoteStorage_FileDelete(self.rs, self.name.as_ptr()) }
152    }
153    /// Deletes the file remotely whilst keeping it locally.
154    ///
155    /// Returns whether a file was actually forgotten
156    pub fn forget(&self) -> bool {
157        unsafe { sys::SteamAPI_ISteamRemoteStorage_FileForget(self.rs, self.name.as_ptr()) }
158    }
159
160    /// Returns whether a file exists
161    pub fn exists(&self) -> bool {
162        unsafe { sys::SteamAPI_ISteamRemoteStorage_FileExists(self.rs, self.name.as_ptr()) }
163    }
164
165    /// Returns whether a file is persisted in the steam cloud
166    pub fn is_persisted(&self) -> bool {
167        unsafe { sys::SteamAPI_ISteamRemoteStorage_FilePersisted(self.rs, self.name.as_ptr()) }
168    }
169
170    /// Returns the timestamp of the file
171    pub fn timestamp(&self) -> i64 {
172        unsafe { sys::SteamAPI_ISteamRemoteStorage_GetFileTimestamp(self.rs, self.name.as_ptr()) }
173    }
174
175    /// Set which platforms the file should be available on
176    pub fn set_sync_platforms(&self, platforms: RemoteStoragePlatforms) {
177        unsafe {
178            sys::SteamAPI_ISteamRemoteStorage_SetSyncPlatforms(
179                self.rs,
180                self.name.as_ptr(),
181                platforms.into(),
182            );
183        }
184    }
185
186    /// Returns the platforms the file is available on
187    pub fn get_sync_platforms(&self) -> RemoteStoragePlatforms {
188        let bits = unsafe {
189            sys::SteamAPI_ISteamRemoteStorage_GetSyncPlatforms(self.rs, self.name.as_ptr())
190        };
191        RemoteStoragePlatforms::from_bits_truncate(bits.0 as _)
192    }
193
194    pub fn write(self) -> SteamFileWriter {
195        unsafe {
196            let handle =
197                sys::SteamAPI_ISteamRemoteStorage_FileWriteStreamOpen(self.rs, self.name.as_ptr());
198            SteamFileWriter { file: self, handle }
199        }
200    }
201
202    pub fn read(self) -> SteamFileReader {
203        unsafe {
204            SteamFileReader {
205                offset: 0,
206                size: sys::SteamAPI_ISteamRemoteStorage_GetFileSize(self.rs, self.name.as_ptr())
207                    as usize,
208                file: self,
209            }
210        }
211    }
212}
213/// A write handle for a steam cloud file
214pub struct SteamFileWriter {
215    file: SteamFile,
216    handle: sys::UGCFileWriteStreamHandle_t,
217}
218
219impl std::io::Write for SteamFileWriter {
220    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
221        unsafe {
222            if sys::SteamAPI_ISteamRemoteStorage_FileWriteStreamWriteChunk(
223                self.file.rs,
224                self.handle,
225                buf.as_ptr().cast(),
226                buf.len() as _,
227            ) {
228                Ok(buf.len())
229            } else {
230                Err(std::io::ErrorKind::Other.into())
231            }
232        }
233    }
234
235    fn flush(&mut self) -> std::io::Result<()> {
236        Ok(())
237    }
238}
239
240impl Drop for SteamFileWriter {
241    fn drop(&mut self) {
242        unsafe {
243            sys::SteamAPI_ISteamRemoteStorage_FileWriteStreamClose(self.file.rs, self.handle);
244        }
245    }
246}
247
248/// A read handle for a steam cloud file
249pub struct SteamFileReader {
250    file: SteamFile,
251    offset: usize,
252    size: usize,
253}
254
255impl std::io::Read for SteamFileReader {
256    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
257        use std::cmp::min;
258        if buf.is_empty() || self.size - self.offset == 0 {
259            return Ok(0);
260        }
261        let len = min(buf.len(), self.size - self.offset);
262        unsafe {
263            let api_call = sys::SteamAPI_ISteamRemoteStorage_FileReadAsync(
264                self.file.rs,
265                self.file.name.as_ptr(),
266                self.offset as _,
267                len as _,
268            );
269
270            let mut failed = false;
271            while !sys::SteamAPI_ISteamUtils_IsAPICallCompleted(
272                self.file.util,
273                api_call,
274                &mut failed,
275            ) {
276                std::thread::yield_now();
277            }
278            if failed {
279                return Err(std::io::ErrorKind::Other.into());
280            }
281            let mut callback: sys::RemoteStorageFileReadAsyncComplete_t = std::mem::zeroed();
282            sys::SteamAPI_ISteamUtils_GetAPICallResult(
283                self.file.util,
284                api_call,
285                (&mut callback) as *mut _ as *mut _,
286                std::mem::size_of::<sys::RemoteStorageFileReadAsyncComplete_t>() as _,
287                1332,
288                &mut failed,
289            );
290
291            if callback.m_eResult != sys::EResult::k_EResultOK {
292                return Err(std::io::ErrorKind::Other.into());
293            }
294            let size = callback.m_cubRead as usize;
295            sys::SteamAPI_ISteamRemoteStorage_FileReadAsyncComplete(
296                self.file.rs,
297                callback.m_hFileReadAsync,
298                buf.as_mut_ptr().cast(),
299                callback.m_cubRead,
300            );
301
302            self.offset += size;
303            Ok(size)
304        }
305    }
306}
307
308impl std::io::Seek for SteamFileReader {
309    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
310        match pos {
311            std::io::SeekFrom::Current(o) => {
312                if self.offset as isize + o as isize >= self.size as isize {
313                    return Err(std::io::ErrorKind::InvalidInput.into());
314                }
315                self.offset = (self.offset as isize + o as isize) as usize;
316            }
317            std::io::SeekFrom::End(o) => {
318                if o as isize >= self.size as isize {
319                    return Err(std::io::ErrorKind::InvalidInput.into());
320                }
321                self.offset = (self.size as isize - 1 - o as isize) as usize;
322            }
323            std::io::SeekFrom::Start(o) => {
324                if o as usize >= self.size {
325                    return Err(std::io::ErrorKind::InvalidInput.into());
326                }
327                self.offset = o as usize;
328            }
329        }
330        Ok(self.offset as u64)
331    }
332}
333
334/// Name and size information about a file in the steam cloud
335#[derive(Clone, Debug)]
336#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
337pub struct SteamFileInfo {
338    /// The file name
339    pub name: String,
340    /// The size of the file in bytes
341    pub size: u64,
342}
343
344#[test]
345#[serial]
346fn test_cloud() {
347    use std::io::{Read, Write};
348    let client = Client::init().unwrap();
349
350    let rs = client.remote_storage();
351    println!("Listing files:");
352    for f in rs.files() {
353        println!("{:?}", f);
354    }
355
356    {
357        let test = rs.file("test.txt");
358        let mut w = test.write();
359        write!(w, "Testing").unwrap();
360    }
361
362    println!("Listing files:");
363    for f in rs.files() {
364        println!("{:?}", f);
365    }
366
367    let mut output = String::new();
368    let test = rs.file("test.txt");
369    test.read().read_to_string(&mut output).unwrap();
370    println!("Got: {:?}", output);
371
372    assert_eq!(output, "Testing");
373}