ashpd/documents/
mod.rs

1//! # Examples
2//!
3//! ```rust,no_run
4//! use std::str::FromStr;
5//!
6//! use ashpd::{
7//!     documents::{Documents, Permission},
8//!     AppID,
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::{proxy::Proxy, AppID, Error, FilePath};
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<'a>(Proxy<'a>);
151
152impl<'a> Documents<'a> {
153    /// Create a new instance of [`Documents`].
154    pub async fn new() -> Result<Documents<'a>, Error> {
155        let proxy = Proxy::new_documents("org.freedesktop.portal.Documents").await?;
156        Ok(Self(proxy))
157    }
158
159    /// Adds a file to the document store.
160    /// The file is passed in the form of an open file descriptor
161    /// to prove that the caller has access to the file.
162    ///
163    /// # Arguments
164    ///
165    /// * `o_path_fd` - Open file descriptor for the file to add.
166    /// * `reuse_existing` - Whether to reuse an existing document store entry
167    ///   for the file.
168    /// * `persistent` - Whether to add the file only for this session or
169    ///   permanently.
170    ///
171    /// # Returns
172    ///
173    /// The ID of the file in the document store.
174    ///
175    /// # Specifications
176    ///
177    /// See also [`Add`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-add).
178    #[doc(alias = "Add")]
179    pub async fn add(
180        &self,
181        o_path_fd: &impl AsFd,
182        reuse_existing: bool,
183        persistent: bool,
184    ) -> Result<DocumentID, Error> {
185        self.0
186            .call("Add", &(Fd::from(o_path_fd), reuse_existing, persistent))
187            .await
188    }
189
190    /// Adds multiple files to the document store.
191    /// The files are passed in the form of an open file descriptor
192    /// to prove that the caller has access to the file.
193    ///
194    /// # Arguments
195    ///
196    /// * `o_path_fds` - Open file descriptors for the files to export.
197    /// * `flags` - A [`DocumentFlags`].
198    /// * `app_id` - An application ID, or `None`.
199    /// * `permissions` - The permissions to grant.
200    ///
201    /// # Returns
202    ///
203    /// The IDs of the files in the document store along with other extra info.
204    ///
205    /// # Required version
206    ///
207    /// The method requires the 2nd version implementation of the portal and
208    /// would fail with [`Error::RequiresVersion`] otherwise.
209    ///
210    /// # Specifications
211    ///
212    /// See also [`AddFull`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addfull).
213    #[doc(alias = "AddFull")]
214    pub async fn add_full(
215        &self,
216        o_path_fds: &[impl AsFd],
217        flags: BitFlags<DocumentFlags>,
218        app_id: Option<&AppID>,
219        permissions: &[Permission],
220    ) -> Result<(Vec<DocumentID>, HashMap<String, OwnedValue>), Error> {
221        let o_path: Vec<Fd> = o_path_fds.iter().map(Fd::from).collect();
222        let app_id = app_id.map(|id| id.as_ref()).unwrap_or("");
223        self.0
224            .call_versioned("AddFull", &(o_path, flags, app_id, permissions), 2)
225            .await
226    }
227
228    /// Creates an entry in the document store for writing a new file.
229    ///
230    /// # Arguments
231    ///
232    /// * `o_path_parent_fd` - Open file descriptor for the parent directory.
233    /// * `filename` - The basename for the file.
234    /// * `reuse_existing` - Whether to reuse an existing document store entry
235    ///   for the file.
236    /// * `persistent` - Whether to add the file only for this session or
237    ///   permanently.
238    ///
239    /// # Returns
240    ///
241    /// The ID of the file in the document store.
242    ///
243    /// # Specifications
244    ///
245    /// See also [`AddNamed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addnamed).
246    #[doc(alias = "AddNamed")]
247    pub async fn add_named(
248        &self,
249        o_path_parent_fd: &impl AsFd,
250        filename: impl AsRef<Path>,
251        reuse_existing: bool,
252        persistent: bool,
253    ) -> Result<DocumentID, Error> {
254        let filename = FilePath::new(filename)?;
255        self.0
256            .call(
257                "AddNamed",
258                &(
259                    Fd::from(o_path_parent_fd),
260                    filename,
261                    reuse_existing,
262                    persistent,
263                ),
264            )
265            .await
266    }
267
268    /// Adds multiple files to the document store.
269    /// The files are passed in the form of an open file descriptor
270    /// to prove that the caller has access to the file.
271    ///
272    /// # Arguments
273    ///
274    /// * `o_path_fd` - Open file descriptor for the parent directory.
275    /// * `filename` - The basename for the file.
276    /// * `flags` - A [`DocumentFlags`].
277    /// * `app_id` - An application ID, or `None`.
278    /// * `permissions` - The permissions to grant.
279    ///
280    /// # Returns
281    ///
282    /// The ID of the file in the document store along with other extra info.
283    ///
284    /// # Required version
285    ///
286    /// The method requires the 3nd version implementation of the portal and
287    /// would fail with [`Error::RequiresVersion`] otherwise.
288    ///
289    /// # Specifications
290    ///
291    /// See also [`AddNamedFull`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addnamedfull).
292    #[doc(alias = "AddNamedFull")]
293    pub async fn add_named_full(
294        &self,
295        o_path_fd: &impl AsFd,
296        filename: impl AsRef<Path>,
297        flags: BitFlags<DocumentFlags>,
298        app_id: Option<&AppID>,
299        permissions: &[Permission],
300    ) -> Result<(DocumentID, HashMap<String, OwnedValue>), Error> {
301        let app_id = app_id.map(|id| id.as_ref()).unwrap_or("");
302        let filename = FilePath::new(filename)?;
303        self.0
304            .call_versioned(
305                "AddNamedFull",
306                &(Fd::from(o_path_fd), filename, flags, app_id, permissions),
307                3,
308            )
309            .await
310    }
311
312    /// Removes an entry from the document store. The file itself is not
313    /// deleted.
314    ///
315    /// **Note** This call is available inside the sandbox if the
316    /// application has the [`Permission::Delete`] for the document.
317    ///
318    /// # Arguments
319    ///
320    /// * `doc_id` - The ID of the file in the document store.
321    ///
322    /// # Specifications
323    ///
324    /// See also [`Delete`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-delete).
325    #[doc(alias = "Delete")]
326    pub async fn delete(&self, doc_id: impl Into<DocumentID>) -> Result<(), Error> {
327        self.0.call("Delete", &(doc_id.into())).await
328    }
329
330    /// Returns the path at which the document store fuse filesystem is mounted.
331    /// This will typically be `/run/user/$UID/doc/`.
332    ///
333    /// # Specifications
334    ///
335    /// See also [`GetMountPoint`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-getmountpoint).
336    #[doc(alias = "GetMountPoint")]
337    #[doc(alias = "get_mount_point")]
338    pub async fn mount_point(&self) -> Result<FilePath, Error> {
339        self.0.call("GetMountPoint", &()).await
340    }
341
342    /// Grants access permissions for a file in the document store to an
343    /// application.
344    ///
345    /// **Note** This call is available inside the sandbox if the
346    /// application has the [`Permission::GrantPermissions`] for the document.
347    ///
348    /// # Arguments
349    ///
350    /// * `doc_id` - The ID of the file in the document store.
351    /// * `app_id` - The ID of the application to which permissions are granted.
352    /// * `permissions` - The permissions to grant.
353    ///
354    /// # Specifications
355    ///
356    /// See also [`GrantPermissions`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-grantpermissions).
357    #[doc(alias = "GrantPermissions")]
358    pub async fn grant_permissions(
359        &self,
360        doc_id: impl Into<DocumentID>,
361        app_id: &AppID,
362        permissions: &[Permission],
363    ) -> Result<(), Error> {
364        self.0
365            .call("GrantPermissions", &(doc_id.into(), app_id, permissions))
366            .await
367    }
368
369    /// Gets the filesystem path and application permissions for a document
370    /// store entry.
371    ///
372    /// **Note** This call is not available inside the sandbox.
373    ///
374    /// # Arguments
375    ///
376    /// * `doc_id` - The ID of the file in the document store.
377    ///
378    /// # Returns
379    ///
380    /// The path of the file in the host filesystem along with the
381    /// [`Permissions`].
382    ///
383    /// # Specifications
384    ///
385    /// See also [`Info`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-info).
386    #[doc(alias = "Info")]
387    pub async fn info(
388        &self,
389        doc_id: impl Into<DocumentID>,
390    ) -> Result<(FilePath, Permissions), Error> {
391        self.0.call("Info", &(doc_id.into())).await
392    }
393
394    /// Lists documents in the document store for an application (or for all
395    /// applications).
396    ///
397    /// **Note** This call is not available inside the sandbox.
398    ///
399    /// # Arguments
400    ///
401    /// * `app-id` - The application ID, or `None` to list all documents.
402    ///
403    /// # Returns
404    ///
405    /// [`HashMap`] mapping document IDs to their filesystem path on the host
406    /// system.
407    ///
408    /// # Specifications
409    ///
410    /// See also [`List`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-list).
411    #[doc(alias = "List")]
412    pub async fn list(
413        &self,
414        app_id: Option<&AppID>,
415    ) -> Result<HashMap<DocumentID, FilePath>, Error> {
416        let app_id = app_id.map(|id| id.as_ref()).unwrap_or("");
417        let response: HashMap<String, FilePath> = self.0.call("List", &(app_id)).await?;
418
419        let mut new_response: HashMap<DocumentID, FilePath> = HashMap::new();
420        for (key, file_name) in response {
421            new_response.insert(DocumentID::from(key), file_name);
422        }
423
424        Ok(new_response)
425    }
426
427    /// Looks up the document ID for a file.
428    ///
429    /// **Note** This call is not available inside the sandbox.
430    ///
431    /// # Arguments
432    ///
433    /// * `filename` - A path in the host filesystem.
434    ///
435    /// # Returns
436    ///
437    /// The ID of the file in the document store, or [`None`] if the file is not
438    /// in the document store.
439    ///
440    /// # Specifications
441    ///
442    /// See also [`Lookup`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-lookup).
443    #[doc(alias = "Lookup")]
444    pub async fn lookup(&self, filename: impl AsRef<Path>) -> Result<Option<DocumentID>, Error> {
445        let filename = FilePath::new(filename)?;
446        let doc_id: String = self.0.call("Lookup", &(filename)).await?;
447        if doc_id.is_empty() {
448            Ok(None)
449        } else {
450            Ok(Some(doc_id.into()))
451        }
452    }
453
454    /// Revokes access permissions for a file in the document store from an
455    /// application.
456    ///
457    /// **Note** This call is available inside the sandbox if the
458    /// application has the [`Permission::GrantPermissions`] for the document.
459    ///
460    /// # Arguments
461    ///
462    /// * `doc_id` - The ID of the file in the document store.
463    /// * `app_id` - The ID of the application from which the permissions are
464    ///   revoked.
465    /// * `permissions` - The permissions to revoke.
466    ///
467    /// # Specifications
468    ///
469    /// See also [`RevokePermissions`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-revokepermissions).
470    #[doc(alias = "RevokePermissions")]
471    pub async fn revoke_permissions(
472        &self,
473        doc_id: impl Into<DocumentID>,
474        app_id: &AppID,
475        permissions: &[Permission],
476    ) -> Result<(), Error> {
477        self.0
478            .call("RevokePermissions", &(doc_id.into(), app_id, permissions))
479            .await
480    }
481
482    /// Retrieves the host filesystem paths from their document IDs.
483    ///
484    /// # Arguments
485    ///
486    /// * `doc_ids` - A list of file IDs in the document store.
487    ///
488    /// # Returns
489    ///
490    /// A dictionary mapping document IDs to the paths in the host filesystem
491    ///
492    /// # Specifications
493    ///
494    /// See also [`GetHostPaths`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-gethostpaths).
495    #[doc(alias = "GetHostPaths")]
496    pub async fn host_paths(
497        &self,
498        doc_ids: &[DocumentID],
499    ) -> Result<HashMap<DocumentID, FilePath>, Error> {
500        self.0.call_versioned("GetHostPaths", &(doc_ids,), 5).await
501    }
502}
503
504impl<'a> std::ops::Deref for Documents<'a> {
505    type Target = zbus::Proxy<'a>;
506
507    fn deref(&self) -> &Self::Target {
508        &self.0
509    }
510}
511
512/// Interact with `org.freedesktop.portal.FileTransfer` interface.
513mod file_transfer;
514
515pub use file_transfer::FileTransfer;
516
517#[cfg(test)]
518mod tests {
519    use std::collections::HashMap;
520
521    use zbus::zvariant::Type;
522
523    use crate::{app_id::DocumentID, documents::Permission, FilePath};
524
525    #[test]
526    fn serialize_deserialize() {
527        let permission = Permission::GrantPermissions;
528        let string = serde_json::to_string(&permission).unwrap();
529        assert_eq!(string, "\"grant-permissions\"");
530
531        let decoded = serde_json::from_str(&string).unwrap();
532        assert_eq!(permission, decoded);
533
534        assert_eq!(HashMap::<DocumentID, FilePath>::SIGNATURE, "a{say}");
535    }
536}