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}