1 | /* |
2 | * Copyright (C) 2018 Matthias Clasen |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | #include "config.h" |
19 | |
20 | #include <errno.h> |
21 | |
22 | #include <sys/types.h> |
23 | #include <sys/stat.h> |
24 | #include <fcntl.h> |
25 | |
26 | #include "gdkcontentformats.h" |
27 | #include "gdkcontentserializer.h" |
28 | #include "gdkcontentdeserializer.h" |
29 | |
30 | #include <gio/gio.h> |
31 | |
32 | #ifdef G_OS_UNIX |
33 | |
34 | #include <gio/gunixfdlist.h> |
35 | |
36 | #ifndef O_PATH |
37 | #define O_PATH 0 |
38 | #endif |
39 | |
40 | #ifndef O_CLOEXEC |
41 | #define O_CLOEXEC 0 |
42 | #else |
43 | #define HAVE_O_CLOEXEC 1 |
44 | #endif |
45 | |
46 | #include "filetransferportalprivate.h" |
47 | |
48 | static GDBusProxy *file_transfer_proxy = NULL; |
49 | |
50 | typedef struct { |
51 | GTask *task; |
52 | const char **files; |
53 | int len; |
54 | int start; |
55 | } AddFileData; |
56 | |
57 | static void add_files (GDBusProxy *proxy, |
58 | AddFileData *afd); |
59 | |
60 | static void |
61 | add_files_done (GObject *object, |
62 | GAsyncResult *result, |
63 | gpointer data) |
64 | { |
65 | GDBusProxy *proxy = G_DBUS_PROXY (object); |
66 | AddFileData *afd = data; |
67 | GError *error = NULL; |
68 | GVariant *ret; |
69 | |
70 | ret = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, NULL, res: result, error: &error); |
71 | if (ret == NULL) |
72 | { |
73 | g_task_return_error (task: afd->task, error); |
74 | g_object_unref (object: afd->task); |
75 | g_free (mem: afd); |
76 | return; |
77 | } |
78 | |
79 | g_variant_unref (value: ret); |
80 | |
81 | if (afd->start >= afd->len) |
82 | { |
83 | g_task_return_boolean (task: afd->task, TRUE); |
84 | g_object_unref (object: afd->task); |
85 | g_free (mem: afd); |
86 | return; |
87 | } |
88 | |
89 | add_files (proxy, afd); |
90 | } |
91 | |
92 | /* We call AddFiles in chunks of 16 to avoid running into |
93 | * the per-message fd limit of the bus. |
94 | */ |
95 | static void |
96 | add_files (GDBusProxy *proxy, |
97 | AddFileData *afd) |
98 | { |
99 | GUnixFDList *fd_list; |
100 | GVariantBuilder fds, options; |
101 | int i; |
102 | char *key; |
103 | |
104 | g_variant_builder_init (builder: &fds, G_VARIANT_TYPE ("ah" )); |
105 | fd_list = g_unix_fd_list_new (); |
106 | |
107 | for (i = 0; afd->files[afd->start + i]; i++) |
108 | { |
109 | int fd; |
110 | int fd_in; |
111 | GError *error = NULL; |
112 | |
113 | if (i == 16) |
114 | break; |
115 | |
116 | fd = open (file: afd->files[afd->start + i], O_PATH | O_CLOEXEC); |
117 | if (fd == -1) |
118 | { |
119 | g_task_return_new_error (task: afd->task, G_IO_ERROR, code: g_io_error_from_errno (errno), |
120 | format: "Failed to open %s" , afd->files[afd->start + i]); |
121 | g_object_unref (object: afd->task); |
122 | g_free (mem: afd); |
123 | g_object_unref (object: fd_list); |
124 | return; |
125 | } |
126 | |
127 | #ifndef HAVE_O_CLOEXEC |
128 | fcntl (fd, F_SETFD, FD_CLOEXEC); |
129 | #endif |
130 | fd_in = g_unix_fd_list_append (list: fd_list, fd, error: &error); |
131 | close (fd: fd); |
132 | |
133 | if (fd_in == -1) |
134 | { |
135 | g_task_return_error (task: afd->task, error); |
136 | g_object_unref (object: afd->task); |
137 | g_free (mem: afd); |
138 | g_object_unref (object: fd_list); |
139 | return; |
140 | } |
141 | |
142 | g_variant_builder_add (builder: &fds, format_string: "h" , fd_in); |
143 | } |
144 | |
145 | afd->start += 16; |
146 | |
147 | key = (char *)g_object_get_data (G_OBJECT (afd->task), key: "key" ); |
148 | |
149 | g_variant_builder_init (builder: &options, G_VARIANT_TYPE_VARDICT); |
150 | g_dbus_proxy_call_with_unix_fd_list (proxy, |
151 | method_name: "AddFiles" , |
152 | parameters: g_variant_new (format_string: "(saha{sv})" , key, &fds, &options), |
153 | flags: 0, timeout_msec: -1, |
154 | fd_list, |
155 | NULL, |
156 | callback: add_files_done, user_data: afd); |
157 | |
158 | g_object_unref (object: fd_list); |
159 | } |
160 | |
161 | static void |
162 | start_session_done (GObject *object, |
163 | GAsyncResult *result, |
164 | gpointer data) |
165 | { |
166 | GDBusProxy *proxy = G_DBUS_PROXY (object); |
167 | AddFileData *afd = data; |
168 | GError *error = NULL; |
169 | GVariant *ret; |
170 | const char *key; |
171 | |
172 | ret = g_dbus_proxy_call_finish (proxy, res: result, error: &error); |
173 | if (ret == NULL) |
174 | { |
175 | g_task_return_error (task: afd->task, error); |
176 | g_object_unref (object: afd->task); |
177 | g_free (mem: afd); |
178 | return; |
179 | } |
180 | |
181 | g_variant_get (value: ret, format_string: "(&s)" , &key); |
182 | |
183 | g_object_set_data_full (G_OBJECT (afd->task), key: "key" , data: g_strdup (str: key), destroy: g_free); |
184 | |
185 | g_variant_unref (value: ret); |
186 | |
187 | add_files (proxy, afd); |
188 | } |
189 | |
190 | void |
191 | file_transfer_portal_register_files (const char **files, |
192 | gboolean writable, |
193 | GAsyncReadyCallback callback, |
194 | gpointer data) |
195 | { |
196 | GTask *task; |
197 | AddFileData *afd; |
198 | GVariantBuilder options; |
199 | |
200 | task = g_task_new (NULL, NULL, callback, callback_data: data); |
201 | |
202 | if (file_transfer_proxy == NULL) |
203 | { |
204 | g_task_return_new_error (task, G_IO_ERROR, code: G_IO_ERROR_NOT_SUPPORTED, |
205 | format: "No portal found" ); |
206 | g_object_unref (object: task); |
207 | return; |
208 | } |
209 | |
210 | afd = g_new (AddFileData, 1); |
211 | afd->task = task; |
212 | afd->files = files; |
213 | afd->len = g_strv_length (str_array: (char **)files); |
214 | afd->start = 0; |
215 | |
216 | g_variant_builder_init (builder: &options, G_VARIANT_TYPE_VARDICT); |
217 | g_variant_builder_add (builder: &options, format_string: "{sv}" , "writable" , g_variant_new_boolean (value: writable)); |
218 | g_variant_builder_add (builder: &options, format_string: "{sv}" , "autostop" , g_variant_new_boolean (TRUE)); |
219 | |
220 | g_dbus_proxy_call (proxy: file_transfer_proxy, method_name: "StartTransfer" , |
221 | parameters: g_variant_new (format_string: "(a{sv})" , &options), |
222 | flags: 0, timeout_msec: -1, NULL, callback: start_session_done, user_data: afd); |
223 | } |
224 | |
225 | gboolean |
226 | file_transfer_portal_register_files_finish (GAsyncResult *result, |
227 | char **key, |
228 | GError **error) |
229 | { |
230 | if (g_task_propagate_boolean (G_TASK (result), error)) |
231 | { |
232 | *key = g_strdup (str: g_object_get_data (G_OBJECT (result), key: "key" )); |
233 | return TRUE; |
234 | } |
235 | |
236 | return FALSE; |
237 | } |
238 | |
239 | static void |
240 | retrieve_files_done (GObject *object, |
241 | GAsyncResult *result, |
242 | gpointer data) |
243 | { |
244 | GDBusProxy *proxy = G_DBUS_PROXY (object); |
245 | GTask *task = data; |
246 | GError *error = NULL; |
247 | GVariant *ret; |
248 | char **files; |
249 | |
250 | ret = g_dbus_proxy_call_finish (proxy, res: result, error: &error); |
251 | if (ret == NULL) |
252 | { |
253 | g_task_return_error (task, error); |
254 | g_object_unref (object: task); |
255 | return; |
256 | } |
257 | |
258 | g_variant_get (value: ret, format_string: "(^a&s)" , &files); |
259 | |
260 | g_object_set_data_full (G_OBJECT (task), key: "files" , data: g_strdupv (str_array: files), destroy: (GDestroyNotify)g_strfreev); |
261 | |
262 | g_variant_unref (value: ret); |
263 | |
264 | g_task_return_boolean (task, TRUE); |
265 | } |
266 | |
267 | void |
268 | file_transfer_portal_retrieve_files (const char *key, |
269 | GAsyncReadyCallback callback, |
270 | gpointer data) |
271 | { |
272 | GTask *task; |
273 | GVariantBuilder options; |
274 | |
275 | task = g_task_new (NULL, NULL, callback, callback_data: data); |
276 | |
277 | if (file_transfer_proxy == NULL) |
278 | { |
279 | g_task_return_new_error (task, G_IO_ERROR, code: G_IO_ERROR_NOT_SUPPORTED, |
280 | format: "No portal found" ); |
281 | g_object_unref (object: task); |
282 | return; |
283 | } |
284 | |
285 | g_variant_builder_init (builder: &options, G_VARIANT_TYPE_VARDICT); |
286 | g_dbus_proxy_call (proxy: file_transfer_proxy, |
287 | method_name: "RetrieveFiles" , |
288 | parameters: g_variant_new (format_string: "(sa{sv})" , key, &options), |
289 | flags: 0, timeout_msec: -1, NULL, |
290 | callback: retrieve_files_done, user_data: task); |
291 | } |
292 | |
293 | gboolean |
294 | file_transfer_portal_retrieve_files_finish (GAsyncResult *result, |
295 | char ***files, |
296 | GError **error) |
297 | { |
298 | if (g_task_propagate_boolean (G_TASK (result), error)) |
299 | { |
300 | *files = g_strdupv (str_array: g_object_get_data (G_OBJECT (result), key: "files" )); |
301 | return TRUE; |
302 | } |
303 | |
304 | return FALSE; |
305 | } |
306 | |
307 | |
308 | /* serializer */ |
309 | |
310 | static void |
311 | file_serializer_finish (GObject *source, |
312 | GAsyncResult *result, |
313 | gpointer serializer) |
314 | { |
315 | GOutputStream *stream = G_OUTPUT_STREAM (source); |
316 | GError *error = NULL; |
317 | |
318 | if (!g_output_stream_write_all_finish (stream, result, NULL, error: &error)) |
319 | gdk_content_serializer_return_error (serializer, error); |
320 | else |
321 | gdk_content_serializer_return_success (serializer); |
322 | } |
323 | |
324 | static void |
325 | portal_ready (GObject *object, |
326 | GAsyncResult *result, |
327 | gpointer serializer) |
328 | { |
329 | GError *error = NULL; |
330 | char *key; |
331 | |
332 | if (!file_transfer_portal_register_files_finish (result, key: &key, error: &error)) |
333 | { |
334 | gdk_content_serializer_return_error (serializer, error); |
335 | return; |
336 | } |
337 | |
338 | g_output_stream_write_all_async (stream: gdk_content_serializer_get_output_stream (serializer), |
339 | buffer: key, |
340 | count: strlen (s: key) + 1, |
341 | io_priority: gdk_content_serializer_get_priority (serializer), |
342 | cancellable: gdk_content_serializer_get_cancellable (serializer), |
343 | callback: file_serializer_finish, |
344 | user_data: serializer); |
345 | gdk_content_serializer_set_task_data (serializer, data: key, notify: g_free); |
346 | } |
347 | |
348 | static void |
349 | portal_file_serializer (GdkContentSerializer *serializer) |
350 | { |
351 | GFile *file; |
352 | const GValue *value; |
353 | GPtrArray *files; |
354 | |
355 | files = g_ptr_array_new_with_free_func (element_free_func: g_free); |
356 | |
357 | value = gdk_content_serializer_get_value (serializer); |
358 | |
359 | if (G_VALUE_HOLDS (value, G_TYPE_FILE)) |
360 | { |
361 | file = g_value_get_object (value: gdk_content_serializer_get_value (serializer)); |
362 | if (file) |
363 | g_ptr_array_add (array: files, data: g_file_get_path (file)); |
364 | g_ptr_array_add (array: files, NULL); |
365 | } |
366 | else if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) |
367 | { |
368 | GSList *l; |
369 | |
370 | for (l = g_value_get_boxed (value); l; l = l->next) |
371 | g_ptr_array_add (array: files, data: g_file_get_path (file: l->data)); |
372 | |
373 | g_ptr_array_add (array: files, NULL); |
374 | } |
375 | |
376 | /* this call doesn't copy the strings, so keep the array around until the registration is done */ |
377 | file_transfer_portal_register_files (files: (const char **)files->pdata, TRUE, callback: portal_ready, data: serializer); |
378 | gdk_content_serializer_set_task_data (serializer, data: files, notify: (GDestroyNotify)g_ptr_array_unref); |
379 | } |
380 | |
381 | /* deserializer */ |
382 | |
383 | static void |
384 | portal_finish (GObject *object, |
385 | GAsyncResult *result, |
386 | gpointer deserializer) |
387 | { |
388 | char **files = NULL; |
389 | GError *error = NULL; |
390 | GValue *value; |
391 | |
392 | if (!file_transfer_portal_retrieve_files_finish (result, files: &files, error: &error)) |
393 | { |
394 | gdk_content_deserializer_return_error (deserializer, error); |
395 | return; |
396 | } |
397 | |
398 | value = gdk_content_deserializer_get_value (deserializer); |
399 | if (G_VALUE_HOLDS (value, G_TYPE_FILE)) |
400 | { |
401 | if (files[0] != NULL) |
402 | g_value_take_object (value, v_object: g_file_new_for_path (path: files[0])); |
403 | } |
404 | else |
405 | { |
406 | GSList *l = NULL; |
407 | gsize i; |
408 | |
409 | for (i = 0; files[i] != NULL; i++) |
410 | l = g_slist_prepend (list: l, data: g_file_new_for_path (path: files[i])); |
411 | g_value_take_boxed (value, v_boxed: g_slist_reverse (list: l)); |
412 | } |
413 | g_strfreev (str_array: files); |
414 | |
415 | gdk_content_deserializer_return_success (deserializer); |
416 | } |
417 | |
418 | static void |
419 | portal_file_deserializer_finish (GObject *source, |
420 | GAsyncResult *result, |
421 | gpointer deserializer) |
422 | { |
423 | GOutputStream *stream = G_OUTPUT_STREAM (source); |
424 | GError *error = NULL; |
425 | gssize written; |
426 | char *key; |
427 | |
428 | written = g_output_stream_splice_finish (stream, result, error: &error); |
429 | if (written < 0) |
430 | { |
431 | gdk_content_deserializer_return_error (deserializer, error); |
432 | return; |
433 | } |
434 | |
435 | /* write terminating NULL */ |
436 | if (!g_output_stream_write (stream, buffer: "" , count: 1, NULL, error: &error)) |
437 | { |
438 | gdk_content_deserializer_return_error (deserializer, error); |
439 | return; |
440 | } |
441 | |
442 | key = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (stream)); |
443 | if (key == NULL) |
444 | { |
445 | GError *gerror = g_error_new (G_IO_ERROR, |
446 | code: G_IO_ERROR_NOT_FOUND, |
447 | format: "Could not convert data from %s to %s" , |
448 | gdk_content_deserializer_get_mime_type (deserializer), |
449 | g_type_name (type: gdk_content_deserializer_get_gtype (deserializer))); |
450 | gdk_content_deserializer_return_error (deserializer, error: gerror); |
451 | return; |
452 | } |
453 | |
454 | file_transfer_portal_retrieve_files (key, callback: portal_finish, data: deserializer); |
455 | gdk_content_deserializer_set_task_data (deserializer, data: key, notify: g_free); |
456 | } |
457 | |
458 | static void |
459 | portal_file_deserializer (GdkContentDeserializer *deserializer) |
460 | { |
461 | GOutputStream *output; |
462 | |
463 | output = g_memory_output_stream_new_resizable (); |
464 | |
465 | g_output_stream_splice_async (stream: output, |
466 | source: gdk_content_deserializer_get_input_stream (deserializer), |
467 | flags: G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, |
468 | io_priority: gdk_content_deserializer_get_priority (deserializer), |
469 | cancellable: gdk_content_deserializer_get_cancellable (deserializer), |
470 | callback: portal_file_deserializer_finish, |
471 | user_data: deserializer); |
472 | g_object_unref (object: output); |
473 | } |
474 | |
475 | static void |
476 | connection_closed (GDBusConnection *connection, |
477 | gboolean remote_peer_vanished, |
478 | GError *error) |
479 | { |
480 | g_clear_object (&file_transfer_proxy); |
481 | } |
482 | |
483 | static void |
484 | finish_registration (void) |
485 | { |
486 | gdk_content_register_serializer (G_TYPE_FILE, |
487 | mime_type: "application/vnd.portal.filetransfer" , |
488 | serialize: portal_file_serializer, |
489 | NULL, |
490 | NULL); |
491 | |
492 | gdk_content_register_serializer (GDK_TYPE_FILE_LIST, |
493 | mime_type: "application/vnd.portal.filetransfer" , |
494 | serialize: portal_file_serializer, |
495 | NULL, |
496 | NULL); |
497 | |
498 | gdk_content_register_deserializer (mime_type: "application/vnd.portal.filetransfer" , |
499 | GDK_TYPE_FILE_LIST, |
500 | deserialize: portal_file_deserializer, |
501 | NULL, |
502 | NULL); |
503 | |
504 | gdk_content_register_deserializer (mime_type: "application/vnd.portal.filetransfer" , |
505 | G_TYPE_FILE, |
506 | deserialize: portal_file_deserializer, |
507 | NULL, |
508 | NULL); |
509 | |
510 | /* FIXME: I missed up and used the wrong mime type here when |
511 | * I implemented my own protocol. Keep these around for a while |
512 | * so we can interoperate with existing flatpaks using GTK 4.6 |
513 | */ |
514 | gdk_content_register_serializer (G_TYPE_FILE, |
515 | mime_type: "application/vnd.portal.files" , |
516 | serialize: portal_file_serializer, |
517 | NULL, |
518 | NULL); |
519 | |
520 | gdk_content_register_serializer (GDK_TYPE_FILE_LIST, |
521 | mime_type: "application/vnd.portal.files" , |
522 | serialize: portal_file_serializer, |
523 | NULL, |
524 | NULL); |
525 | |
526 | gdk_content_register_deserializer (mime_type: "application/vnd.portal.files" , |
527 | GDK_TYPE_FILE_LIST, |
528 | deserialize: portal_file_deserializer, |
529 | NULL, |
530 | NULL); |
531 | |
532 | gdk_content_register_deserializer (mime_type: "application/vnd.portal.files" , |
533 | G_TYPE_FILE, |
534 | deserialize: portal_file_deserializer, |
535 | NULL, |
536 | NULL); |
537 | |
538 | /* Free the singleton when the connection closes, important for test */ |
539 | g_signal_connect (g_dbus_proxy_get_connection (G_DBUS_PROXY (file_transfer_proxy)), |
540 | "closed" , G_CALLBACK (connection_closed), NULL); |
541 | } |
542 | |
543 | static gboolean |
544 | proxy_has_owner (GDBusProxy *proxy) |
545 | { |
546 | char *owner; |
547 | |
548 | owner = g_dbus_proxy_get_name_owner (proxy); |
549 | if (owner) |
550 | { |
551 | g_free (mem: owner); |
552 | return TRUE; |
553 | } |
554 | |
555 | return FALSE; |
556 | } |
557 | |
558 | void |
559 | file_transfer_portal_register (void) |
560 | { |
561 | static gboolean called; |
562 | |
563 | if (!called) |
564 | { |
565 | called = TRUE; |
566 | |
567 | file_transfer_proxy = g_dbus_proxy_new_for_bus_sync (bus_type: G_BUS_TYPE_SESSION, |
568 | flags: G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
569 | | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
570 | | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, |
571 | NULL, |
572 | name: "org.freedesktop.portal.Documents" , |
573 | object_path: "/org/freedesktop/portal/documents" , |
574 | interface_name: "org.freedesktop.portal.FileTransfer" , |
575 | NULL, |
576 | NULL); |
577 | |
578 | if (file_transfer_proxy && !proxy_has_owner (proxy: file_transfer_proxy)) |
579 | g_clear_object (&file_transfer_proxy); |
580 | |
581 | if (file_transfer_proxy) |
582 | finish_registration (); |
583 | } |
584 | } |
585 | |
586 | |
587 | #endif /* G_OS_UNIX */ |
588 | |