gvfs/daemon/gvfsbackenddav.c

4201 lines
117 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* GIO - GLib Input, Output and Streaming Library
*
* Copyright (C) 2008 Red Hat, Inc.
* Copyright (C) 2021 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* Author: Christian Kellner <gicmo@gnome.org>
*/
#include <config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <glib/gstdio.h>
#include <glib/gi18n.h>
#include <gio/gio.h>
#include <libsoup/soup.h>
/* LibXML2 includes */
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
#include "gvfsbackenddav.h"
#include "gvfskeyring.h"
#include "gvfsjobmount.h"
#include "gvfsjobopenforread.h"
#include "gvfsjobread.h"
#include "gvfsjobseekread.h"
#include "gvfsjobopenforwrite.h"
#include "gvfsjobwrite.h"
#include "gvfsjobseekwrite.h"
#include "gvfsjobsetdisplayname.h"
#include "gvfsjobqueryinfo.h"
#include "gvfsjobqueryfsinfo.h"
#include "gvfsjobqueryattributes.h"
#include "gvfsjobenumerate.h"
#include "gvfsjobpush.h"
#include "gvfsdaemonprotocol.h"
#include "gvfsdaemonutils.h"
#include "gvfsutils.h"
#ifdef HAVE_AVAHI
#include "gvfsdnssdutils.h"
#include "gvfsdnssdresolver.h"
#endif
typedef struct _MountAuthData MountAuthData;
static void mount_auth_info_free (MountAuthData *info);
#ifdef HAVE_AVAHI
static void dns_sd_resolver_changed (GVfsDnsSdResolver *resolver, GVfsBackendDav *dav_backend);
#endif
static gboolean
soup_authenticate (SoupMessage *msg,
SoupAuth *auth,
gboolean retrying,
gpointer user_data);
typedef struct _AuthInfo {
/* for server authentication */
char *username;
char *password;
char *realm;
GPasswordSave pw_save;
} AuthInfo;
struct _MountAuthData {
SoupSession *session;
GMountSource *mount_source;
gboolean retrying_after_403;
gboolean interactive;
AuthInfo server_auth;
AuthInfo proxy_auth;
};
struct _GVfsBackendDav
{
GVfsBackendHttp parent_instance;
MountAuthData auth_info;
gchar *last_good_path;
/* Used for user-verified secure connections. */
GTlsCertificate *certificate;
GTlsCertificateFlags certificate_errors;
#ifdef HAVE_AVAHI
/* only set if we're handling a [dav|davs]+sd:// mounts */
GVfsDnsSdResolver *resolver;
#endif
};
G_DEFINE_TYPE (GVfsBackendDav, g_vfs_backend_dav, G_VFS_TYPE_BACKEND_HTTP);
static void
g_vfs_backend_dav_finalize (GObject *object)
{
GVfsBackendDav *dav_backend;
dav_backend = G_VFS_BACKEND_DAV (object);
#ifdef HAVE_AVAHI
if (dav_backend->resolver != NULL)
{
g_signal_handlers_disconnect_by_func (dav_backend->resolver, dns_sd_resolver_changed, dav_backend);
g_object_unref (dav_backend->resolver);
}
#endif
mount_auth_info_free (&(dav_backend->auth_info));
g_clear_object (&dav_backend->certificate);
if (G_OBJECT_CLASS (g_vfs_backend_dav_parent_class)->finalize)
(*G_OBJECT_CLASS (g_vfs_backend_dav_parent_class)->finalize) (object);
}
static void
g_vfs_backend_dav_init (GVfsBackendDav *backend)
{
g_vfs_backend_set_user_visible (G_VFS_BACKEND (backend), TRUE);
}
/* ************************************************************************* */
/* Small utility functions */
static gboolean
string_to_uint64 (const char *str, guint64 *value)
{
char *endptr;
*value = g_ascii_strtoull (str, &endptr, 10);
return endptr != str;
}
static inline gboolean
sm_has_header (SoupMessage *msg, const char *header)
{
return soup_message_headers_get_one (soup_message_get_response_headers (msg),
header) != NULL;
}
static char *
path_get_parent_dir (const char *path)
{
char *parent;
size_t len;
len = strlen (path);
while (len > 0 && path[len - 1] == '/')
len--;
if (len == 0)
return g_strdup ("/");
parent = g_strrstr_len (path, len, "/");
if (parent == NULL)
return g_strdup ("/");
return g_strndup (path, (parent - path) + 1);
}
/* message utility functions */
static void
message_add_destination_header (SoupMessage *msg,
GUri *uri)
{
char *string;
string = g_uri_to_string_partial (uri, G_URI_HIDE_PASSWORD);
soup_message_headers_append (soup_message_get_request_headers (msg),
"Destination",
string);
g_free (string);
}
static void
message_add_overwrite_header (SoupMessage *msg,
gboolean overwrite)
{
soup_message_headers_append (soup_message_get_request_headers (msg),
"Overwrite",
overwrite ? "T" : "F");
}
static void
message_add_redirect_header (SoupMessage *msg,
GFileQueryInfoFlags flags)
{
const char *header_redirect;
/* RFC 4437 */
if (flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
header_redirect = "F";
else
header_redirect = "T";
soup_message_headers_append (soup_message_get_request_headers (msg),
"Apply-To-Redirect-Ref",
header_redirect);
}
static inline gboolean
str_equal (const char *a, const char *b, gboolean insensitive)
{
if (a == NULL || b == NULL)
return a == b;
return insensitive ? !g_ascii_strcasecmp (a, b) : !strcmp (a, b);
}
static gboolean
path_equal (const char *a, const char *b, gboolean relax)
{
gboolean res;
size_t a_len, b_len;
if (relax == FALSE)
return str_equal (a, b, FALSE);
if (a == NULL || b == NULL)
return a == b;
a_len = strlen (a);
b_len = strlen (b);
while (a_len > 0 && a[a_len - 1] == '/')
a_len--;
while (b_len > 0 && b[b_len - 1] == '/')
b_len--;
if (a_len == b_len)
res = ! strncmp (a, b, a_len);
else
res = FALSE;
return res;
}
static gboolean
dav_uri_match (GUri *a, GUri *b, gboolean relax)
{
gboolean diff;
char *ua, *ub;
ua = g_uri_unescape_string (g_uri_get_path (a), "/");
ub = g_uri_unescape_string (g_uri_get_path (b), "/");
diff = g_uri_get_port (a) != g_uri_get_port (b) ||
!str_equal (g_uri_get_scheme (a), g_uri_get_scheme (b), FALSE) ||
!str_equal (g_uri_get_host (a), g_uri_get_host (b), TRUE) ||
!path_equal (ua, ub, relax) ||
!str_equal (g_uri_get_query (a), g_uri_get_query (b), FALSE) ||
!str_equal (g_uri_get_fragment (a), g_uri_get_fragment (b), FALSE);
g_free (ua);
g_free (ub);
return !diff;
}
static char *
dav_uri_encode (const char *path_to_encode)
{
char *path;
static const char *allowed_reserved_chars = "/";
path = g_uri_escape_string (path_to_encode,
allowed_reserved_chars,
FALSE);
return path;
}
static gboolean
message_should_apply_redir_ref (SoupMessage *msg)
{
const char *header;
header = soup_message_headers_get_one (soup_message_get_request_headers (msg),
"Apply-To-Redirect-Ref");
if (header == NULL || g_ascii_strcasecmp (header, "T"))
return FALSE;
return TRUE;
}
static GUri *
g_vfs_backend_dav_uri_for_path (GVfsBackend *backend,
const char *path,
gboolean is_dir)
{
GUri *mount_base;
GUri *uri;
char *fn_encoded;
char *new_path;
mount_base = http_backend_get_mount_base (backend);
/* "/" means "whatever mount_base is" */
if (!strcmp (path, "/"))
return g_uri_ref (mount_base);
/* The mount_base path is escaped already so we need to
escape the new path as well */
fn_encoded = dav_uri_encode (path);
/* Otherwise, we append filename to mount_base (which is assumed to
* be a directory in this case).
*
* Add a "/" in cases where it is likely that the url is going
* to be a directory to avoid redirections
*/
if (is_dir == FALSE || g_str_has_suffix (path, "/"))
new_path = g_build_path ("/", g_uri_get_path (mount_base), fn_encoded, NULL);
else
new_path = g_build_path ("/", g_uri_get_path (mount_base), fn_encoded, "/", NULL);
uri = soup_uri_copy (mount_base, SOUP_URI_PATH, new_path, SOUP_URI_NONE);
g_free (fn_encoded);
g_free (new_path);
return uri;
}
static gboolean
g_vfs_backend_dav_stream_skip (GInputStream *stream, GError **error)
{
for (;;)
{
gssize skipped = g_input_stream_skip (stream, 4096, NULL, error);
if (skipped < 0)
{
return FALSE;
}
else if (!skipped)
break;
}
g_input_stream_close (stream, NULL, NULL);
return TRUE;
}
static void
g_vfs_backend_dav_setup_display_name (GVfsBackend *backend)
{
GUri *mount_base;
char *display_name;
char port[7] = {0, };
gint gport;
#ifdef HAVE_AVAHI
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend);
if (dav_backend->resolver != NULL)
{
const char *name;
name = g_vfs_dns_sd_resolver_get_service_name (dav_backend->resolver);
g_vfs_backend_set_display_name (backend, name);
return;
}
#endif
mount_base = http_backend_get_mount_base (backend);
gport = g_uri_get_port (mount_base);
if ((gport > 0) && (gport != 80) && (gport != 443))
g_snprintf (port, sizeof (port), ":%u", g_uri_get_port (mount_base));
if (g_uri_get_user (mount_base) != NULL)
/* Translators: This is the name of the WebDAV share constructed as
"WebDAV as <username> on <hostname>:<port>"; the ":<port>" part is
the second %s and only shown if it is not the default http(s) port. */
display_name = g_strdup_printf (_("%s on %s%s"),
g_uri_get_user (mount_base),
g_uri_get_host (mount_base),
port);
else
display_name = g_strdup_printf ("%s%s",
g_uri_get_host (mount_base),
port);
g_vfs_backend_set_display_name (backend, display_name);
g_free (display_name);
}
static gboolean
accept_certificate (SoupMessage *msg,
GTlsCertificate *certificate,
GTlsCertificateFlags errors,
gpointer user_data)
{
GVfsBackendDav *dav = G_VFS_BACKEND_DAV (user_data);
return (errors == dav->certificate_errors &&
g_tls_certificate_is_same (certificate, dav->certificate));
}
static void
dav_message_connect_signals (SoupMessage *message, GVfsBackend *backend)
{
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend);
/* we always have to connect this as the message can be
* re-sent with a differently set certificate_errors field
*/
g_signal_connect (message, "accept-certificate",
G_CALLBACK (accept_certificate), backend);
g_signal_connect (message, "authenticate",
G_CALLBACK (soup_authenticate),
&dav_backend->auth_info);
}
static void
dav_send_async_with_redir_cb (GObject *source, GAsyncResult *ret, gpointer user_data)
{
SoupSession *session = SOUP_SESSION (source);
SoupMessage *msg = soup_session_get_async_result_message (session, ret);
GInputStream *body;
GError *error = NULL;
GTask *task = user_data;
const char *new_loc;
GUri *new_uri;
GUri *old_uri;
GUri *tmp;
guint status;
gboolean redirect;
body = soup_session_send_finish (session, ret, &error);
if (!body)
goto return_error;
status = soup_message_get_status (msg);
if (!SOUP_STATUS_IS_REDIRECTION (status))
goto return_body;
new_loc = soup_message_headers_get_one (soup_message_get_response_headers (msg),
"Location");
if (new_loc == NULL)
goto return_body;
old_uri = soup_message_get_uri (msg);
new_uri = g_uri_parse_relative (old_uri, new_loc,
SOUP_HTTP_URI_FLAGS, &error);
if (new_uri == NULL)
{
g_object_unref (body);
goto return_error;
}
tmp = new_uri;
new_uri = soup_uri_copy (new_uri,
SOUP_URI_USER, g_uri_get_user (old_uri),
SOUP_URI_AUTH_PARAMS, g_uri_get_auth_params (old_uri),
SOUP_URI_NONE);
g_uri_unref (tmp);
/* Check if this is a trailing slash redirect (i.e. /a/b to /a/b/),
* redirect it right away
*/
redirect = dav_uri_match (new_uri, old_uri, TRUE);
if (redirect)
{
const char *dest;
dest = soup_message_headers_get_one (soup_message_get_request_headers (msg),
"Destination");
if (dest && g_str_has_suffix (dest, "/") == FALSE)
{
char *new_dest = g_strconcat (dest, "/", NULL);
soup_message_headers_replace (soup_message_get_request_headers (msg),
"Destination",
new_dest);
g_free (new_dest);
}
}
else if (message_should_apply_redir_ref (msg))
{
if (status == SOUP_STATUS_MOVED_PERMANENTLY ||
status == SOUP_STATUS_TEMPORARY_REDIRECT)
{
const char *method = soup_message_get_method (msg);
/* Only cross-site redirect safe methods */
if (method == SOUP_METHOD_GET &&
method == SOUP_METHOD_HEAD &&
method == SOUP_METHOD_OPTIONS &&
method == SOUP_METHOD_PROPFIND)
redirect = TRUE;
}
/* Two possibilities:
*
* 1) It's a non-redirecty 3xx response (300, 304,
* 305, 306)
* 2) It's some newly-defined 3xx response (308+)
*
* We ignore both of these cases. In the first,
* redirecting would be explicitly wrong, and in the
* last case, we have no clue if the 3xx response is
* supposed to be redirecty or non-redirecty. Plus,
* 2616 says unrecognized status codes should be
* treated as the equivalent to the x00 code, and we
* don't redirect on 300, so therefore we shouldn't
* redirect on 308+ either.
*/
}
if (!redirect)
{
g_uri_unref (new_uri);
goto return_body;
}
if (!g_vfs_backend_dav_stream_skip (body, &error))
{
g_object_unref (body);
goto return_error;
}
g_object_unref (body);
soup_message_set_uri (msg, new_uri);
g_uri_unref (new_uri);
/* recurse */
soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, NULL,
dav_send_async_with_redir_cb, g_object_ref (task));
goto return_done;
return_body:
g_task_return_pointer (task, body, g_object_unref);
goto return_done;
return_error:
g_task_return_error (task, error);
return_done:
g_object_unref (task);
}
static void
g_vfs_backend_dav_send_async (GVfsBackend *backend,
SoupMessage *message,
GAsyncReadyCallback callback,
gpointer user_data)
{
SoupSession *session = G_VFS_BACKEND_HTTP (backend)->session;
GTask *task = g_task_new (backend, NULL, callback, user_data);
g_task_set_source_tag (task, g_vfs_backend_dav_send_async);
g_task_set_task_data (task, g_object_ref (message), g_object_unref);
soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
soup_session_send_async (session, message, G_PRIORITY_DEFAULT, NULL,
dav_send_async_with_redir_cb, task);
}
static GInputStream *
g_vfs_backend_dav_send_finish (GVfsBackend *backend,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (G_VFS_IS_BACKEND (backend), NULL);
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
g_return_val_if_fail (g_task_is_valid (result, backend), NULL);
g_return_val_if_fail (g_async_result_is_tagged (result, g_vfs_backend_dav_send_async), NULL);
return g_task_propagate_pointer (G_TASK (result), error);
}
static SoupMessage *
g_vfs_backend_dav_get_async_result_message (GVfsBackend *backend,
GAsyncResult *result)
{
g_return_val_if_fail (G_VFS_IS_BACKEND (backend), NULL);
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
g_return_val_if_fail (g_task_is_valid (result, backend), NULL);
return g_task_get_task_data (G_TASK (result));
}
/* ************************************************************************* */
/* generic xml parsing functions */
static inline gboolean
node_has_name (xmlNodePtr node, const char *name)
{
g_return_val_if_fail (node != NULL, FALSE);
return ! strcmp ((char *) node->name, name);
}
static inline gboolean
node_has_name_ns (xmlNodePtr node, const char *name, const char *ns_href)
{
gboolean has_name;
gboolean has_ns;
g_return_val_if_fail (node != NULL, FALSE);
has_name = has_ns = TRUE;
if (name)
has_name = node->name && ! strcmp ((char *) node->name, name);
if (ns_href)
has_ns = node->ns && node->ns->href &&
! g_ascii_strcasecmp ((char *) node->ns->href, ns_href);
return has_name && has_ns;
}
static inline gboolean
node_is_element (xmlNodePtr node)
{
return node->type == XML_ELEMENT_NODE && node->name != NULL;
}
static inline gboolean
node_is_element_with_name (xmlNodePtr node, const char *name)
{
return node->type == XML_ELEMENT_NODE &&
node->name != NULL &&
! strcmp ((char *) node->name, name);
}
static const char *
node_get_content (xmlNodePtr node)
{
if (node == NULL)
return NULL;
switch (node->type)
{
case XML_ELEMENT_NODE:
return node_get_content (node->children);
break;
case XML_TEXT_NODE:
return (const char *) node->content;
break;
default:
return NULL;
}
}
static gboolean
node_is_empty (xmlNodePtr node)
{
if (node == NULL)
return TRUE;
if (node->type == XML_TEXT_NODE)
return node->content == NULL || node->content[0] == '\0';
return node->children == NULL;
}
typedef struct _xmlNodeIter {
xmlNodePtr cur_node;
xmlNodePtr next_node;
const char *name;
const char *ns_href;
void *user_data;
} xmlNodeIter;
static xmlNodePtr
xml_node_iter_next (xmlNodeIter *iter)
{
xmlNodePtr node;
while ((node = iter->next_node))
{
iter->next_node = node->next;
if (node->type == XML_ELEMENT_NODE) {
if (node_has_name_ns (node, iter->name, iter->ns_href))
break;
}
}
iter->cur_node = node;
return node;
}
static void *
xml_node_iter_get_user_data (xmlNodeIter *iter)
{
return iter->user_data;
}
static xmlNodePtr
xml_node_iter_get_current (xmlNodeIter *iter)
{
return iter->cur_node;
}
static int
xml_read_cb (void *ctx, char *buf, int len)
{
return g_input_stream_read (ctx, buf, len, NULL, NULL);
}
static xmlDocPtr
parse_xml (SoupMessage *msg,
GInputStream *body,
xmlNodePtr *root,
const char *name,
GError **error)
{
xmlDocPtr doc;
if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg)))
{
g_set_error (error,
G_IO_ERROR,
http_error_code_from_status (soup_message_get_status (msg)),
_("HTTP Error: %s"), soup_message_get_reason_phrase (msg));
return NULL;
}
doc = xmlReadIO (xml_read_cb, NULL, body, "response.xml", NULL,
XML_PARSE_NONET |
XML_PARSE_NOWARNING |
XML_PARSE_NOBLANKS |
XML_PARSE_NSCLEAN |
XML_PARSE_NOCDATA |
XML_PARSE_COMPACT);
if (doc == NULL)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not parse response"));
return NULL;
}
*root = xmlDocGetRootElement (doc);
if (*root == NULL || (*root)->children == NULL)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Empty response"));
xmlFreeDoc (doc);
return NULL;
}
if (strcmp ((char *) (*root)->name, name))
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Unexpected reply from server"));
xmlFreeDoc (doc);
return NULL;
}
return doc;
}
/* ************************************************************************* */
/* Multistatus parsing code */
typedef struct _Multistatus Multistatus;
typedef struct _MsResponse MsResponse;
typedef struct _MsPropstat MsPropstat;
struct _Multistatus {
xmlDocPtr doc;
xmlNodePtr root;
GUri *target;
char *path;
};
struct _MsResponse {
Multistatus *multistatus;
char *path;
gboolean is_target;
xmlNodePtr first_propstat;
};
struct _MsPropstat {
Multistatus *multistatus;
xmlNodePtr prop_node;
guint status_code;
};
static gboolean
multistatus_parse (SoupMessage *msg,
GInputStream *body,
Multistatus *multistatus,
GError **error)
{
xmlDocPtr doc;
xmlNodePtr root;
GUri *uri;
doc = parse_xml (msg, body, &root, "multistatus", error);
if (doc == NULL)
return FALSE;
uri = soup_message_get_uri (msg);
multistatus->doc = doc;
multistatus->root = root;
multistatus->target = uri;
multistatus->path = g_uri_unescape_string (g_uri_get_path (uri), "/");
return TRUE;
}
static void
multistatus_free (Multistatus *multistatus)
{
xmlFreeDoc (multistatus->doc);
g_free (multistatus->path);
}
static void
multistatus_get_response_iter (Multistatus *multistatus, xmlNodeIter *iter)
{
iter->cur_node = multistatus->root->children;
iter->next_node = multistatus->root->children;
iter->name = "response";
iter->ns_href = "DAV:";
iter->user_data = multistatus;
}
static gboolean
multistatus_get_response (xmlNodeIter *resp_iter, MsResponse *response)
{
Multistatus *multistatus;
xmlNodePtr resp_node;
xmlNodePtr iter;
xmlNodePtr href;
xmlNodePtr propstat;
GUri *uri;
const char *text;
char *path;
multistatus = xml_node_iter_get_user_data (resp_iter);
resp_node = xml_node_iter_get_current (resp_iter);
if (resp_node == NULL)
return FALSE;
propstat = NULL;
href = NULL;
for (iter = resp_node->children; iter; iter = iter->next)
{
if (! node_is_element (iter))
{
continue;
}
else if (node_has_name_ns (iter, "href", "DAV:"))
{
href = iter;
}
else if (node_has_name_ns (iter, "propstat", "DAV:"))
{
if (propstat == NULL)
propstat = iter;
}
if (href && propstat)
break;
}
if (href == NULL)
return FALSE;
text = node_get_content (href);
if (text == NULL)
return FALSE;
uri = g_uri_parse_relative (multistatus->target, text, SOUP_HTTP_URI_FLAGS, NULL);
if (uri == NULL)
return FALSE;
path = g_uri_unescape_string (g_uri_get_path (uri), "/");
g_uri_unref (uri);
response->path = path;
response->is_target = path_equal (path, multistatus->path, TRUE);
response->multistatus = multistatus;
response->first_propstat = propstat;
return resp_node != NULL;
}
static void
ms_response_clear (MsResponse *response)
{
g_free (response->path);
}
static char *
ms_response_get_basename (MsResponse *response)
{
return http_path_get_basename (response->path);
}
static void
ms_response_get_propstat_iter (MsResponse *response, xmlNodeIter *iter)
{
iter->cur_node = response->first_propstat;
iter->next_node = response->first_propstat;
iter->name = "propstat";
iter->ns_href = "DAV:";
iter->user_data = response;
}
static guint
ms_response_get_propstat (xmlNodeIter *cur_node, MsPropstat *propstat)
{
MsResponse *response;
xmlNodePtr pstat_node;
xmlNodePtr iter;
xmlNodePtr prop;
xmlNodePtr status;
const char *status_text;
gboolean res;
guint code;
response = xml_node_iter_get_user_data (cur_node);
pstat_node = xml_node_iter_get_current (cur_node);
if (pstat_node == NULL)
return 0;
status = NULL;
prop = NULL;
for (iter = pstat_node->children; iter; iter = iter->next)
{
if (!node_is_element (iter))
{
continue;
}
else if (node_has_name_ns (iter, "status", "DAV:"))
{
status = iter;
}
else if (node_has_name_ns (iter, "prop", "DAV:"))
{
prop = iter;
}
if (status && prop)
break;
}
status_text = node_get_content (status);
if (status_text == NULL || prop == NULL)
return 0;
res = soup_headers_parse_status_line ((char *) status_text,
NULL,
&code,
NULL);
if (res == FALSE)
return 0;
propstat->prop_node = prop;
propstat->status_code = code;
propstat->multistatus = response->multistatus;
return code;
}
static GFileType
parse_resourcetype (xmlNodePtr rt)
{
xmlNodePtr node;
GFileType type;
for (node = rt->children; node; node = node->next)
{
if (node_is_element (node))
break;
}
if (node == NULL)
return G_FILE_TYPE_REGULAR;
if (! strcmp ((char *) node->name, "collection"))
type = G_FILE_TYPE_DIRECTORY;
else if (! strcmp ((char *) node->name, "redirectref"))
type = G_FILE_TYPE_SYMBOLIC_LINK;
else
type = G_FILE_TYPE_UNKNOWN;
return type;
}
static inline void
file_info_set_content_type (GFileInfo *info,
const char *type,
gboolean uncertain_content_type)
{
if (!uncertain_content_type)
g_file_info_set_content_type (info, type);
g_file_info_set_attribute_string (info,
G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE,
type);
}
static void
ms_response_to_file_info (MsResponse *response,
GFileInfo *info)
{
xmlNodeIter iter;
MsPropstat propstat;
xmlNodePtr node;
guint status;
char *basename;
const char *text;
GFileType file_type;
char *mime_type;
gboolean uncertain_content_type;
GIcon *icon;
GIcon *symbolic_icon;
gboolean have_display_name;
basename = ms_response_get_basename (response);
g_file_info_set_name (info, basename);
g_file_info_set_edit_name (info, basename);
if (basename && basename[0] == '.')
g_file_info_set_is_hidden (info, TRUE);
g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE);
file_type = G_FILE_TYPE_REGULAR;
mime_type = NULL;
uncertain_content_type = FALSE;
have_display_name = FALSE;
ms_response_get_propstat_iter (response, &iter);
while (xml_node_iter_next (&iter))
{
status = ms_response_get_propstat (&iter, &propstat);
if (! SOUP_STATUS_IS_SUCCESSFUL (status))
continue;
for (node = propstat.prop_node->children; node; node = node->next)
{
if (! node_is_element (node) || node_is_empty (node))
continue; /* TODO: check namespace, parse user data nodes*/
text = node_get_content (node);
if (node_has_name (node, "resourcetype"))
{
file_type = parse_resourcetype (node);
}
else if (node_has_name (node, "displayname") && text)
{
g_file_info_set_display_name (info, text);
have_display_name = TRUE;
}
else if (node_has_name (node, "getetag"))
{
g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE,
text);
}
else if (node_has_name (node, "creationdate"))
{
GDateTime *dt;
if ((dt = g_date_time_new_from_iso8601 (text, NULL)) == NULL)
continue;
g_file_info_set_attribute_uint64 (info,
G_FILE_ATTRIBUTE_TIME_CREATED,
g_date_time_to_unix (dt));
g_date_time_unref (dt);
}
else if (node_has_name (node, "getcontenttype"))
{
char *ptr;
mime_type = g_strdup (text);
/* Ignore parameters of the content type */
ptr = strchr (mime_type, ';');
if (ptr)
{
do
*ptr-- = '\0';
while (ptr >= mime_type && g_ascii_isspace (*ptr));
}
}
else if (node_has_name (node, "getcontentlength"))
{
gint64 size;
size = g_ascii_strtoll (text, NULL, 10);
g_file_info_set_size (info, size);
}
else if (node_has_name (node, "getlastmodified"))
{
GDateTime *gd;
gd = soup_date_time_new_from_http_string (text);
if (gd)
{
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED, g_date_time_to_unix (gd));
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, 0);
g_date_time_unref (gd);
}
}
}
}
g_file_info_set_file_type (info, file_type);
if (file_type == G_FILE_TYPE_DIRECTORY)
{
g_clear_pointer (&mime_type, g_free);
mime_type = g_strdup ("inode/directory");
icon = g_content_type_get_icon (mime_type);
symbolic_icon = g_content_type_get_symbolic_icon (mime_type);
file_info_set_content_type (info, mime_type, FALSE);
/* Ignore file size for directories. Most of the servers don't report it
* for directories anyway. However, some servers report total size of
* files inside the directory, which is not expected and causes issues
* for clients.
*/
g_file_info_remove_attribute (info, G_FILE_ATTRIBUTE_STANDARD_SIZE);
}
else
{
if (mime_type == NULL)
mime_type = g_content_type_guess (basename, NULL, 0, &uncertain_content_type);
icon = g_content_type_get_icon (mime_type);
if (G_IS_THEMED_ICON (icon))
{
g_themed_icon_append_name (G_THEMED_ICON (icon), "text-x-generic");
}
symbolic_icon = g_content_type_get_symbolic_icon (mime_type);
if (G_IS_THEMED_ICON (icon))
{
g_themed_icon_append_name (G_THEMED_ICON (symbolic_icon), "text-x-generic-symbolic");
}
file_info_set_content_type (info, mime_type, uncertain_content_type);
}
if (have_display_name == FALSE)
g_file_info_set_display_name (info, basename);
g_file_info_set_icon (info, icon);
g_file_info_set_symbolic_icon (info, symbolic_icon);
g_object_unref (icon);
g_object_unref (symbolic_icon);
g_free (mime_type);
g_free (basename);
}
static void
ms_response_to_fs_info (MsResponse *response,
GFileInfo *info)
{
xmlNodeIter iter;
MsPropstat propstat;
xmlNodePtr node;
guint status;
const char *text;
guint64 bytes_avail;
guint64 bytes_used;
gboolean have_bytes_avail;
gboolean have_bytes_used;
bytes_avail = bytes_used = 0;
have_bytes_avail = have_bytes_used = FALSE;
ms_response_get_propstat_iter (response, &iter);
while (xml_node_iter_next (&iter))
{
status = ms_response_get_propstat (&iter, &propstat);
if (! SOUP_STATUS_IS_SUCCESSFUL (status))
continue;
for (node = propstat.prop_node->children; node; node = node->next)
{
if (! node_is_element (node))
continue;
text = node_get_content (node);
if (text == NULL)
continue;
if (node_has_name (node, "quota-available-bytes"))
{
if (! string_to_uint64 (text, &bytes_avail))
continue;
have_bytes_avail = TRUE;
}
else if (node_has_name (node, "quota-used-bytes"))
{
if (! string_to_uint64 (text, &bytes_used))
continue;
have_bytes_used = TRUE;
}
}
}
if (have_bytes_used)
g_file_info_set_attribute_uint64 (info,
G_FILE_ATTRIBUTE_FILESYSTEM_USED,
bytes_used);
if (have_bytes_avail)
{
g_file_info_set_attribute_uint64 (info,
G_FILE_ATTRIBUTE_FILESYSTEM_FREE,
bytes_avail);
if (have_bytes_used && G_MAXUINT64 - bytes_avail >= bytes_used)
g_file_info_set_attribute_uint64 (info,
G_FILE_ATTRIBUTE_FILESYSTEM_SIZE,
bytes_avail + bytes_used);
}
}
#define PROPSTAT_XML_BEGIN \
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" \
" <D:propfind xmlns:D=\"DAV:\">\n"
#define PROPSTAT_XML_ALLPROP " <D:allprop/>\n"
#define PROPSTAT_XML_PROP_BEGIN " <D:prop>\n"
#define PROPSTAT_XML_PROP_END " </D:prop>\n"
#define PROPSTAT_XML_END \
" </D:propfind>"
typedef struct _PropName {
const char *name;
const char *namespace;
} PropName;
static void
propfind_stat_msg_starting_restarted (SoupMessage *msg, gpointer user_data)
{
soup_message_set_request_body_from_bytes (msg, "application/xml", user_data);
}
static void
propfind_stat_bytes_unref (gpointer data, GClosure *closure)
{
(void)closure;
g_bytes_unref (data);
}
static SoupMessage *
propfind_request_new (GVfsBackend *backend,
const char *filename,
guint depth,
const PropName *properties)
{
SoupMessage *msg;
GUri *uri;
const char *header_depth;
GString *body;
GBytes *bytes;
uri = g_vfs_backend_dav_uri_for_path (backend, filename, depth > 0);
msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri);
g_uri_unref (uri);
if (msg == NULL)
return NULL;
if (depth == 0)
header_depth = "0";
else if (depth == 1)
header_depth = "1";
else
header_depth = "infinity";
soup_message_headers_append (soup_message_get_request_headers (msg),
"Depth", header_depth);
body = g_string_new (PROPSTAT_XML_BEGIN);
if (properties != NULL)
{
const PropName *prop;
g_string_append (body, PROPSTAT_XML_PROP_BEGIN);
for (prop = properties; prop->name; prop++)
{
if (prop->namespace != NULL)
g_string_append_printf (body, "<%s xmlns=\"%s\"/>\n",
prop->name,
prop->namespace);
else
g_string_append_printf (body, "<D:%s/>\n", prop->name);
}
g_string_append (body, PROPSTAT_XML_PROP_END);
}
else
g_string_append (body, PROPSTAT_XML_ALLPROP);
g_string_append (body, PROPSTAT_XML_END);
bytes = g_string_free_to_bytes (body);
g_signal_connect_data (msg, "starting",
G_CALLBACK (propfind_stat_msg_starting_restarted),
g_bytes_ref (bytes), propfind_stat_bytes_unref, 0);
/* only called with implicit redirects enabled */
g_signal_connect_data (msg, "restarted",
G_CALLBACK (propfind_stat_msg_starting_restarted),
g_bytes_ref (bytes), propfind_stat_bytes_unref, 0);
g_bytes_unref (bytes);
return msg;
}
static SoupMessage *
stat_location_start (GUri *uri,
gboolean count_children)
{
SoupMessage *msg;
const char *depth;
GBytes *bytes;
static const char *stat_profind_body =
PROPSTAT_XML_BEGIN
PROPSTAT_XML_PROP_BEGIN
"<D:resourcetype/>\n"
"<D:getcontentlength/>\n"
PROPSTAT_XML_PROP_END
PROPSTAT_XML_END;
msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri);
if (count_children)
depth = "1";
else
depth = "0";
soup_message_headers_append (soup_message_get_request_headers (msg),
"Depth", depth);
bytes = g_bytes_new (stat_profind_body, strlen (stat_profind_body));
g_signal_connect_data (msg, "starting",
G_CALLBACK (propfind_stat_msg_starting_restarted),
g_bytes_ref (bytes), propfind_stat_bytes_unref, 0);
/* only called with implicit redirects enabled */
g_signal_connect_data (msg, "restarted",
G_CALLBACK (propfind_stat_msg_starting_restarted),
g_bytes_ref (bytes), propfind_stat_bytes_unref, 0);
g_bytes_unref (bytes);
return msg;
}
static gboolean
stat_location_end (SoupMessage *msg,
GInputStream *body,
GFileType *target_type,
gint64 *target_size,
guint *num_children)
{
Multistatus ms;
xmlNodeIter iter;
gboolean res;
guint child_count;
GFileInfo *file_info;
if (!body || soup_message_get_status (msg) != SOUP_STATUS_MULTI_STATUS)
return FALSE;
res = multistatus_parse (msg, body, &ms, NULL);
if (res == FALSE)
return FALSE;
res = FALSE;
child_count = 0;
file_info = g_file_info_new ();
multistatus_get_response_iter (&ms, &iter);
while (xml_node_iter_next (&iter))
{
MsResponse response;
if (! multistatus_get_response (&iter, &response))
continue;
if (response.is_target)
{
ms_response_to_file_info (&response, file_info);
res = TRUE;
}
else
child_count++;
ms_response_clear (&response);
}
if (res)
{
if (target_type)
*target_type = g_file_info_get_file_type (file_info);
if (target_size)
*target_size = g_file_info_get_size (file_info);
if (num_children)
*num_children = child_count;
}
multistatus_free (&ms);
g_object_unref (file_info);
return res;
}
typedef struct _StatLocationData {
GUri *uri;
GFileType target_type;
gint64 target_size;
guint num_children;
} StatLocationData;
static void
stat_location_data_free (gpointer p)
{
g_uri_unref (((StatLocationData *)p)->uri);
g_slice_free (StatLocationData, p);
}
static void
stat_location_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GTask *task = user_data;
StatLocationData *data = g_task_get_task_data (task);
GInputStream *body;
GError *error = NULL;
guint status;
gboolean res;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
g_object_unref (msg);
g_task_return_error (task, error);
g_object_unref (task);
return;
}
status = soup_message_get_status (msg);
if (status != SOUP_STATUS_MULTI_STATUS)
{
error = g_error_new (G_IO_ERROR,
http_error_code_from_status (status),
_("HTTP Error: %s"),
soup_message_get_reason_phrase (msg));
g_object_unref (msg);
g_task_return_error (task, error);
g_object_unref (body);
g_object_unref (task);
return;
}
res = stat_location_end (msg, body, &data->target_type,
&data->target_size, &data->num_children);
g_object_unref (msg);
g_object_unref (body);
if (res == FALSE)
{
error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
_("Response invalid"));
g_task_return_error (task, error);
}
else
g_task_return_boolean (task, TRUE);
g_object_unref (task);
}
static void
stat_location_async (GVfsBackend *backend,
GUri *uri,
gboolean count_children,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
SoupMessage *msg;
StatLocationData *data;
task = g_task_new (backend, NULL, callback, user_data);
data = g_slice_new (StatLocationData);
data->uri = g_uri_ref (uri);
g_task_set_source_tag (task, stat_location_async);
g_task_set_task_data (task, data, stat_location_data_free);
msg = stat_location_start (uri, count_children);
if (msg == NULL)
{
g_task_return_boolean (task, FALSE);
g_object_unref (task);
return;
}
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, stat_location_cb, task);
}
static gboolean
stat_location_finish (GVfsBackend *backend,
GFileType *target_type,
gint64 *target_size,
guint *num_children,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (G_VFS_IS_BACKEND (backend), FALSE);
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
g_return_val_if_fail (g_task_is_valid (result, backend), FALSE);
g_return_val_if_fail (g_async_result_is_tagged (result, stat_location_async), FALSE);
if (g_task_propagate_boolean (G_TASK (result), error))
{
StatLocationData *data = g_task_get_task_data (G_TASK (result));
if (target_type) *target_type = data->target_type;
if (target_size) *target_size = data->target_size;
if (num_children) *num_children = data->num_children;
return TRUE;
}
return FALSE;
}
static GUri *
stat_location_async_get_uri (GVfsBackend *backend, GAsyncResult *result)
{
StatLocationData *data;
g_return_val_if_fail (G_VFS_IS_BACKEND (backend), NULL);
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
g_return_val_if_fail (g_task_is_valid (result, backend), NULL);
data = g_task_get_task_data (G_TASK (result));
return data->uri;
}
/* ************************************************************************* */
/* Authentication */
static void
mount_auth_info_free (MountAuthData *data)
{
if (data->mount_source)
g_object_unref (data->mount_source);
g_free (data->server_auth.username);
g_free (data->server_auth.password);
g_free (data->server_auth.realm);
g_free (data->proxy_auth.username);
g_free (data->proxy_auth.password);
}
static gboolean
soup_authenticate (SoupMessage *msg,
SoupAuth *auth,
gboolean retrying,
gpointer user_data)
{
MountAuthData *data;
AuthInfo *info;
GAskPasswordFlags pw_ask_flags;
GPasswordSave pw_save;
const char *realm;
gboolean res;
gboolean aborted;
gboolean is_proxy;
gboolean have_auth;
char *new_username;
char *new_password;
char *prompt;
data = (MountAuthData *) user_data;
is_proxy = soup_auth_is_for_proxy (auth);
if (is_proxy)
info = &(data->proxy_auth);
else
info = &(data->server_auth);
if (!data->interactive)
{
g_debug ("+ soup_authenticate (%s) \n",
retrying ? "retrying" : "first auth");
if (!retrying)
soup_auth_authenticate (auth, info->username, info->password);
return FALSE;
}
if (data->retrying_after_403)
retrying = TRUE;
g_debug ("+ soup_authenticate (interactive, %s) \n",
retrying ? "retrying" : "first auth");
new_username = NULL;
new_password = NULL;
realm = NULL;
pw_ask_flags = G_ASK_PASSWORD_NEED_PASSWORD;
realm = soup_auth_get_realm (auth);
if (realm && info->realm == NULL)
info->realm = g_strdup (realm);
else if (realm && info->realm && !g_str_equal (realm, info->realm))
return FALSE;
have_auth = info->username && info->password;
if (have_auth == FALSE && g_vfs_keyring_is_available ())
{
const char *host;
gchar *hostp = NULL;
gint port;
pw_ask_flags |= G_ASK_PASSWORD_SAVING_SUPPORTED;
if (is_proxy)
{
gboolean ret;
gchar *urip;
urip = g_strdup_printf ("http://%s", soup_auth_get_authority (auth));
ret = g_uri_split_network (urip, G_URI_FLAGS_NONE,
NULL, &hostp, &port, NULL);
g_free (urip);
if (!ret)
return FALSE;
host = hostp;
}
else
{
GUri *uri = soup_message_get_uri (msg);
host = g_uri_get_host (uri);
port = g_uri_get_port (uri);
}
res = g_vfs_keyring_lookup_password (info->username,
host,
NULL,
"http",
realm,
is_proxy ? "proxy" : "basic",
port,
&new_username,
NULL,
&new_password);
if (res == TRUE)
{
have_auth = TRUE;
g_free (info->username);
g_free (info->password);
info->username = new_username;
info->password = new_password;
}
if (hostp)
g_free (hostp);
}
if (retrying == FALSE && have_auth)
{
soup_auth_authenticate (auth, info->username, info->password);
return FALSE;
}
if (is_proxy == FALSE)
{
if (realm == NULL)
realm = _("WebDAV share");
/* Translators: %s is the name of the WebDAV share */
prompt = g_strdup_printf (_("Authentication Required\nEnter password for “%s”:"), realm);
}
else
prompt = g_strdup (_("Authentication Required\nEnter proxy password:"));
if (info->username == NULL)
pw_ask_flags |= G_ASK_PASSWORD_NEED_USERNAME;
res = g_mount_source_ask_password (data->mount_source,
prompt,
info->username,
NULL,
pw_ask_flags,
&aborted,
&new_password,
&new_username,
NULL,
NULL,
&pw_save);
if (res && !aborted)
{
/* it's not safe to assume that we get the username filed in,
in the case that we provied a default username */
if (new_username == NULL)
new_username = g_strdup (info->username);
soup_auth_authenticate (auth, new_username, new_password);
g_free (info->username);
g_free (info->password);
info->username = new_username;
info->password = new_password;
info->pw_save = pw_save;
}
else
soup_auth_cancel (auth);
g_debug ("- soup_authenticate \n");
g_free (prompt);
return !res || aborted;
}
static void
keyring_save_authinfo (AuthInfo *info,
GUri *uri,
gboolean is_proxy)
{
const char *type = is_proxy ? "proxy" : "basic";
g_vfs_keyring_save_password (info->username,
g_uri_get_host (uri),
NULL,
"http",
info->realm,
type,
g_uri_get_port (uri),
info->password,
info->pw_save);
}
/* ************************************************************************* */
static GUri *
g_mount_spec_to_dav_uri (GMountSpec *spec)
{
GUri *uri;
const char *host;
const char *port;
const char *ssl;
const char *path;
const char *scheme;
char *host_str;
char *path_str;
gint port_num;
host = g_mount_spec_get (spec, "host");
port = g_mount_spec_get (spec, "port");
ssl = g_mount_spec_get (spec, "ssl");
path = spec->mount_prefix;
if (host == NULL || *host == 0)
return NULL;
if (ssl != NULL && (strcmp (ssl, "true") == 0))
scheme = "https";
else
scheme = "http";
port_num = -1;
/* always a valid number if set */
if (port != NULL)
port_num = atoi (port);
/* IPv6 host does not include brackets in SoupURI, but GMountSpec host does */
if (gvfs_is_ipv6 (host))
host_str = g_strndup (host + 1, strlen (host) - 2);
else
host_str = g_strdup (host);
path_str = dav_uri_encode (path);
/* A username is not part of URI as a workaround for:
* https://gitlab.gnome.org/GNOME/gvfs/-/issues/617.
*/
uri = g_uri_build (SOUP_HTTP_URI_FLAGS,
scheme,
NULL,
host_str,
port_num,
path_str,
NULL,
NULL);
g_free (host_str);
g_free (path_str);
return uri;
}
static GMountSpec *
g_mount_spec_from_dav_uri (GVfsBackendDav *dav_backend,
GUri *uri)
{
GMountSpec *spec;
char *local_path;
gboolean ssl;
gint port_num;
#ifdef HAVE_AVAHI
if (dav_backend->resolver != NULL)
{
const char *type;
const char *service_type;
service_type = g_vfs_dns_sd_resolver_get_service_type (dav_backend->resolver);
if (strcmp (service_type, "_webdavs._tcp") == 0)
type = "davs+sd";
else
type = "dav+sd";
spec = g_mount_spec_new (type);
g_mount_spec_set (spec,
"host",
g_vfs_dns_sd_resolver_get_encoded_triple (dav_backend->resolver));
return spec;
}
#endif
spec = g_mount_spec_new ("dav");
/* IPv6 host does not include brackets in GUri, but GMountSpec host does */
if (strchr (g_uri_get_host (uri), ':'))
{
char *host = g_strdup_printf ("[%s]", g_uri_get_host (uri));
g_mount_spec_set (spec, "host", host);
g_free (host);
}
else
g_mount_spec_set (spec, "host", g_uri_get_host (uri));
ssl = !strcmp (g_uri_get_scheme (uri), "https");
g_mount_spec_set (spec, "ssl", ssl ? "true" : "false");
port_num = g_uri_get_port (uri);
if (port_num > 0 && port_num != (ssl ? 443 : 80))
{
char *port = g_strdup_printf ("%d", port_num);
g_mount_spec_set (spec, "port", port);
g_free (port);
}
/* There must not be any illegal characters in the
URL at this point */
local_path = g_uri_unescape_string (g_uri_get_path (uri), "/");
g_mount_spec_set_mount_prefix (spec, local_path);
g_free (local_path);
return spec;
}
#ifdef HAVE_AVAHI
static GUri *
dav_uri_from_dns_sd_resolver (GVfsBackendDav *dav_backend)
{
GUri *uri;
char *user;
char *path;
char *address;
char *host;
gchar *interface;
const char *service_type;
const char *scheme;
guint port;
service_type = g_vfs_dns_sd_resolver_get_service_type (dav_backend->resolver);
address = g_vfs_dns_sd_resolver_get_address (dav_backend->resolver);
interface = g_vfs_dns_sd_resolver_get_interface (dav_backend->resolver);
port = g_vfs_dns_sd_resolver_get_port (dav_backend->resolver);
user = g_vfs_dns_sd_resolver_lookup_txt_record (dav_backend->resolver, "u"); /* mandatory */
path = g_vfs_dns_sd_resolver_lookup_txt_record (dav_backend->resolver, "path"); /* optional */
/* TODO: According to http://www.dns-sd.org/ServiceTypes.html
* there's also a TXT record "p" for password. Handle this.
*/
if (strcmp (service_type, "_webdavs._tcp") == 0)
scheme = "https";
else
scheme = "http";
/* IPv6 host does not include brackets in GUri, but GVfsDnsSdResolver host does */
if (gvfs_is_ipv6 (address))
{
/* Link-local addresses require interface to be specified. */
if (g_str_has_prefix (address, "[fe80:") && interface != NULL)
{
host = g_strconcat (address + 1, interface, NULL);
host[strlen (address) - 2] = '%';
}
else
host = g_strndup (address + 1, strlen (address) - 2);
}
else
host = g_strdup (address);
uri = g_uri_build_with_user (SOUP_HTTP_URI_FLAGS,
scheme,
user,
NULL,
NULL,
host,
port,
path,
NULL,
NULL);
g_free (address);
g_free (interface);
g_free (user);
g_free (path);
g_free (host);
return uri;
}
#endif
#ifdef HAVE_AVAHI
static void
dns_sd_resolver_changed (GVfsDnsSdResolver *resolver,
GVfsBackendDav *dav_backend)
{
/* If anything has changed (e.g. address, port, txt-records or is-resolved),
* it is safest to just unmount. */
g_vfs_backend_force_unmount (G_VFS_BACKEND (dav_backend));
}
#endif
/* ************************************************************************* */
/* Backend Functions */
static void
mount_success (GVfsBackend *backend, GVfsJobMount *job)
{
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend);
GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend);
GMountSpec *mount_spec;
GUri *tmp;
const gchar *user;
/* Save the auth info in the keyring */
keyring_save_authinfo (&(dav_backend->auth_info.server_auth), http_backend->mount_base, FALSE);
/* TODO: save proxy auth */
/* Set the working path in mount path */
tmp = http_backend->mount_base;
http_backend->mount_base = soup_uri_copy (tmp, SOUP_URI_PATH,
dav_backend->last_good_path,
SOUP_URI_NONE);
g_uri_unref (tmp);
g_clear_pointer (&dav_backend->last_good_path, g_free);
/* dup the mountspec, but only copy known fields */
mount_spec = g_mount_spec_from_dav_uri (dav_backend, http_backend->mount_base);
/* A username is not part of URI as a workaround for:
* https://gitlab.gnome.org/GNOME/gvfs/-/issues/617
* So it has to be restored here in order to avoid:
* https://gitlab.gnome.org/GNOME/gvfs/-/issues/614
*/
user = g_mount_spec_get (job->mount_spec, "user");
if (user != NULL)
g_mount_spec_set (mount_spec, "user", user);
g_vfs_backend_set_mount_spec (backend, mount_spec);
g_vfs_backend_set_icon_name (backend, "folder-remote");
g_vfs_backend_set_symbolic_icon_name (backend, "folder-remote-symbolic");
g_vfs_backend_dav_setup_display_name (backend);
/* cleanup */
g_mount_spec_unref (mount_spec);
g_vfs_job_succeeded (G_VFS_JOB (job));
g_debug ("- mount\n");
}
static void try_mount_opts_cb (GObject *source, GAsyncResult *result, gpointer user_data);
static void
try_mount_stat_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend);
GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend);
GVfsJobMount *job = user_data;
SoupMessage *msg_stat = g_vfs_backend_dav_get_async_result_message (backend, result);
SoupMessage *msg_opts;
GInputStream *body;
GError *error = NULL;
GFileType file_type;
gboolean res;
gboolean is_collection;
GUri *tmp;
char *new_path;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (body == NULL)
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
goto clear_msg;
}
res = stat_location_end (msg_stat, body, &file_type, NULL, NULL);
is_collection = res && file_type == G_FILE_TYPE_DIRECTORY;
if (!g_vfs_backend_dav_stream_skip (body, &error))
{
g_object_unref (body);
if (dav_backend->last_good_path == NULL)
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
else
mount_success (backend, job);
g_error_free (error);
goto clear_msg;
}
g_object_unref (body);
soup_message_headers_clear (soup_message_get_response_headers (msg_stat));
g_debug (" [%s] webdav: %d, collection %d [res: %d]\n",
g_uri_get_path (http_backend->mount_base), TRUE, is_collection, res);
if ((is_collection == FALSE) && (dav_backend->last_good_path != NULL))
{
mount_success (backend, job);
goto clear_msg;
}
else if (res == FALSE)
{
int error_code = http_error_code_from_status (soup_message_get_status (msg_stat));
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, error_code,
_("HTTP Error: %s"),
soup_message_get_reason_phrase (msg_stat));
goto clear_msg;
}
else if (is_collection == FALSE)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not find an enclosing directory"));
goto clear_msg;
}
/* we have found a new good root, try the parent ... */
g_free (dav_backend->last_good_path);
dav_backend->last_good_path = g_strdup (g_uri_get_path (http_backend->mount_base));
new_path = path_get_parent_dir (dav_backend->last_good_path);
tmp = http_backend->mount_base;
http_backend->mount_base = soup_uri_copy (tmp, SOUP_URI_PATH, new_path, SOUP_URI_NONE);
g_uri_unref (tmp);
g_free (new_path);
/* if we have found a root that is good then we assume
that we also have obtained to correct credentials
and we switch the auth handler. This will prevent us
from asking for *different* credentials *again* if the
server should response with 401 for some of the parent
collections. See also bug #677753 */
dav_backend->auth_info.interactive = FALSE;
/* break out */
if (g_strcmp0 (dav_backend->last_good_path, "/") == 0)
{
mount_success (backend, job);
goto clear_msg;
}
/* else loop the whole thing from options */
msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS,
http_backend->mount_base);
dav_message_connect_signals (msg_opts, backend);
g_vfs_backend_dav_send_async (backend, msg_opts, try_mount_opts_cb, job);
clear_msg:
g_object_unref (msg_stat);
}
static void
try_mount_opts_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend);
GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend);
GVfsJobMount *job = user_data;
SoupMessage *msg_opts = g_vfs_backend_dav_get_async_result_message (backend, result);
SoupMessage *msg_stat;
GInputStream *body;
GError *error = NULL;
GUri *cur_uri;
guint status;
gboolean is_success, is_webdav;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
/* If SSL is used and the certificate verifies OK, then ssl-strict remains
* on for all further connections.
* If SSL is used and the certificate does not verify OK, then the user
* gets a chance to override it. If they do, ssl-strict is disabled but
* the certificate is stored, and checked on each subsequent connection to
* ensure that it hasn't changed. */
if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE) &&
!dav_backend->certificate_errors)
{
GTlsCertificate *certificate;
GTlsCertificateFlags errors;
certificate = soup_message_get_tls_peer_certificate (msg_opts);
errors = soup_message_get_tls_peer_certificate_errors (msg_opts);
if (gvfs_accept_certificate (dav_backend->auth_info.mount_source, certificate, errors))
{
g_clear_error (&error);
dav_backend->certificate = g_object_ref (certificate);
dav_backend->certificate_errors = errors;
/* re-send the opts message; re-create since we're still in its cb */
g_object_unref (msg_opts);
msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS,
http_backend->mount_base);
dav_message_connect_signals (msg_opts, backend);
g_vfs_backend_dav_send_async (backend, msg_opts,
try_mount_opts_cb, job);
return;
}
}
/* message failed, propagate the error */
if (!body)
{
if (dav_backend->last_good_path == NULL)
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
else
mount_success (backend, job);
g_error_free (error);
goto clear_msgs;
}
status = soup_message_get_status (msg_opts);
/* Workaround for servers which response with 403 instead of 401 in case of
* wrong credentials to let the user specify its credentials again. */
if (status == SOUP_STATUS_FORBIDDEN &&
dav_backend->last_good_path == NULL &&
(dav_backend->auth_info.server_auth.password != NULL ||
dav_backend->auth_info.proxy_auth.password != NULL))
{
SoupSessionFeature *auth_manager;
dav_backend->auth_info.retrying_after_403 = TRUE;
g_clear_pointer (&dav_backend->auth_info.server_auth.username, g_free);
dav_backend->auth_info.server_auth.username = g_strdup (g_uri_get_user (http_backend->mount_base));
g_clear_pointer (&dav_backend->auth_info.server_auth.password, g_free);
g_clear_pointer (&dav_backend->auth_info.proxy_auth.password, g_free);
auth_manager = soup_session_get_feature (http_backend->session, SOUP_TYPE_AUTH_MANAGER);
soup_auth_manager_clear_cached_credentials (SOUP_AUTH_MANAGER (auth_manager));
/* re-send the opts message; re-create since we're still in its cb */
g_object_unref (msg_opts);
msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS,
http_backend->mount_base);
dav_message_connect_signals (msg_opts, backend);
g_vfs_backend_dav_send_async (backend, msg_opts, try_mount_opts_cb, job);
return;
}
is_success = SOUP_STATUS_IS_SUCCESSFUL (status);
is_webdav = sm_has_header (msg_opts, "DAV");
if ((is_success && !is_webdav) || (status == SOUP_STATUS_METHOD_NOT_ALLOWED))
{
if (dav_backend->last_good_path == NULL)
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Not a WebDAV enabled share"));
else
mount_success (backend, job);
goto clear_msgs;
}
else if (!is_success)
{
int error_code = http_error_code_from_status (soup_message_get_status (msg_opts));
if (dav_backend->last_good_path == NULL)
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, error_code,
_("HTTP Error: %s"),
soup_message_get_reason_phrase (msg_opts));
else
mount_success (backend, job);
goto clear_msgs;
}
if (!g_vfs_backend_dav_stream_skip (body, &error))
{
if (dav_backend->last_good_path == NULL)
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
else
mount_success (backend, job);
g_error_free (error);
goto clear_msgs;
}
g_object_unref (body);
cur_uri = soup_message_get_uri (msg_opts);
/* The count_children parameter is intentionally set to TRUE to be sure that
enumeration is possible: https://gitlab.gnome.org/GNOME/gvfs/-/issues/468 */
msg_stat = stat_location_start (cur_uri, TRUE);
dav_message_connect_signals (msg_stat, backend);
g_vfs_backend_dav_send_async (backend, msg_stat, try_mount_stat_cb, job);
clear_msgs:
g_object_unref (msg_opts);
}
static void
try_mount_send_opts (GVfsJobMount *job)
{
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (job->backend);
GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (job->backend);
SoupMessage *msg_opts;
const gchar *user;
if (http_backend->mount_base == NULL)
{
g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR,
G_IO_ERROR_INVALID_ARGUMENT,
_("Invalid mount spec"));
return;
}
soup_session_add_feature_by_type (http_backend->session,
SOUP_TYPE_AUTH_NEGOTIATE);
soup_session_add_feature_by_type (http_backend->session,
SOUP_TYPE_AUTH_NTLM);
user = g_mount_spec_get (job->mount_spec, "user");
dav_backend->auth_info.mount_source = g_object_ref (job->mount_source);
dav_backend->auth_info.server_auth.username = g_strdup (user);
dav_backend->auth_info.server_auth.password = NULL;
dav_backend->auth_info.server_auth.pw_save = G_PASSWORD_SAVE_NEVER;
dav_backend->auth_info.proxy_auth.pw_save = G_PASSWORD_SAVE_NEVER;
dav_backend->auth_info.interactive = TRUE;
dav_backend->last_good_path = NULL;
msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, http_backend->mount_base);
dav_message_connect_signals (msg_opts, job->backend);
g_vfs_backend_dav_send_async (job->backend, msg_opts, try_mount_opts_cb, job);
}
#ifdef HAVE_AVAHI
static void
try_mount_resolve_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsJobMount *job = user_data;
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (job->backend);
GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (job->backend);
GError *error = NULL;
if (!g_vfs_dns_sd_resolver_resolve_finish (dav_backend->resolver,
result,
&error))
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
return;
}
g_signal_connect (dav_backend->resolver,
"changed",
(GCallback) dns_sd_resolver_changed,
dav_backend);
http_backend->mount_base = dav_uri_from_dns_sd_resolver (dav_backend);
try_mount_send_opts (job);
}
#endif
static gboolean
try_mount (GVfsBackend *backend,
GVfsJobMount *job,
GMountSpec *mount_spec,
GMountSource *mount_source,
gboolean is_automount)
{
GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend);
g_debug ("+ mount\n");
#ifdef HAVE_AVAHI
GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend);
const char *host;
const char *type;
host = g_mount_spec_get (mount_spec, "host");
type = g_mount_spec_get (mount_spec, "type");
/* resolve DNS-SD style URIs */
if ((strcmp (type, "dav+sd") == 0 || strcmp (type, "davs+sd") == 0) && host != NULL)
{
dav_backend->resolver = g_vfs_dns_sd_resolver_new_for_encoded_triple (host, "u");
g_vfs_dns_sd_resolver_resolve (dav_backend->resolver,
G_VFS_JOB (job)->cancellable,
try_mount_resolve_cb,
job);
}
else
#endif
{
http_backend->mount_base = g_mount_spec_to_dav_uri (mount_spec);
try_mount_send_opts (job);
}
return TRUE;
}
static PropName ls_propnames[] = {
{"creationdate", NULL},
{"displayname", NULL},
{"getcontentlength", NULL},
{"getcontenttype", NULL},
{"getetag", NULL},
{"getlastmodified", NULL},
{"resourcetype", NULL},
{NULL, NULL}
};
/* *** query_info () *** */
static void
try_query_info_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GVfsJobQueryInfo *job = user_data;
GInputStream *body;
GError *error = NULL;
Multistatus ms;
xmlNodeIter iter;
gboolean res;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
goto error;
res = multistatus_parse (msg, body, &ms, &error);
g_object_unref (body);
if (res == FALSE)
goto error;
res = FALSE;
multistatus_get_response_iter (&ms, &iter);
while (xml_node_iter_next (&iter))
{
MsResponse response;
if (! multistatus_get_response (&iter, &response))
continue;
if (response.is_target)
{
ms_response_to_file_info (&response, job->file_info);
res = TRUE;
}
ms_response_clear (&response);
}
multistatus_free (&ms);
g_object_unref (msg);
if (res)
g_vfs_job_succeeded (G_VFS_JOB (job));
else
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Response invalid"));
return;
error:
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
g_object_unref (msg);
}
static gboolean
try_query_info (GVfsBackend *backend,
GVfsJobQueryInfo *job,
const char *filename,
GFileQueryInfoFlags flags,
GFileInfo *info,
GFileAttributeMatcher *matcher)
{
SoupMessage *msg;
g_debug ("Query info %s\n", filename);
msg = propfind_request_new (backend, filename, 0, ls_propnames);
if (msg == NULL)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not create request"));
return TRUE;
}
message_add_redirect_header (msg, flags);
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, try_query_info_cb, job);
return TRUE;
}
static PropName fs_info_propnames[] = {
{"quota-available-bytes", NULL},
{"quota-used-bytes", NULL},
{NULL, NULL}
};
static void
try_query_fs_info_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GVfsJobQueryFsInfo *job = user_data;
GInputStream *body;
GError *error = NULL;
Multistatus ms;
xmlNodeIter iter;
gboolean res;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
goto error;
res = multistatus_parse (msg, body, &ms, &error);
g_object_unref (body);
if (res == FALSE)
goto error;
res = FALSE;
multistatus_get_response_iter (&ms, &iter);
while (xml_node_iter_next (&iter))
{
MsResponse response;
if (! multistatus_get_response (&iter, &response))
continue;
if (response.is_target)
{
ms_response_to_fs_info (&response, job->file_info);
res = TRUE;
}
ms_response_clear (&response);
}
multistatus_free (&ms);
g_object_unref (msg);
if (res)
g_vfs_job_succeeded (G_VFS_JOB (job));
else
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Response invalid"));
return;
error:
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
g_object_unref (msg);
}
static gboolean
try_query_fs_info (GVfsBackend *backend,
GVfsJobQueryFsInfo *job,
const char *filename,
GFileInfo *info,
GFileAttributeMatcher *attribute_matcher)
{
SoupMessage *msg;
g_file_info_set_attribute_string (info,
G_FILE_ATTRIBUTE_FILESYSTEM_TYPE,
"webdav");
g_file_info_set_attribute_boolean (info,
G_FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
TRUE);
g_file_info_set_attribute_uint32 (info,
G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW,
G_FILESYSTEM_PREVIEW_TYPE_IF_ALWAYS);
if (! (g_file_attribute_matcher_matches (attribute_matcher,
G_FILE_ATTRIBUTE_FILESYSTEM_SIZE) ||
g_file_attribute_matcher_matches (attribute_matcher,
G_FILE_ATTRIBUTE_FILESYSTEM_USED) ||
g_file_attribute_matcher_matches (attribute_matcher,
G_FILE_ATTRIBUTE_FILESYSTEM_FREE)))
{
g_vfs_job_succeeded (G_VFS_JOB (job));
return TRUE;
}
msg = propfind_request_new (backend, filename, 0, fs_info_propnames);
if (msg == NULL)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not create request"));
return TRUE;
}
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, try_query_fs_info_cb, job);
return TRUE;
}
/* *** enumerate *** */
static void
try_enumerate_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GVfsJobEnumerate *job = user_data;
GInputStream *body;
GError *error = NULL;
Multistatus ms;
xmlNodeIter iter;
gboolean res;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
goto error;
res = multistatus_parse (msg, body, &ms, &error);
g_object_unref (body);
if (res == FALSE)
goto error;
g_vfs_job_succeeded (G_VFS_JOB (job));
multistatus_get_response_iter (&ms, &iter);
while (xml_node_iter_next (&iter))
{
MsResponse response;
GFileInfo *info;
if (! multistatus_get_response (&iter, &response))
continue;
if (response.is_target == FALSE)
{
info = g_file_info_new ();
ms_response_to_file_info (&response, info);
g_vfs_job_enumerate_add_info (job, info);
g_object_unref (info);
}
ms_response_clear (&response);
}
multistatus_free (&ms);
g_object_unref (msg);
g_vfs_job_enumerate_done (G_VFS_JOB_ENUMERATE (job));
return;
error:
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
g_object_unref (msg);
}
static gboolean
try_enumerate (GVfsBackend *backend,
GVfsJobEnumerate *job,
const char *filename,
GFileAttributeMatcher *matcher,
GFileQueryInfoFlags flags)
{
SoupMessage *msg;
g_debug ("+ try_enumerate: %s\n", filename);
msg = propfind_request_new (backend, filename, 1, ls_propnames);
if (msg == NULL)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not create request"));
return TRUE;
}
message_add_redirect_header (msg, flags);
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, try_enumerate_cb, job);
return TRUE;
}
/* ************************************************************************* */
/* */
/* *** open () *** */
static void
try_open_stat_done (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
GInputStream *body;
SoupSession *session = SOUP_SESSION (source);
SoupMessage *msg;
GVfsJob *job = G_VFS_JOB (user_data);
GVfsBackend *backend = job->backend_data;
GError *error = NULL;
GUri *uri;
GFileType target_type;
gboolean res;
msg = soup_session_get_async_result_message (session, result);
if (soup_message_get_status (msg) != SOUP_STATUS_MULTI_STATUS)
{
http_job_failed (job, msg);
return;
}
body = soup_session_send_finish (session, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (job, error);
g_error_free (error);
return;
}
res = stat_location_end (msg, body, &target_type, NULL, NULL);
g_object_unref (body);
if (res == FALSE)
{
g_vfs_job_failed (job,
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Response invalid"));
return;
}
if (target_type == G_FILE_TYPE_DIRECTORY)
{
g_vfs_job_failed (job,
G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY,
_("File is directory"));
return;
}
uri = soup_message_get_uri (msg);
http_backend_open_for_read (backend, job, uri);
}
static gboolean
try_open_for_read (GVfsBackend *backend,
GVfsJobOpenForRead *job,
const char *filename)
{
SoupMessage *msg;
GUri *uri;
uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE);
msg = stat_location_start (uri, FALSE);
g_uri_unref (uri);
if (msg == NULL)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not create request"));
return FALSE;
}
dav_message_connect_signals (msg, backend);
g_vfs_job_set_backend_data (G_VFS_JOB (job), backend, NULL);
soup_session_send_async (G_VFS_BACKEND_HTTP (backend)->session, msg,
G_PRIORITY_DEFAULT, NULL, try_open_stat_done, job);
return TRUE;
}
/* *** create () *** */
static void
try_create_tested_existence (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
GVfsJob *job = G_VFS_JOB (user_data);
GOutputStream *stream;
GInputStream *body;
SoupMessage *msg = job->backend_data;
SoupMessage *put_msg;
GError *error = NULL;
GUri *uri;
body = soup_session_send_finish (SOUP_SESSION (source), result, &error);
if (!body)
{
g_vfs_job_failed_from_error (job, error);
g_error_free (error);
return;
}
if (SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg)))
{
g_object_unref (body);
g_vfs_job_failed (job,
G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
return;
}
/* TODO: other errors */
g_object_unref (body);
uri = soup_message_get_uri (msg);
put_msg = soup_message_new_from_uri (SOUP_METHOD_PUT, uri);
/*
* Doesn't work with apache > 2.2.9
* soup_message_headers_append (soup_message_get_request_headers (put_msg), "If-None-Match", "*");
*/
stream = g_memory_output_stream_new (NULL, 0, g_try_realloc, g_free);
g_object_set_data_full (G_OBJECT (stream), "-gvfs-stream-msg", put_msg, g_object_unref);
g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream);
g_vfs_job_open_for_write_set_can_seek (G_VFS_JOB_OPEN_FOR_WRITE (job),
g_seekable_can_seek (G_SEEKABLE (stream)));
g_vfs_job_open_for_write_set_can_truncate (G_VFS_JOB_OPEN_FOR_WRITE (job),
g_seekable_can_truncate (G_SEEKABLE (stream)));
g_vfs_job_succeeded (job);
}
static gboolean
try_create (GVfsBackend *backend,
GVfsJobOpenForWrite *job,
const char *filename,
GFileCreateFlags flags)
{
SoupMessage *msg;
GUri *uri;
/* TODO: if we supported chunked requests, we could
* use a PUT with "If-None-Match: *" and "Expect: 100-continue"
*/
uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE);
msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri);
g_uri_unref (uri);
g_vfs_job_set_backend_data (G_VFS_JOB (job), msg, NULL);
dav_message_connect_signals (msg, backend);
soup_session_send_async (G_VFS_BACKEND_HTTP (backend)->session, msg,
G_PRIORITY_DEFAULT, NULL,
try_create_tested_existence, job);
return TRUE;
}
/* *** replace () *** */
static void
open_for_replace_succeeded (GVfsBackendHttp *op_backend,
GVfsJob *job,
GUri *uri,
const char *etag)
{
SoupMessage *put_msg;
GOutputStream *stream;
put_msg = soup_message_new_from_uri (SOUP_METHOD_PUT, uri);
if (etag)
soup_message_headers_append (soup_message_get_request_headers (put_msg),
"If-Match",
etag);
stream = g_memory_output_stream_new (NULL, 0, g_try_realloc, g_free);
g_object_set_data_full (G_OBJECT (stream), "-gvfs-stream-msg", put_msg, g_object_unref);
g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream);
g_vfs_job_open_for_write_set_can_seek (G_VFS_JOB_OPEN_FOR_WRITE (job),
g_seekable_can_seek (G_SEEKABLE (stream)));
g_vfs_job_open_for_write_set_can_truncate (G_VFS_JOB_OPEN_FOR_WRITE (job),
g_seekable_can_truncate (G_SEEKABLE (stream)));
g_vfs_job_succeeded (job);
}
static void
try_replace_checked_etag (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
GVfsJob *job = G_VFS_JOB (user_data);
GVfsBackendHttp *op_backend = job->backend_data;
GInputStream *body;
SoupSession *session = SOUP_SESSION (source);
SoupMessage *msg;
GError *error;
msg = soup_session_get_async_result_message (session, result);
body = soup_session_send_finish (session, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (job, error);
g_error_free (error);
return;
}
if (soup_message_get_status (msg) == SOUP_STATUS_PRECONDITION_FAILED)
{
g_object_unref (body);
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_WRONG_ETAG,
_("The file was externally modified"));
return;
}
/* TODO: other errors */
g_object_unref (body);
open_for_replace_succeeded (op_backend, job, soup_message_get_uri (msg),
soup_message_headers_get_one (soup_message_get_request_headers (msg),
"If-Match"));
}
static gboolean
try_replace (GVfsBackend *backend,
GVfsJobOpenForWrite *job,
const char *filename,
const char *etag,
gboolean make_backup,
GFileCreateFlags flags)
{
GVfsBackendHttp *op_backend;
GUri *uri;
/* TODO: if SoupOutputStream supported chunked requests, we could
* use a PUT with "If-Match: ..." and "Expect: 100-continue"
*/
op_backend = G_VFS_BACKEND_HTTP (backend);
if (make_backup)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_CANT_CREATE_BACKUP,
_("Backup file creation failed"));
return TRUE;
}
uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE);
if (etag)
{
SoupMessage *msg;
msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri);
g_uri_unref (uri);
dav_message_connect_signals (msg, backend);
soup_message_headers_append (soup_message_get_request_headers (msg),
"If-Match", etag);
g_vfs_job_set_backend_data (G_VFS_JOB (job), op_backend, NULL);
soup_session_send_async (op_backend->session, msg, G_PRIORITY_DEFAULT,
NULL, try_replace_checked_etag, job);
return TRUE;
}
open_for_replace_succeeded (op_backend, G_VFS_JOB (job), uri, NULL);
g_uri_unref (uri);
return TRUE;
}
/* *** write () *** */
static void
write_ready (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GOutputStream *stream;
GVfsJob *job;
GError *error;
gssize nwrote;
stream = G_OUTPUT_STREAM (source_object);
error = NULL;
job = G_VFS_JOB (user_data);
nwrote = g_output_stream_write_finish (stream, result, &error);
if (nwrote < 0)
{
g_vfs_job_failed_literal (G_VFS_JOB (job),
error->domain,
error->code,
error->message);
g_error_free (error);
return;
}
g_vfs_job_write_set_written_size (G_VFS_JOB_WRITE (job), nwrote);
g_vfs_job_succeeded (job);
}
static gboolean
try_write (GVfsBackend *backend,
GVfsJobWrite *job,
GVfsBackendHandle handle,
char *buffer,
gsize buffer_size)
{
GOutputStream *stream;
stream = G_OUTPUT_STREAM (handle);
g_output_stream_write_async (stream,
buffer,
buffer_size,
G_PRIORITY_DEFAULT,
G_VFS_JOB (job)->cancellable,
write_ready,
job);
return TRUE;
}
/* this does not invoke libsoup API, so it can be synchronous/threaded */
static void
do_seek_on_write (GVfsBackend *backend,
GVfsJobSeekWrite *job,
GVfsBackendHandle handle,
goffset offset,
GSeekType type)
{
GSeekable *stream = G_SEEKABLE (handle);
GError *error = NULL;
if (g_seekable_seek (stream, offset, type, G_VFS_JOB (job)->cancellable, &error))
{
g_vfs_job_seek_write_set_offset (job, g_seekable_tell (stream));
g_vfs_job_succeeded (G_VFS_JOB (job));
}
else
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
}
}
/* this does not invoke libsoup API, so it can be synchronous/threaded */
static void
do_truncate (GVfsBackend *backend,
GVfsJobTruncate *job,
GVfsBackendHandle handle,
goffset size)
{
GSeekable *stream = G_SEEKABLE (handle);
GError *error = NULL;
if (g_seekable_truncate (stream, size, G_VFS_JOB (job)->cancellable, &error))
{
g_vfs_job_succeeded (G_VFS_JOB (job));
}
else
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
}
}
/* *** close_write () *** */
static void
try_close_write_sent (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
GInputStream *body;
GVfsJob *job = G_VFS_JOB (user_data);
SoupSession *session = SOUP_SESSION (source);
SoupMessage *msg = soup_session_get_async_result_message (session, result);
GError *error;
body = soup_session_send_finish (session, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (job, error);
g_error_free (error);
return;
}
g_object_unref (body);
if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg)))
http_job_failed (job, msg);
else
g_vfs_job_succeeded (job);
}
static gboolean
try_close_write (GVfsBackend *backend,
GVfsJobCloseWrite *job,
GVfsBackendHandle handle)
{
GOutputStream *stream;
SoupMessage *msg;
GBytes *bytes;
stream = G_OUTPUT_STREAM (handle);
msg = g_object_get_data (G_OBJECT (stream), "-gvfs-stream-msg");
g_object_ref (msg);
g_object_set_data (G_OBJECT (stream), "-gvfs-stream-msg", NULL);
dav_message_connect_signals (msg, backend);
g_output_stream_close (stream, NULL, NULL);
bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (stream));
g_object_unref (stream);
soup_message_set_request_body_from_bytes (msg, NULL, bytes);
soup_session_send_async (G_VFS_BACKEND_HTTP (backend)->session, msg,
G_PRIORITY_DEFAULT, NULL, try_close_write_sent, job);
g_bytes_unref (bytes);
return TRUE;
}
static void
make_directory_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
GVfsJobMakeDirectory *job = user_data;
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GInputStream *body;
GError *error = NULL;
guint status;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
g_object_unref (msg);
return;
}
status = soup_message_get_status (msg);
if (! SOUP_STATUS_IS_SUCCESSFUL (status))
if (status == SOUP_STATUS_METHOD_NOT_ALLOWED)
g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
else
http_job_failed (G_VFS_JOB (job), msg);
else
g_vfs_job_succeeded (G_VFS_JOB (job));
g_object_unref (body);
g_object_unref (msg);
}
static gboolean
try_make_directory (GVfsBackend *backend,
GVfsJobMakeDirectory *job,
const char *filename)
{
SoupMessage *msg;
GUri *uri;
uri = g_vfs_backend_dav_uri_for_path (backend, filename, TRUE);
msg = soup_message_new_from_uri (SOUP_METHOD_MKCOL, uri);
g_uri_unref (uri);
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, make_directory_cb, job);
return TRUE;
}
static void
try_delete_send_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
GVfsJobDelete *job = user_data;
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GInputStream *body;
GError *error = NULL;
guint status;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_error_free (error);
g_object_unref (msg);
return;
}
status = soup_message_get_status (msg);
if (!SOUP_STATUS_IS_SUCCESSFUL (status))
http_job_failed (G_VFS_JOB (job), msg);
else
g_vfs_job_succeeded (G_VFS_JOB (job));
g_object_unref (msg);
g_object_unref (body);
}
static void
try_delete_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
GVfsJobDelete *job = user_data;
SoupMessage *msg = NULL;
GFileType file_type;
guint num_children;
GError *error = NULL;
gboolean res;
GUri *uri = stat_location_async_get_uri (backend, result);
res = stat_location_finish (backend, &file_type, NULL,
&num_children, result, &error);
if (res == FALSE)
{
g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
g_clear_error (&error);
return;
}
if (file_type == G_FILE_TYPE_DIRECTORY && num_children)
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_NOT_EMPTY,
_("Directory not empty"));
return;
}
msg = soup_message_new_from_uri (SOUP_METHOD_DELETE, uri);
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, try_delete_send_cb, job);
}
static gboolean
try_delete (GVfsBackend *backend, GVfsJobDelete *job, const char *filename)
{
GUri *uri;
uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE);
stat_location_async (backend, uri, TRUE, try_delete_cb, job);
return TRUE;
}
typedef struct _TrySetDisplayNameData {
GVfsJobSetDisplayName *job;
char *target_path;
} TrySetDisplayNameData;
static void
try_set_display_name_cb (GObject *source, GAsyncResult *result,
gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
TrySetDisplayNameData *data = user_data;
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
GInputStream *body;
GError *error = NULL;
guint status;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
http_job_failed (G_VFS_JOB (data->job), msg);
goto error;
}
status = soup_message_get_status (msg);
/*
* The precondition of SOUP_STATUS_PRECONDITION_FAILED (412) in
* this case was triggered by the "Overwrite: F" header which
* means that the target already exists.
* Also if we get a REDIRECTION it means that there was no
* "Location" header, since otherwise that would have triggered
* our redirection handler. This probably means we are dealing
* with an web dav implementation (like mod_dav) that also sends
* redirects for the destionaion (i.e. "Destination: /foo" header)
* which very likely means that the target also exists (and is a
* directory). That or the webdav server is broken.
* We could find out by doing another stat and but I think this is
* such a corner case that we are totally fine with returning
* G_IO_ERROR_EXISTS.
* */
if (SOUP_STATUS_IS_SUCCESSFUL (status))
{
g_debug ("new target_path: %s\n", data->target_path);
g_vfs_job_set_display_name_set_new_path (data->job, data->target_path);
g_vfs_job_succeeded (G_VFS_JOB (data->job));
}
else if (status == SOUP_STATUS_PRECONDITION_FAILED ||
SOUP_STATUS_IS_REDIRECTION (status))
g_vfs_job_failed (G_VFS_JOB (data->job), G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
else
http_job_failed (G_VFS_JOB (data->job), msg);
g_object_unref (body);
error:
g_clear_error (&error);
g_object_unref (msg);
g_free (data->target_path);
g_slice_free (TrySetDisplayNameData, data);
}
static gboolean
try_set_display_name (GVfsBackend *backend,
GVfsJobSetDisplayName *job,
const char *filename,
const char *display_name)
{
SoupMessage *msg;
GUri *source;
GUri *target;
TrySetDisplayNameData *data = g_slice_new (TrySetDisplayNameData);
char *dirname;
source = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE);
msg = soup_message_new_from_uri (SOUP_METHOD_MOVE, source);
dirname = g_path_get_dirname (filename);
data->target_path = g_build_filename (dirname, display_name, NULL);
target = g_vfs_backend_dav_uri_for_path (backend, data->target_path, FALSE);
message_add_destination_header (msg, target);
message_add_overwrite_header (msg, FALSE);
data->job = job;
g_free (dirname);
g_uri_unref (target);
g_uri_unref (source);
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg, try_set_display_name_cb, data);
return TRUE;
}
typedef struct _CopyData {
GVfsJob *job;
SoupMessage *msg;
GUri *source_uri;
GUri *target_uri;
GFileProgressCallback progress_callback;
gpointer progress_callback_data;
gint64 file_size;
GFileType source_ft;
GFileType target_ft;
GFileCopyFlags flags;
gboolean source_res;
gboolean target_res;
} CopyData;
static void
copy_data_free (gpointer data)
{
CopyData *p = data;
g_clear_pointer (&p->source_uri, g_uri_unref);
g_clear_pointer (&p->target_uri, g_uri_unref);
g_clear_object (&p->msg);
g_slice_free (CopyData, p);
}
static void
try_move_do_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
CopyData *data = user_data;
GInputStream *body;
GError *error = NULL;
guint status;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (data->job, error);
copy_data_free (data);
g_clear_error (&error);
return;
}
/* See try_set_display_name () for the explanation of the PRECONDITION_FAILED
* and IS_REDIRECTION handling below. */
status = soup_message_get_status (data->msg);
if (SOUP_STATUS_IS_SUCCESSFUL (status))
{
if (data->source_res && data->progress_callback)
data->progress_callback (data->file_size, data->file_size,
data->progress_callback_data);
g_vfs_job_succeeded (data->job);
}
else if (status == SOUP_STATUS_PRECONDITION_FAILED ||
SOUP_STATUS_IS_REDIRECTION (status))
g_vfs_job_failed (data->job, G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
else
http_job_failed (data->job, data->msg);
copy_data_free (data);
}
static void
try_move_do (GVfsBackend *backend, CopyData *data)
{
message_add_destination_header (data->msg, data->target_uri);
message_add_overwrite_header (data->msg, data->flags & G_FILE_COPY_OVERWRITE);
g_vfs_backend_dav_send_async (backend, data->msg, try_move_do_cb, data);
}
static void
try_move_target_delete_cb (GObject *source, GAsyncResult *result,
gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result);
CopyData *data = user_data;
GInputStream *body;
GError *error = NULL;
guint status;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (data->job, error);
copy_data_free (data);
g_object_unref (msg);
g_clear_error (&error);
return;
}
status = soup_message_get_status (msg);
if (!SOUP_STATUS_IS_SUCCESSFUL (status))
{
http_job_failed (data->job, msg);
g_object_unref (msg);
copy_data_free (data);
return;
}
g_object_unref (body);
g_object_unref (msg);
try_move_do (backend, data);
}
static void
try_move_source_stat_cb (GObject *source, GAsyncResult *result,
gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
CopyData *data = user_data;
GError *error = NULL;
data->source_res = stat_location_finish (backend, &data->source_ft,
&data->file_size, NULL,
result, &error);
if (data->target_res)
{
if (data->flags & G_FILE_COPY_OVERWRITE)
{
if (data->source_res)
{
if (data->target_ft == G_FILE_TYPE_DIRECTORY)
{
if (data->source_ft == G_FILE_TYPE_DIRECTORY)
g_vfs_job_failed_literal (G_VFS_JOB(data->job),
G_IO_ERROR,
G_IO_ERROR_WOULD_MERGE,
_("Cant move directory over directory"));
else
g_vfs_job_failed_literal (G_VFS_JOB(data->job),
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("Cant move over directory"));
copy_data_free (data);
return;
}
else if (data->source_ft == G_FILE_TYPE_DIRECTORY)
{
/* Overwriting a file with a directory, first remove the
* file */
SoupMessage *msg;
msg = soup_message_new_from_uri (SOUP_METHOD_DELETE,
data->target_uri);
dav_message_connect_signals (msg, backend);
g_vfs_backend_dav_send_async (backend, msg,
try_move_target_delete_cb,
data);
return;
}
}
else
{
g_vfs_job_failed_from_error (data->job, error);
copy_data_free (data);
g_clear_error (&error);
return;
}
}
else
{
g_vfs_job_failed_literal (G_VFS_JOB(data->job),
G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file exists"));
copy_data_free (data);
g_clear_error (&error);
return;
}
}
try_move_do (backend, data);
}
static void
try_move_target_stat_cb (GObject *source, GAsyncResult *result,
gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
CopyData *data = user_data;
gboolean res;
GError *error = NULL;
res = stat_location_finish (backend, &data->target_ft, NULL, NULL,
result, &error);
if (!res && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
{
g_vfs_job_failed_from_error (data->job, error);
g_clear_error (&error);
copy_data_free (data);
return;
}
g_clear_error (&error);
data->target_res = res;
stat_location_async (backend, data->source_uri, FALSE,
try_move_source_stat_cb, data);
}
static gboolean
try_move (GVfsBackend *backend,
GVfsJobMove *job,
const char *source,
const char *destination,
GFileCopyFlags flags,
GFileProgressCallback progress_callback,
gpointer progress_callback_data)
{
CopyData *data = g_slice_new0 (CopyData);
data->job = G_VFS_JOB (job);
data->flags = flags;
data->progress_callback = progress_callback;
data->progress_callback_data = progress_callback_data;
if (flags & G_FILE_COPY_BACKUP)
{
if (flags & G_FILE_COPY_NO_FALLBACK_FOR_MOVE)
{
g_vfs_job_failed_literal (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_CANT_CREATE_BACKUP,
_("Backups not supported"));
}
else
{
/* Return G_IO_ERROR_NOT_SUPPORTED instead of G_IO_ERROR_CANT_CREATE_BACKUP
* to be proceeded with copy and delete fallback (see g_file_move). */
g_vfs_job_failed_literal (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"Operation not supported");
}
copy_data_free (data);
return TRUE;
}
data->source_uri = g_vfs_backend_dav_uri_for_path (backend, source, FALSE);
data->msg = soup_message_new_from_uri (SOUP_METHOD_MOVE, data->source_uri);
data->target_uri = g_vfs_backend_dav_uri_for_path (backend, destination, FALSE);
dav_message_connect_signals (data->msg, backend);
stat_location_async (backend, data->target_uri, FALSE,
try_move_target_stat_cb, data);
return TRUE;
}
static void
try_copy_do_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
CopyData *data = user_data;
GInputStream *body;
GError *error = NULL;
guint status;
body = g_vfs_backend_dav_send_finish (backend, result, &error);
if (!body)
{
g_vfs_job_failed_from_error (data->job, error);
copy_data_free (data);
return;
}
/* See try_set_display_name () for the explanation of the PRECONDITION_FAILED
* and IS_REDIRECTION handling below. */
status = soup_message_get_status (data->msg);
if (SOUP_STATUS_IS_SUCCESSFUL (status))
{
if (data->progress_callback)
data->progress_callback (data->file_size, data->file_size,
data->progress_callback_data);
g_vfs_job_succeeded (data->job);
}
else if (status == SOUP_STATUS_PRECONDITION_FAILED ||
SOUP_STATUS_IS_REDIRECTION (status))
g_vfs_job_failed (data->job, G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
else
http_job_failed (data->job, data->msg);
g_object_unref (body);
copy_data_free (data);
}
static void
try_copy_target_stat_cb (GObject *source, GAsyncResult *result,
gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
CopyData *data = user_data;
GError *error = NULL;
data->target_res = stat_location_finish (backend, &data->target_ft,
NULL, NULL, result, &error);
if (data->target_res)
{
if (data->flags & G_FILE_COPY_OVERWRITE)
{
if (data->target_ft == G_FILE_TYPE_DIRECTORY)
{
if (data->source_ft == G_FILE_TYPE_DIRECTORY)
g_vfs_job_failed_literal (G_VFS_JOB(data->job),
G_IO_ERROR,
G_IO_ERROR_WOULD_MERGE,
_("Cant copy directory over directory"));
else
g_vfs_job_failed_literal (G_VFS_JOB(data->job),
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("File is directory"));
copy_data_free (data);
return;
}
}
else
{
g_vfs_job_failed_literal (data->job,
G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
copy_data_free (data);
return;
}
}
else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
{
g_vfs_job_failed_from_error (data->job, error);
g_clear_error (&error);
copy_data_free (data);
return;
}
g_clear_error (&error);
if (data->source_ft == G_FILE_TYPE_DIRECTORY)
{
g_vfs_job_failed_literal (data->job,
G_IO_ERROR,
G_IO_ERROR_WOULD_RECURSE,
_("Cant recursively copy directory"));
copy_data_free (data);
return;
}
data->msg = soup_message_new_from_uri (SOUP_METHOD_COPY, data->source_uri);
message_add_destination_header (data->msg, data->target_uri);
message_add_overwrite_header (data->msg, data->flags & G_FILE_COPY_OVERWRITE);
dav_message_connect_signals (data->msg, backend);
g_vfs_backend_dav_send_async (backend, data->msg, try_copy_do_cb, data);
}
static void
try_copy_source_stat_cb (GObject *source, GAsyncResult *result,
gpointer user_data)
{
GVfsBackend *backend = G_VFS_BACKEND (source);
CopyData *data = user_data;
gboolean res;
GError *error = NULL;
res = stat_location_finish (backend, &data->source_ft, &data->file_size,
NULL, result, &error);
if (!res)
{
g_vfs_job_failed_from_error (data->job, error);
g_clear_error (&error);
copy_data_free (data);
return;
}
g_clear_error (&error);
data->source_res = res;
stat_location_async (backend, data->target_uri, FALSE,
try_copy_target_stat_cb, data);
}
static gboolean
try_copy (GVfsBackend *backend,
GVfsJobCopy *job,
const char *source,
const char *destination,
GFileCopyFlags flags,
GFileProgressCallback progress_callback,
gpointer progress_callback_data)
{
CopyData *data = g_slice_new0 (CopyData);
data->job = G_VFS_JOB (job);
data->flags = flags;
data->progress_callback = progress_callback;
data->progress_callback_data = progress_callback_data;
if (flags & G_FILE_COPY_BACKUP)
{
/* Return G_IO_ERROR_NOT_SUPPORTED instead of
* G_IO_ERROR_CANT_CREATE_BACKUP to proceed with the GIO fallback
* copy. */
g_vfs_job_failed_literal (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"Operation not supported");
copy_data_free (data);
return TRUE;
}
data->source_uri = g_vfs_backend_dav_uri_for_path (backend, source, FALSE);
data->target_uri = g_vfs_backend_dav_uri_for_path (backend, destination, FALSE);
stat_location_async (backend, data->source_uri, FALSE,
try_copy_source_stat_cb, data);
return TRUE;
}
#define CHUNK_SIZE 65536
typedef struct {
/* Job details */
GVfsBackend *backend;
GVfsJob *job;
GVfsJobPush *op_job;
/* Local file */
GInputStream *in;
goffset size;
/* Remote file */
GUri *uri;
SoupMessage *msg;
goffset n_written;
} PushHandle;
static void
push_handle_free (PushHandle *handle)
{
if (handle->in)
{
g_input_stream_close_async (handle->in, 0, NULL, NULL, NULL);
g_object_unref (handle->in);
}
g_object_unref (handle->backend);
g_object_unref (handle->job);
g_object_unref (handle->msg);
g_uri_unref (handle->uri);
g_slice_free (PushHandle, handle);
}
static void
push_setup_message (PushHandle *handle)
{
message_add_overwrite_header (handle->msg,
handle->op_job->flags & G_FILE_COPY_OVERWRITE);
soup_message_headers_set_encoding (soup_message_get_request_headers (handle->msg),
SOUP_ENCODING_CONTENT_LENGTH);
soup_message_headers_set_content_length (soup_message_get_request_headers (handle->msg),
handle->size);
}
static void
push_restarted (SoupMessage *msg, gpointer user_data)
{
PushHandle *handle = user_data;
handle->n_written = 0;
g_object_set (msg, "method", SOUP_METHOD_PUT, NULL);
push_setup_message (handle);
soup_message_set_request_body (handle->msg, NULL, handle->in, handle->size);
}
static void
push_wrote_body_data (SoupMessage *msg, guint chunk_size, gpointer user_data)
{
PushHandle *handle = user_data;
handle->n_written += chunk_size;
g_vfs_job_progress_callback (handle->n_written, handle->size, handle->job);
}
static void
push_finished (SoupMessage *msg, gpointer user_data)
{
PushHandle *handle = user_data;
if (g_vfs_job_is_finished (handle->job))
; /* We got an error so we finished the job and cancelled msg. */
else if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg)))
http_job_failed (handle->job, msg);
else
{
if (handle->op_job->remove_source)
g_unlink (handle->op_job->local_path);
g_vfs_job_succeeded (handle->job);
}
push_handle_free (handle);
}
static void
push_done (GObject *source, GAsyncResult *result, gpointer user_data)
{
GInputStream *body;
GVfsJob *job = G_VFS_JOB (user_data);
GError *error = NULL;
body = soup_session_send_finish (SOUP_SESSION (source), result, &error);
if (!body)
{
g_vfs_job_failed_from_error (job, error);
g_error_free (error);
}
else
g_object_unref (body);
}
static void
push_stat_dest_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GInputStream *body;
PushHandle *handle = user_data;
GFileType type;
GError *error = NULL;
body = soup_session_send_finish (SOUP_SESSION (source), result, &error);
if (!body)
{
g_vfs_job_failed_from_error (handle->job, error);
g_error_free (error);
return;
}
if (stat_location_end (handle->msg, body, &type, NULL, NULL))
{
if (!(handle->op_job->flags & G_FILE_COPY_OVERWRITE))
{
g_object_unref (body);
g_vfs_job_failed (handle->job,
G_IO_ERROR,
G_IO_ERROR_EXISTS,
_("Target file already exists"));
push_handle_free (handle);
return;
}
if (type == G_FILE_TYPE_DIRECTORY)
{
g_object_unref (body);
g_vfs_job_failed (handle->job,
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("File is directory"));
push_handle_free (handle);
return;
}
}
g_object_unref (body);
g_object_unref (handle->msg);
handle->msg = soup_message_new_from_uri (SOUP_METHOD_PUT, handle->uri);
push_setup_message (handle);
soup_message_set_request_body (handle->msg, NULL, handle->in, handle->size);
g_signal_connect (handle->msg, "restarted",
G_CALLBACK (push_restarted), handle);
g_signal_connect (handle->msg, "wrote-body-data",
G_CALLBACK (push_wrote_body_data), handle);
g_signal_connect (handle->msg, "finished",
G_CALLBACK (push_finished), handle);
dav_message_connect_signals (handle->msg, handle->backend);
soup_session_send_async (G_VFS_BACKEND_HTTP (handle->backend)->session,
handle->msg, G_PRIORITY_DEFAULT,
NULL, push_done, handle->job);
}
static void
push_source_fstat_cb (GObject *source, GAsyncResult *res, gpointer user_data)
{
GFileInputStream *fin = G_FILE_INPUT_STREAM (source);
PushHandle *handle = user_data;
GError *error = NULL;
GFileInfo *info;
info = g_file_input_stream_query_info_finish (fin, res, &error);
if (info)
{
handle->size = g_file_info_get_size (info);
g_object_unref (info);
handle->msg = stat_location_start (handle->uri, FALSE);
dav_message_connect_signals (handle->msg, handle->backend);
soup_session_send_async (G_VFS_BACKEND_HTTP (handle->backend)->session,
handle->msg, G_PRIORITY_DEFAULT, NULL,
push_stat_dest_cb, handle);
}
else
{
g_vfs_job_failed_from_error (handle->job, error);
g_error_free (error);
push_handle_free (handle);
}
}
static void
push_source_open_cb (GObject *source, GAsyncResult *res, gpointer user_data)
{
GFile *source_file = G_FILE (source);
PushHandle *handle = user_data;
GError *error = NULL;
GFileInputStream *fin;
fin = g_file_read_finish (source_file, res, &error);
if (fin)
{
handle->in = G_INPUT_STREAM (fin);
g_file_input_stream_query_info_async (fin,
G_FILE_ATTRIBUTE_STANDARD_SIZE,
0, handle->job->cancellable,
push_source_fstat_cb, handle);
}
else
{
if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_IS_DIRECTORY)
{
/* Fall back to default implementation to improve the error message */
g_vfs_job_failed (handle->job, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
_("Operation not supported"));
}
else
g_vfs_job_failed_from_error (handle->job, error);
g_error_free (error);
push_handle_free (handle);
}
}
static void
push_source_lstat_cb (GObject *source, GAsyncResult *res, gpointer user_data)
{
GFile *source_file = G_FILE (source);
PushHandle *handle = user_data;
GError *error = NULL;
GFileInfo *info;
info = g_file_query_info_finish (source_file, res, &error);
if (!info)
{
g_vfs_job_failed_from_error (handle->job, error);
g_error_free (error);
push_handle_free (handle);
return;
}
if ((handle->op_job->flags & G_FILE_COPY_NOFOLLOW_SYMLINKS) &&
g_file_info_get_file_type (info) == G_FILE_TYPE_SYMBOLIC_LINK)
{
/* Fall back to default implementation to copy symlink */
g_vfs_job_failed (handle->job, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
_("Operation not supported"));
push_handle_free (handle);
g_object_unref (info);
return;
}
g_file_read_async (source_file,
0, handle->job->cancellable,
push_source_open_cb, handle);
g_object_unref (info);
}
static gboolean
try_push (GVfsBackend *backend,
GVfsJobPush *job,
const char *destination,
const char *local_path,
GFileCopyFlags flags,
gboolean remove_source,
GFileProgressCallback progress_callback,
gpointer progress_callback_data)
{
GFile *source;
PushHandle *handle;
if (remove_source && (flags & G_FILE_COPY_NO_FALLBACK_FOR_MOVE))
{
g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
_("Operation not supported"));
return TRUE;
}
handle = g_slice_new0 (PushHandle);
handle->backend = g_object_ref (backend);
handle->job = g_object_ref (G_VFS_JOB (job));
handle->op_job = job;
handle->uri = g_vfs_backend_dav_uri_for_path (backend, destination, FALSE);
source = g_file_new_for_path (local_path);
g_file_query_info_async (source,
G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
0, handle->job->cancellable,
push_source_lstat_cb, handle);
g_object_unref (source);
return TRUE;
}
/* ************************************************************************* */
/* */
static void
g_vfs_backend_dav_class_init (GVfsBackendDavClass *klass)
{
GObjectClass *gobject_class;
GVfsBackendClass *backend_class;
gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = g_vfs_backend_dav_finalize;
backend_class = G_VFS_BACKEND_CLASS (klass);
backend_class->try_mount = try_mount;
backend_class->try_query_info = try_query_info;
backend_class->try_query_fs_info = try_query_fs_info;
backend_class->try_enumerate = try_enumerate;
backend_class->try_open_for_read = try_open_for_read;
backend_class->try_create = try_create;
backend_class->try_replace = try_replace;
backend_class->try_write = try_write;
backend_class->seek_on_write = do_seek_on_write;
backend_class->truncate = do_truncate;
backend_class->try_close_write = try_close_write;
backend_class->try_make_directory = try_make_directory;
backend_class->try_delete = try_delete;
backend_class->try_set_display_name = try_set_display_name;
backend_class->try_move = try_move;
backend_class->try_copy = try_copy;
backend_class->try_push = try_push;
/* override the maximum number of connections, since the libsoup defaults
* of 10 and 2 respectively are too low and may cause backend lockups when
* a lot of files are opened at the same time
*/
http_try_init_session (32, 32);
}