1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! # Examples
//!
//! ```rust,no_run
//! use std::{fs::File, os::fd::AsFd};
//!
//! use ashpd::documents::FileTransfer;
//!
//! async fn run() -> ashpd::Result<()> {
//!     let proxy = FileTransfer::new().await?;
//!
//!     let key = proxy.start_transfer(true, true).await?;
//!     let file = File::open("/home/bilelmoussaoui/Downloads/adwaita-night.jpg").unwrap();
//!     proxy.add_files(&key, &[&file.as_fd()]).await?;
//!
//!     // The files would be retrieved by another process
//!     let files = proxy.retrieve_files(&key).await?;
//!     println!("{:#?}", files);
//!
//!     proxy.stop_transfer(&key).await?;
//!
//!     Ok(())
//! }
//! ```

use std::{collections::HashMap, os::fd::BorrowedFd};

use futures_util::Stream;
use zbus::zvariant::{Fd, SerializeDict, Type, Value};

use crate::{proxy::Proxy, Error};

#[derive(SerializeDict, Debug, Type, Default)]
/// Specified options for a [`FileTransfer::start_transfer`] request.
#[zvariant(signature = "dict")]
struct TransferOptions {
    /// Whether to allow the chosen application to write to the files.
    writeable: Option<bool>,
    /// Whether to stop the transfer automatically after the first
    /// [`retrieve_files()`][`FileTransfer::retrieve_files`] call.
    #[zvariant(rename = "autostop")]
    auto_stop: Option<bool>,
}

impl TransferOptions {
    /// Sets whether the chosen application can write to the files or not.
    #[must_use]
    pub fn writeable(mut self, writeable: impl Into<Option<bool>>) -> Self {
        self.writeable = writeable.into();
        self
    }

    /// Whether to stop the transfer automatically after the first
    /// [`retrieve_files()`][`FileTransfer::retrieve_files`] call.
    #[must_use]
    pub fn auto_stop(mut self, auto_stop: impl Into<Option<bool>>) -> Self {
        self.auto_stop = auto_stop.into();
        self
    }
}

/// The interface operates as a middle-man between apps when transferring files
/// via drag-and-drop or copy-paste, taking care of the necessary exporting of
/// files in the document portal.
///
/// Toolkits are expected to use the application/vnd.portal.filetransfer
/// mimetype when using this mechanism for file exchange via copy-paste or
/// drag-and-drop.
///
/// The data that is transmitted with this mimetype should be the key returned
/// by the StartTransfer method. Upon receiving this mimetype, the target should
/// call RetrieveFiles with the key, to obtain the list of files. The portal
/// will take care of exporting files in the document store as necessary to make
/// them accessible to the target.
///
/// Wrapper of the DBus interface: [`org.freedesktop.portal.FileTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html).
#[derive(Debug)]
#[doc(alias = "org.freedesktop.portal.FileTransfer")]
pub struct FileTransfer<'a>(Proxy<'a>);

impl<'a> FileTransfer<'a> {
    /// Create a new instance of [`FileTransfer`].
    pub async fn new() -> Result<FileTransfer<'a>, Error> {
        let proxy = Proxy::new_documents("org.freedesktop.portal.FileTransfer").await?;
        Ok(Self(proxy))
    }

    /// Adds files to a session. This method can be called multiple times on a
    /// given session. **Note** only regular files (not directories) can be
    /// added.
    ///
    /// # Arguments
    ///
    /// * `key` - A key returned by
    ///   [`start_transfer()`][`FileTransfer::start_transfer`].
    /// * `fds` - A list of file descriptors of the files to register.
    ///
    /// # Specifications
    ///
    /// See also [`AddFiles`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-addfiles).
    #[doc(alias = "AddFiles")]
    pub async fn add_files(&self, key: &str, fds: &[&BorrowedFd<'_>]) -> Result<(), Error> {
        // `options` parameter doesn't seems to be used yet
        let options: HashMap<&str, Value<'_>> = HashMap::new();
        let files: Vec<Fd> = fds.iter().map(Fd::from).collect();

        self.0.call("AddFiles", &(key, files, options)).await
    }

    /// Retrieves files that were previously added to the session with
    /// [`add_files()`][`FileTransfer::add_files`]. The files will be
    /// exported in the document portal as-needed for the caller, and they
    /// will be writeable if the owner of the session allowed it.
    ///
    /// # Arguments
    ///
    /// * `key` - A key returned by
    ///   [`start_transfer()`][`FileTransfer::start_transfer`].
    ///
    /// # Returns
    ///
    /// The list of file paths.
    ///
    /// # Specifications
    ///
    /// See also [`RetrieveFiles`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-retrievefiles).
    #[doc(alias = "RetrieveFiles")]
    pub async fn retrieve_files(&self, key: &str) -> Result<Vec<String>, Error> {
        // `options` parameter doesn't seems to be used yet
        // see https://github.com/GNOME/gtk/blob/master/gdk/filetransferportal.c#L284
        let options: HashMap<&str, Value<'_>> = HashMap::new();

        self.0.call("RetrieveFiles", &(key, options)).await
    }

    /// Starts a session for a file transfer.
    /// The caller should call [`add_files()`][`FileTransfer::add_files`]
    /// at least once, to add files to this session.
    ///
    /// # Arguments
    ///
    /// * `writeable` - Sets whether the chosen application can write to the
    ///   files or not.
    /// * `auto_stop` - Whether to stop the transfer automatically after the
    ///   first [`retrieve_files()`][`FileTransfer::retrieve_files`] call.
    ///
    /// # Returns
    ///
    /// Key that can be passed to
    /// [`retrieve_files()`][`FileTransfer::retrieve_files`] to obtain the
    /// files.
    ///
    /// # Specifications
    ///
    /// See also [`StartTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-starttransfer).
    pub async fn start_transfer(&self, writeable: bool, auto_stop: bool) -> Result<String, Error> {
        let options = TransferOptions::default()
            .writeable(writeable)
            .auto_stop(auto_stop);
        self.0.call("StartTransfer", &(options)).await
    }

    /// Ends the transfer.
    /// Further calls to [`add_files()`][`FileTransfer::add_files`] or
    /// [`retrieve_files()`][`FileTransfer::retrieve_files`] for this key
    /// will return an error.
    ///
    /// # Arguments
    ///
    /// * `key` - A key returned by
    ///   [`start_transfer()`][`FileTransfer::start_transfer`].
    ///
    /// # Specifications
    ///
    /// See also [`StopTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-stoptransfer).
    #[doc(alias = "StopTransfer")]
    pub async fn stop_transfer(&self, key: &str) -> Result<(), Error> {
        self.0.call("StopTransfer", &(key)).await
    }

    /// Emitted when the transfer is closed.
    ///
    /// # Returns
    ///
    /// * The key returned by
    ///   [`start_transfer()`][`FileTransfer::start_transfer`].
    ///
    /// # Specifications
    ///
    /// See also [`TransferClosed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-transferclosed).
    #[doc(alias = "TransferClosed")]
    pub async fn transfer_closed(&self) -> Result<impl Stream<Item = String>, Error> {
        self.0.signal("TransferClosed").await
    }
}

impl<'a> std::ops::Deref for FileTransfer<'a> {
    type Target = zbus::Proxy<'a>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}