Skip to main content

ashpd/documents/
mod.rs

1//! # Examples
2//!
3//! ```rust,no_run
4//! use std::str::FromStr;
5//!
6//! use ashpd::{
7//!     AppID,
8//!     documents::{Documents, Permission},
9//! };
10//!
11//! async fn run() -> ashpd::Result<()> {
12//!     let proxy = Documents::new().await?;
13//!
14//!     println!("{:#?}", proxy.mount_point().await?);
15//!     let app_id = AppID::from_str("org.mozilla.firefox").unwrap();
16//!     for (doc_id, host_path) in proxy.list(Some(&app_id)).await? {
17//!         if doc_id == "f2ee988d".into() {
18//!             let info = proxy.info(doc_id).await?;
19//!             println!("{:#?}", info);
20//!         }
21//!     }
22//!
23//!     proxy
24//!         .grant_permissions("f2ee988d", &app_id, &[Permission::GrantPermissions])
25//!         .await?;
26//!     proxy
27//!         .revoke_permissions("f2ee988d", &app_id, &[Permission::Write])
28//!         .await?;
29//!
30//!     proxy.delete("f2ee988d").await?;
31//!
32//!     Ok(())
33//! }
34//! ```
35
36use std::{collections::HashMap, fmt, os::fd::AsFd, path::Path, str::FromStr};
37
38use enumflags2::{BitFlags, bitflags};
39use serde::{Deserialize, Serialize};
40use serde_repr::{Deserialize_repr, Serialize_repr};
41use zbus::zvariant::{Fd, OwnedValue, Type};
42
43pub use crate::app_id::DocumentID;
44use crate::{AppID, Error, FilePath, proxy::Proxy};
45
46#[bitflags]
47#[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)]
48#[repr(u32)]
49/// Document flags
50pub enum DocumentFlags {
51    /// Reuse the existing document store entry for the file.
52    ReuseExisting,
53    /// Persistent file.
54    Persistent,
55    /// Depends on the application needs.
56    AsNeededByApp,
57    /// Export a directory.
58    ExportDirectory,
59}
60
61/// A [`HashMap`] mapping application IDs to the permissions for that
62/// application
63pub type Permissions = HashMap<AppID, Vec<Permission>>;
64
65#[cfg_attr(feature = "glib", derive(glib::Enum))]
66#[cfg_attr(feature = "glib", enum_type(name = "AshpdPermission"))]
67#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq, Type)]
68#[zvariant(signature = "s")]
69#[serde(rename_all = "kebab-case")]
70/// The possible permissions to grant to a specific application for a specific
71/// document.
72pub enum Permission {
73    /// Read access.
74    Read,
75    /// Write access.
76    Write,
77    /// The possibility to grant new permissions to the file.
78    GrantPermissions,
79    /// Delete access.
80    Delete,
81}
82
83impl fmt::Display for Permission {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Self::Read => write!(f, "Read"),
87            Self::Write => write!(f, "Write"),
88            Self::GrantPermissions => write!(f, "Grant Permissions"),
89            Self::Delete => write!(f, "Delete"),
90        }
91    }
92}
93
94impl AsRef<str> for Permission {
95    fn as_ref(&self) -> &str {
96        match self {
97            Self::Read => "Read",
98            Self::Write => "Write",
99            Self::GrantPermissions => "Grant Permissions",
100            Self::Delete => "Delete",
101        }
102    }
103}
104
105impl From<Permission> for &'static str {
106    fn from(p: Permission) -> Self {
107        match p {
108            Permission::Read => "Read",
109            Permission::Write => "Write",
110            Permission::GrantPermissions => "Grant Permissions",
111            Permission::Delete => "Delete",
112        }
113    }
114}
115
116impl FromStr for Permission {
117    type Err = Error;
118
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        match s {
121            "Read" | "read" => Ok(Permission::Read),
122            "Write" | "write" => Ok(Permission::Write),
123            "GrantPermissions" | "grant-permissions" => Ok(Permission::GrantPermissions),
124            "Delete" | "delete" => Ok(Permission::Delete),
125            _ => Err(Error::ParseError("Failed to parse priority, invalid value")),
126        }
127    }
128}
129
130/// The interface lets sandboxed applications make files from the outside world
131/// available to sandboxed applications in a controlled way.
132///
133/// Exported files will be made accessible to the application via a fuse
134/// filesystem that gets mounted at `/run/user/$UID/doc/`. The filesystem gets
135/// mounted both outside and inside the sandbox, but the view inside the sandbox
136/// is restricted to just those files that the application is allowed to access.
137///
138/// Individual files will appear at `/run/user/$UID/doc/$DOC_ID/filename`,
139/// where `$DOC_ID` is the ID of the file in the document store.
140/// It is returned by the [`Documents::add`] and
141/// [`Documents::add_named`] calls.
142///
143/// The permissions that the application has for a document store entry (see
144/// [`Documents::grant_permissions`]) are reflected in the POSIX mode bits
145/// in the fuse filesystem.
146///
147/// Wrapper of the DBus interface: [`org.freedesktop.portal.Documents`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html).
148#[derive(Debug)]
149#[doc(alias = "org.freedesktop.portal.Documents")]
150pub struct Documents(Proxy<'static>);
151
152impl Documents {
153    /// Create a new instance of [`Documents`].
154    pub async fn new() -> Result<Self, Error> {
155        let proxy = Proxy::new_documents("org.freedesktop.portal.Documents").await?;
156        Ok(Self(proxy))
157    }
158
159    /// Create a new instance of [`Documents`].
160    pub async fn with_connection(connection: zbus::Connection) -> Result<Self, Error> {
161        let proxy =
162            Proxy::new_documents_with_connection(connection, "org.freedesktop.portal.Documents")
163                .await?;
164        Ok(Self(proxy))
165    }
166
167    /// Returns the version of the portal interface.
168    pub fn version(&self) -> u32 {
169        self.0.version()
170    }
171
172    /// Adds a file to the document store.
173    /// The file is passed in the form of an open file descriptor
174    /// to prove that the caller has access to the file.
175    ///
176    /// # Arguments
177    ///
178    /// * `o_path_fd` - Open file descriptor for the file to add.
179    /// * `reuse_existing` - Whether to reuse an existing document store entry
180    ///   for the file.
181    /// * `persistent` - Whether to add the file only for this session or
182    ///   permanently.
183    ///
184    /// # Returns
185    ///
186    /// The ID of the file in the document store.
187    ///
188    /// # Specifications
189    ///
190    /// See also [`Add`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-add).
191    #[doc(alias = "Add")]
192    pub async fn add(
193        &self,
194        o_path_fd: &impl AsFd,
195        reuse_existing: bool,
196        persistent: bool,
197    ) -> Result<DocumentID, Error> {
198        self.0
199            .call("Add", &(Fd::from(o_path_fd), reuse_existing, persistent))
200            .await
201    }
202
203    /// Adds multiple files to the document store.
204    /// The files are passed in the form of an open file descriptor
205    /// to prove that the caller has access to the file.
206    ///
207    /// # Arguments
208    ///
209    /// * `o_path_fds` - Open file descriptors for the files to export.
210    /// * `flags` - A [`DocumentFlags`].
211    /// * `app_id` - An application ID, or `None`.
212    /// * `permissions` - The permissions to grant.
213    ///
214    /// # Returns
215    ///
216    /// The IDs of the files in the document store along with other extra info.
217    ///
218    /// # Required version
219    ///
220    /// The method requires the 2nd version implementation of the portal and
221    /// would fail with [`Error::RequiresVersion`] otherwise.
222    ///
223    /// # Specifications
224    ///
225    /// See also [`AddFull`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addfull).
226    #[doc(alias = "AddFull")]
227    pub async fn add_full(
228        &self,
229        o_path_fds: &[impl AsFd],
230        flags: BitFlags<DocumentFlags>,
231        app_id: Option<&AppID>,
232        permissions: &[Permission],
233    ) -> Result<(Vec<DocumentID>, HashMap<String, OwnedValue>), Error> {
234        let o_path: Vec<Fd> = o_path_fds.iter().map(Fd::from).collect();
235        let app_id = app_id.map(|id| id.as_ref()).unwrap_or("");
236        self.0
237            .call_versioned("AddFull", &(o_path, flags, app_id, permissions), 2)
238            .await
239    }
240
241    /// Creates an entry in the document store for writing a new file.
242    ///
243    /// # Arguments
244    ///
245    /// * `o_path_parent_fd` - Open file descriptor for the parent directory.
246    /// * `filename` - The basename for the file.
247    /// * `reuse_existing` - Whether to reuse an existing document store entry
248    ///   for the file.
249    /// * `persistent` - Whether to add the file only for this session or
250    ///   permanently.
251    ///
252    /// # Returns
253    ///
254    /// The ID of the file in the document store.
255    ///
256    /// # Specifications
257    ///
258    /// See also [`AddNamed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addnamed).
259    #[doc(alias = "AddNamed")]
260    pub async fn add_named(
261        &self,
262        o_path_parent_fd: &impl AsFd,
263        filename: impl AsRef<Path>,
264        reuse_existing: bool,
265        persistent: bool,
266    ) -> Result<DocumentID, Error> {
267        let filename = FilePath::new(filename)?;
268        self.0
269            .call(
270                "AddNamed",
271                &(
272                    Fd::from(o_path_parent_fd),
273                    filename,
274                    reuse_existing,
275                    persistent,
276                ),
277            )
278            .await
279    }
280
281    /// Adds multiple files to the document store.
282    /// The files are passed in the form of an open file descriptor
283    /// to prove that the caller has access to the file.
284    ///
285    /// # Arguments
286    ///
287    /// * `o_path_fd` - Open file descriptor for the parent directory.
288    /// * `filename` - The basename for the file.
289    /// * `flags` - A [`DocumentFlags`].
290    /// * `app_id` - An application ID, or `None`.
291    /// * `permissions` - The permissions to grant.
292    ///
293    /// # Returns
294    ///
295    /// The ID of the file in the document store along with other extra info.
296    ///
297    /// # Required version
298    ///
299    /// The method requires the 3nd version implementation of the portal and
300    /// would fail with [`Error::RequiresVersion`] otherwise.
301    ///
302    /// # Specifications
303    ///
304    /// See also [`AddNamedFull`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addnamedfull).
305    #[doc(alias = "AddNamedFull")]
306    pub async fn add_named_full(
307        &self,
308        o_path_fd: &impl AsFd,
309        filename: impl AsRef<Path>,
310        flags: BitFlags<DocumentFlags>,
311        app_id: Option<&AppID>,
312        permissions: &[Permission],
313    ) -> Result<(DocumentID, HashMap<String, OwnedValue>), Error> {
314        let app_id = app_id.map(|id| id.as_ref()).unwrap_or("");
315        let filename = FilePath::new(filename)?;
316        self.0
317            .call_versioned(
318                "AddNamedFull",
319                &(Fd::from(o_path_fd), filename, flags, app_id, permissions),
320                3,
321            )
322            .await
323    }
324
325    /// Removes an entry from the document store. The file itself is not
326    /// deleted.
327    ///
328    /// **Note** This call is available inside the sandbox if the
329    /// application has the [`Permission::Delete`] for the document.
330    ///
331    /// # Arguments
332    ///
333    /// * `doc_id` - The ID of the file in the document store.
334    ///
335    /// # Specifications
336    ///
337    /// See also [`Delete`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-delete).
338    #[doc(alias = "Delete")]
339    pub async fn delete(&self, doc_id: impl Into<DocumentID>) -> Result<(), Error> {
340        self.0.call("Delete", &(doc_id.into())).await
341    }
342
343    /// Returns the path at which the document store fuse filesystem is mounted.
344    /// This will typically be `/run/user/$UID/doc/`.
345    ///
346    /// # Specifications
347    ///
348    /// See also [`GetMountPoint`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-getmountpoint).
349    #[doc(alias = "GetMountPoint")]
350    #[doc(alias = "get_mount_point")]
351    pub async fn mount_point(&self) -> Result<FilePath, Error> {
352        self.0.call("GetMountPoint", &()).await
353    }
354
355    /// Grants access permissions for a file in the document store to an
356    /// application.
357    ///
358    /// **Note** This call is available inside the sandbox if the
359    /// application has the [`Permission::GrantPermissions`] for the document.
360    ///
361    /// # Arguments
362    ///
363    /// * `doc_id` - The ID of the file in the document store.
364    /// * `app_id` - The ID of the application to which permissions are granted.
365    /// * `permissions` - The permissions to grant.
366    ///
367    /// # Specifications
368    ///
369    /// See also [`GrantPermissions`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-grantpermissions).
370    #[doc(alias = "GrantPermissions")]
371    pub async fn grant_permissions(
372        &self,
373        doc_id: impl Into<DocumentID>,
374        app_id: &AppID,
375        permissions: &[Permission],
376    ) -> Result<(), Error> {
377        self.0
378            .call("GrantPermissions", &(doc_id.into(), app_id, permissions))
379            .await
380    }
381
382    /// Gets the filesystem path and application permissions for a document
383    /// store entry.
384    ///
385    /// **Note** This call is not available inside the sandbox.
386    ///
387    /// # Arguments
388    ///
389    /// * `doc_id` - The ID of the file in the document store.
390    ///
391    /// # Returns
392    ///
393    /// The path of the file in the host filesystem along with the
394    /// [`Permissions`].
395    ///
396    /// # Specifications
397    ///
398    /// See also [`Info`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-info).
399    #[doc(alias = "Info")]
400    pub async fn info(
401        &self,
402        doc_id: impl Into<DocumentID>,
403    ) -> Result<(FilePath, Permissions), Error> {
404        self.0.call("Info", &(doc_id.into())).await
405    }
406
407    /// Lists documents in the document store for an application (or for all
408    /// applications).
409    ///
410    /// **Note** This call is not available inside the sandbox.
411    ///
412    /// # Arguments
413    ///
414    /// * `app-id` - The application ID, or `None` to list all documents.
415    ///
416    /// # Returns
417    ///
418    /// [`HashMap`] mapping document IDs to their filesystem path on the host
419    /// system.
420    ///
421    /// # Specifications
422    ///
423    /// See also [`List`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-list).
424    #[doc(alias = "List")]
425    pub async fn list(
426        &self,
427        app_id: Option<&AppID>,
428    ) -> Result<HashMap<DocumentID, FilePath>, Error> {
429        let app_id = app_id.map(|id| id.as_ref()).unwrap_or("");
430        let response: HashMap<String, FilePath> = self.0.call("List", &(app_id)).await?;
431
432        let mut new_response: HashMap<DocumentID, FilePath> = HashMap::new();
433        for (key, file_name) in response {
434            new_response.insert(DocumentID::from(key), file_name);
435        }
436
437        Ok(new_response)
438    }
439
440    /// Looks up the document ID for a file.
441    ///
442    /// **Note** This call is not available inside the sandbox.
443    ///
444    /// # Arguments
445    ///
446    /// * `filename` - A path in the host filesystem.
447    ///
448    /// # Returns
449    ///
450    /// The ID of the file in the document store, or [`None`] if the file is not
451    /// in the document store.
452    ///
453    /// # Specifications
454    ///
455    /// See also [`Lookup`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-lookup).
456    #[doc(alias = "Lookup")]
457    pub async fn lookup(&self, filename: impl AsRef<Path>) -> Result<Option<DocumentID>, Error> {
458        let filename = FilePath::new(filename)?;
459        let doc_id: String = self.0.call("Lookup", &(filename)).await?;
460        if doc_id.is_empty() {
461            Ok(None)
462        } else {
463            Ok(Some(doc_id.into()))
464        }
465    }
466
467    /// Revokes access permissions for a file in the document store from an
468    /// application.
469    ///
470    /// **Note** This call is available inside the sandbox if the
471    /// application has the [`Permission::GrantPermissions`] for the document.
472    ///
473    /// # Arguments
474    ///
475    /// * `doc_id` - The ID of the file in the document store.
476    /// * `app_id` - The ID of the application from which the permissions are
477    ///   revoked.
478    /// * `permissions` - The permissions to revoke.
479    ///
480    /// # Specifications
481    ///
482    /// See also [`RevokePermissions`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-revokepermissions).
483    #[doc(alias = "RevokePermissions")]
484    pub async fn revoke_permissions(
485        &self,
486        doc_id: impl Into<DocumentID>,
487        app_id: &AppID,
488        permissions: &[Permission],
489    ) -> Result<(), Error> {
490        self.0
491            .call("RevokePermissions", &(doc_id.into(), app_id, permissions))
492            .await
493    }
494
495    /// Retrieves the host filesystem paths from their document IDs.
496    ///
497    /// # Arguments
498    ///
499    /// * `doc_ids` - A list of file IDs in the document store.
500    ///
501    /// # Returns
502    ///
503    /// A dictionary mapping document IDs to the paths in the host filesystem
504    ///
505    /// # Specifications
506    ///
507    /// See also [`GetHostPaths`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-gethostpaths).
508    #[doc(alias = "GetHostPaths")]
509    pub async fn host_paths(
510        &self,
511        doc_ids: &[DocumentID],
512    ) -> Result<HashMap<DocumentID, FilePath>, Error> {
513        self.0.call_versioned("GetHostPaths", &(doc_ids,), 5).await
514    }
515}
516
517impl std::ops::Deref for Documents {
518    type Target = zbus::Proxy<'static>;
519
520    fn deref(&self) -> &Self::Target {
521        &self.0
522    }
523}
524
525/// Interact with `org.freedesktop.portal.FileTransfer` interface.
526pub mod file_transfer;
527
528#[cfg(test)]
529mod tests {
530    use std::collections::HashMap;
531
532    use zbus::zvariant::Type;
533
534    use crate::{FilePath, app_id::DocumentID, documents::Permission};
535
536    #[test]
537    fn serialize_deserialize() {
538        let permission = Permission::GrantPermissions;
539        let string = serde_json::to_string(&permission).unwrap();
540        assert_eq!(string, "\"grant-permissions\"");
541
542        let decoded = serde_json::from_str(&string).unwrap();
543        assert_eq!(permission, decoded);
544
545        assert_eq!(HashMap::<DocumentID, FilePath>::SIGNATURE, "a{say}");
546    }
547}