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}