Skip to main content

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    pub fn share(&self, cb: impl FnOnce(Result<u64, SteamError>) + 'static + Send) {
214        let api_call =
215            unsafe { sys::SteamAPI_ISteamRemoteStorage_FileShare(self.rs, self.name.as_ptr()) };
216        unsafe {
217            register_call_result::<sys::RemoteStorageFileShareResult_t, _>(
218                &self._inner,
219                api_call,
220                move |v, io_error| {
221                    if io_error {
222                        cb(Err(SteamError::IOFailure));
223                        return;
224                    }
225                    if v.m_eResult != sys::EResult::k_EResultOK {
226                        cb(Err(v.m_eResult.into()));
227                        return;
228                    }
229
230                    cb(Ok(v.m_hFile))
231                },
232            )
233        }
234    }
235}
236
237/// A write handle for a steam cloud file
238pub struct SteamFileWriter {
239    file: SteamFile,
240    handle: sys::UGCFileWriteStreamHandle_t,
241}
242
243impl std::io::Write for SteamFileWriter {
244    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
245        unsafe {
246            if sys::SteamAPI_ISteamRemoteStorage_FileWriteStreamWriteChunk(
247                self.file.rs,
248                self.handle,
249                buf.as_ptr().cast(),
250                buf.len() as _,
251            ) {
252                Ok(buf.len())
253            } else {
254                Err(std::io::ErrorKind::Other.into())
255            }
256        }
257    }
258
259    fn flush(&mut self) -> std::io::Result<()> {
260        Ok(())
261    }
262}
263
264impl Drop for SteamFileWriter {
265    fn drop(&mut self) {
266        unsafe {
267            sys::SteamAPI_ISteamRemoteStorage_FileWriteStreamClose(self.file.rs, self.handle);
268        }
269    }
270}
271
272/// A read handle for a steam cloud file
273pub struct SteamFileReader {
274    file: SteamFile,
275    offset: usize,
276    size: usize,
277}
278
279impl std::io::Read for SteamFileReader {
280    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
281        use std::cmp::min;
282        if buf.is_empty() || self.size - self.offset == 0 {
283            return Ok(0);
284        }
285        let len = min(buf.len(), self.size - self.offset);
286        unsafe {
287            let api_call = sys::SteamAPI_ISteamRemoteStorage_FileReadAsync(
288                self.file.rs,
289                self.file.name.as_ptr(),
290                self.offset as _,
291                len as _,
292            );
293
294            let mut failed = false;
295            while !sys::SteamAPI_ISteamUtils_IsAPICallCompleted(
296                self.file.util,
297                api_call,
298                &mut failed,
299            ) {
300                std::thread::yield_now();
301            }
302            if failed {
303                return Err(std::io::ErrorKind::Other.into());
304            }
305            let mut callback: sys::RemoteStorageFileReadAsyncComplete_t = std::mem::zeroed();
306            sys::SteamAPI_ISteamUtils_GetAPICallResult(
307                self.file.util,
308                api_call,
309                (&mut callback) as *mut _ as *mut _,
310                std::mem::size_of::<sys::RemoteStorageFileReadAsyncComplete_t>() as _,
311                1332,
312                &mut failed,
313            );
314
315            if callback.m_eResult != sys::EResult::k_EResultOK {
316                return Err(std::io::ErrorKind::Other.into());
317            }
318            let size = callback.m_cubRead as usize;
319            sys::SteamAPI_ISteamRemoteStorage_FileReadAsyncComplete(
320                self.file.rs,
321                callback.m_hFileReadAsync,
322                buf.as_mut_ptr().cast(),
323                callback.m_cubRead,
324            );
325
326            self.offset += size;
327            Ok(size)
328        }
329    }
330}
331
332impl std::io::Seek for SteamFileReader {
333    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
334        match pos {
335            std::io::SeekFrom::Current(o) => {
336                if self.offset as isize + o as isize >= self.size as isize {
337                    return Err(std::io::ErrorKind::InvalidInput.into());
338                }
339                self.offset = (self.offset as isize + o as isize) as usize;
340            }
341            std::io::SeekFrom::End(o) => {
342                if o as isize >= self.size as isize {
343                    return Err(std::io::ErrorKind::InvalidInput.into());
344                }
345                self.offset = (self.size as isize - 1 - o as isize) as usize;
346            }
347            std::io::SeekFrom::Start(o) => {
348                if o as usize >= self.size {
349                    return Err(std::io::ErrorKind::InvalidInput.into());
350                }
351                self.offset = o as usize;
352            }
353        }
354        Ok(self.offset as u64)
355    }
356}
357
358/// Name and size information about a file in the steam cloud
359#[derive(Clone, Debug)]
360#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
361pub struct SteamFileInfo {
362    /// The file name
363    pub name: String,
364    /// The size of the file in bytes
365    pub size: u64,
366}
367
368#[test]
369#[serial]
370fn test_cloud() {
371    use std::io::{Read, Write};
372    let client = Client::init().unwrap();
373
374    let rs = client.remote_storage();
375    println!("Listing files:");
376    for f in rs.files() {
377        println!("{:?}", f);
378    }
379
380    {
381        let test = rs.file("test.txt");
382        let mut w = test.write();
383        write!(w, "Testing").unwrap();
384    }
385
386    println!("Listing files:");
387    for f in rs.files() {
388        println!("{:?}", f);
389    }
390
391    let mut output = String::new();
392    let test = rs.file("test.txt");
393    test.read().read_to_string(&mut output).unwrap();
394    println!("Got: {:?}", output);
395
396    assert_eq!(output, "Testing");
397}