Commit 874bc00b authored by Ross Lagerwall's avatar Ross Lagerwall

Add an nfs backend based on libnfs

Add an nfs backend based on libnfs to make userspace mounting and usage
of nfs shares easier.  The backend is written in a single-threaded,
asynchronous style.  Performance measurements show around 60-70 MiB/s
throughput on 1GbE.

To make use of it, simply mount the share with gvfs-mount or Nautilus
with the following syntax:
nfs://host/export/path

Authentication is simple, based on UNIX uid.

Since this is a userspace nfs client, it comes with the caveat that the
mount needs to be exported with "insecure" on Linux (or some equivalent
for other NFS servers) so that it allows connections from port numbers
higher than 1023.  Alternatively, a special capability can be given to
the binary:
sudo setcap 'cap_net_bind_service=+ep' /path/to/executable

https://bugzilla.gnome.org/show_bug.cgi?id=738967
parent 2fc159a7
......@@ -738,6 +738,27 @@ if test "x$enable_afp" != "xno"; then
fi
AM_CONDITIONAL(USE_AFP, test "x$enable_afp" != "xno")
dnl *******************
dnl *** NFS backend ***
dnl *******************
AC_ARG_ENABLE(nfs, AS_HELP_STRING([--disable-nfs], [build without NFS support]))
msg_nfs="no"
NFS_CFLAGS=
NFS_LIBS=
if test "x$enable_nfs" != "xno"; then
PKG_CHECK_EXISTS([libnfs >= 1.9.7], msg_nfs=yes)
if test "x$msg_nfs" = "xyes"; then
PKG_CHECK_MODULES([NFS],[libnfs >= 1.9.7])
AC_DEFINE(HAVE_NFS, 1, [Define to 1 if nfs is going to be built])
fi
fi
AC_SUBST(NFS_CFLAGS)
AC_SUBST(NFS_LIBS)
AM_CONDITIONAL(HAVE_NFS, [test "$msg_nfs" = "yes"])
dnl ***************************************
dnl *** Check for nl_langinfo constants ***
dnl ***************************************
......@@ -949,6 +970,7 @@ echo "
archive support: $msg_archive
AFC support: $msg_afc
AFP support: $msg_afp
NFS support: $msg_nfs
DNS-SD support: $msg_avahi
Build HAL volume monitor: $msg_hal (with fast init path: $have_hal_fast_init)
Build GDU volume monitor: $msg_gdu
......
......@@ -14,6 +14,7 @@ gvfsd-http
gvfsd-localtest
gvfsd-network
gvfsd-mtp
gvfsd-nfs
gvfsd-obexftp
gvfsd-recent
gvfsd-sftp
......
......@@ -129,6 +129,12 @@ mount_DATA += afp-browse.mount afp.mount
libexec_PROGRAMS += gvfsd-afp-browse gvfsd-afp
endif
mount_in_files += nfs.mount.in
if HAVE_NFS
mount_DATA += nfs.mount
libexec_PROGRAMS += gvfsd-nfs
endif
noinst_DATA = $(mount_DATA:.mount=.localmount)
EXTRA_DIST = \
......@@ -608,6 +614,21 @@ gvfsd_afp_LDADD = \
$(libraries) \
$(LIBGCRYPT_LIBS)
gvfsd_nfs_SOURCES = \
gvfsbackendnfs.c gvfsbackendnfs.h \
daemon-main.c daemon-main.h \
daemon-main-generic.c
gvfsd_nfs_CPPFLAGS = \
$(flags) \
-DBACKEND_HEADER=gvfsbackendnfs.h \
-DDEFAULT_BACKEND_TYPE=nfs \
-DMAX_JOB_THREADS=1 \
-DBACKEND_TYPES='"nfs", G_VFS_TYPE_BACKEND_NFS,' \
$(NFS_CFLAGS)
gvfsd_nfs_LDADD = $(libraries) $(NFS_LIBS)
# GSettings stuff
gsettings_ENUM_NAMESPACE = org.gnome.system.gvfs
......
/* GIO - GLib Input, Output and Streaming Library
*
* Copyright (C) 2014 Ross Lagerwall
*
* 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, see
* <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <glib/gstdio.h>
#include <glib/gi18n.h>
#include <gio/gio.h>
#include "gvfsbackendnfs.h"
#include "gvfsjobopenforread.h"
#include "gvfsjobread.h"
#include "gvfsjobseekread.h"
#include "gvfsjobdelete.h"
#include "gvfsjobopenforwrite.h"
#include "gvfsjobwrite.h"
#include "gvfsjobclosewrite.h"
#include "gvfsjobseekwrite.h"
#include "gvfsjobtruncate.h"
#include "gvfsjobsetdisplayname.h"
#include "gvfsjobqueryinfo.h"
#include "gvfsjobqueryinforead.h"
#include "gvfsjobqueryinfowrite.h"
#include "gvfsjobqueryfsinfo.h"
#include "gvfsjobqueryattributes.h"
#include "gvfsjobsetattribute.h"
#include "gvfsjobenumerate.h"
#include "gvfsjobmove.h"
#include "gvfsdaemonprotocol.h"
#include "gvfsdaemonutils.h"
#include "gvfsutils.h"
#include <nfsc/libnfs.h>
#include <nfsc/libnfs-raw-nfs.h>
#include <nfsc/libnfs-raw-mount.h>
struct _GVfsBackendNfs
{
GVfsBackend parent_instance;
struct nfs_context *ctx;
GSource *source;
mode_t umask; /* cached umask of process */
};
typedef struct
{
GSource source;
struct nfs_context *ctx;
GVfsBackendNfs *backend;
int fd; /* fd registered for IO */
gpointer tag; /* tag for fd attached to this source */
int events; /* IO events we're interested in */
} NfsSource;
G_DEFINE_TYPE (GVfsBackendNfs, g_vfs_backend_nfs, G_VFS_TYPE_BACKEND)
static void
g_vfs_backend_nfs_init (GVfsBackendNfs *backend)
{
}
static void
g_vfs_backend_nfs_destroy_context (GVfsBackendNfs *backend)
{
if (backend->ctx)
{
nfs_destroy_context (backend->ctx);
backend->ctx = NULL;
}
if (backend->source)
{
g_source_destroy (backend->source);
g_source_unref (backend->source);
backend->source = NULL;
}
}
static void
g_vfs_backend_nfs_finalize (GObject *object)
{
GVfsBackendNfs *backend = G_VFS_BACKEND_NFS (object);
g_vfs_backend_nfs_destroy_context (backend);
if (G_OBJECT_CLASS (g_vfs_backend_nfs_parent_class)->finalize)
(*G_OBJECT_CLASS (g_vfs_backend_nfs_parent_class)->finalize) (object);
}
static gboolean
nfs_source_prepare (GSource *source, gint *timeout)
{
NfsSource *nfs_source = (NfsSource *) source;
int events, fd;
*timeout = -1;
fd = nfs_get_fd (nfs_source->ctx);
events = nfs_which_events (nfs_source->ctx);
if (fd < 0)
{
g_vfs_backend_force_unmount (G_VFS_BACKEND (nfs_source->backend));
g_vfs_backend_nfs_destroy_context (nfs_source->backend);
}
else if (fd != nfs_source->fd)
{
g_source_remove_unix_fd (source, nfs_source->tag);
nfs_source->fd = fd;
nfs_source->events = events;
nfs_source->tag = g_source_add_unix_fd (source, nfs_source->fd, events);
}
else if (events != nfs_source->events)
{
nfs_source->events = events;
g_source_modify_unix_fd (source, nfs_source->tag, events);
}
return FALSE;
}
static gboolean
nfs_source_dispatch (GSource *source, GSourceFunc callback, gpointer user_data)
{
NfsSource *nfs_source = (NfsSource *) source;
int err;
err = nfs_service (nfs_source->ctx,
g_source_query_unix_fd (source, nfs_source->tag));
if (err)
{
g_warning ("nfs_service error: %d, %s\n",
err, nfs_get_error (nfs_source->ctx));
g_vfs_backend_force_unmount (G_VFS_BACKEND (nfs_source->backend));
g_vfs_backend_nfs_destroy_context (nfs_source->backend);
}
return G_SOURCE_CONTINUE;
}
static void
do_mount (GVfsBackend *backend,
GVfsJobMount *job,
GMountSpec *mount_spec,
GMountSource *mount_source,
gboolean is_automount)
{
GVfsBackendNfs *op_backend = G_VFS_BACKEND_NFS (backend);
GMountSpec *nfs_mount_spec;
GSource *source;
NfsSource *nfs_source;
struct exportnode *export_list, *ptr;
const char *host;
char *basename, *display_name, *export = NULL;
int err;
size_t pathlen = strlen (mount_spec->mount_prefix);
size_t exportlen = SIZE_MAX;
static GSourceFuncs nfs_source_callbacks = {
nfs_source_prepare,
NULL,
nfs_source_dispatch,
NULL
};
host = g_mount_spec_get (mount_spec, "host");
export_list = mount_getexports (host);
/* Find the shortest matching mount. E.g. if the given mount_prefix is
* /some/long/path and there exist two mounts, /some and /some/long, match
* against /some. */
for (ptr = export_list; ptr; ptr = ptr->ex_next)
{
/* First check that the NFS mount point is a prefix of the mount_prefix. */
if (g_str_has_prefix (mount_spec->mount_prefix, ptr->ex_dir))
{
size_t this_exportlen = strlen (ptr->ex_dir);
/* Check if the mount_prefix is longer than the NFS mount point.
* E.g. mount_prefix: /mnt/file, mount point: /mnt */
if (pathlen > this_exportlen)
{
/* Check if the mount_prefix has a slash at the correct point.
* E.g. if the mount point is /mnt, then it's a match if the
* mount_prefix is /mnt/a but not a match if the mount_prefix is
* /mnta. Choose it if it is the shortest found so far. */
char *s = mount_spec->mount_prefix + this_exportlen;
if (*s == '/' && this_exportlen < exportlen)
{
export = ptr->ex_dir;
exportlen = this_exportlen;
}
}
/* The mount_prefix and NFS mount point are identical. Choose it if
* it is the shortest found so far. */
else if (this_exportlen < exportlen)
{
export = ptr->ex_dir;
exportlen = this_exportlen;
}
}
}
if (!export)
{
mount_free_export_list (export_list);
g_vfs_job_failed_literal (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
_("Mount point does not exist"));
return;
}
export = strdup (export);
mount_free_export_list (export_list);
op_backend->ctx = nfs_init_context ();
err = nfs_mount (op_backend->ctx, host, export);
if (err)
{
if (err == -EACCES)
{
g_vfs_job_failed_literal (G_VFS_JOB (job),
G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
_("Permission denied: Perhaps this host is disallowed or a privileged port is needed"));
}
else
{
g_vfs_job_failed_from_errno (G_VFS_JOB (job), -err);
}
g_free (export);
return;
}
source = g_source_new (&nfs_source_callbacks, sizeof (NfsSource));
nfs_source = (NfsSource *) source;
nfs_source->ctx = op_backend->ctx;
nfs_source->backend = op_backend;
nfs_source->events = nfs_which_events (op_backend->ctx);
nfs_source->fd = nfs_get_fd (op_backend->ctx);
nfs_source->tag = g_source_add_unix_fd (source,
nfs_source->fd,
nfs_source->events);
g_source_attach (source, NULL);
op_backend->source = source;
basename = g_path_get_basename (export);
/* Translators: This is "<mount point> on <host>" and is used as name for an NFS mount */
display_name = g_strdup_printf (_("%s on %s"), basename, host);
g_vfs_backend_set_display_name (backend, display_name);
g_free (basename);
g_free (display_name);
g_vfs_backend_set_icon_name (G_VFS_BACKEND (backend), "folder-remote");
g_vfs_backend_set_symbolic_icon_name (G_VFS_BACKEND (backend), "folder-remote-symbolic");
nfs_mount_spec = g_mount_spec_new ("nfs");
g_mount_spec_set (nfs_mount_spec, "host", host);
g_mount_spec_set_mount_prefix (nfs_mount_spec, export);
g_vfs_backend_set_mount_spec (backend, nfs_mount_spec);
g_mount_spec_unref (nfs_mount_spec);
g_free (export);
/* cache the process's umask for later */
op_backend->umask = umask (0);
umask (op_backend->umask);
g_vfs_job_succeeded (G_VFS_JOB (job));
}
static void
null_cb (int err, struct nfs_context *ctx, void *data, void *private_data)
{
}
static void
generic_cb (int err, struct nfs_context *ctx, void *data, void *private_data)
{
GVfsJob *job = G_VFS_JOB (private_data);
if (err == 0)
g_vfs_job_succeeded (job);
else
g_vfs_job_failed_from_errno (job, -err);
}
static char *
create_etag (uint64_t mtime, uint32_t nsec)
{
return g_strdup_printf ("%lu:%lu",
(long unsigned int)mtime,
(long unsigned int)nsec);
}
static void
open_for_read_fstat_cb (int err,
struct nfs_context *ctx,
void *data, void *private_data)
{
GVfsJob *job = G_VFS_JOB (private_data);
if (err == 0)
{
GVfsJobOpenForRead *op_job = G_VFS_JOB_OPEN_FOR_READ (job);
struct stat *st = data;
if (S_ISDIR (st->st_mode))
{
struct nfsfh *fh = op_job->backend_handle;
nfs_close_async (ctx, fh, null_cb, NULL);
g_vfs_job_failed_literal (job,
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("Can't open directory"));
return;
}
}
g_vfs_job_succeeded (job);
}
static void
open_for_read_cb (int err,
struct nfs_context *ctx,
void *data, void *private_data)
{
if (err == 0)
{
GVfsJobOpenForRead *op_job = G_VFS_JOB_OPEN_FOR_READ (private_data);
g_vfs_job_open_for_read_set_handle (op_job, data);
g_vfs_job_open_for_read_set_can_seek (op_job, TRUE);
nfs_fstat_async (ctx, data, open_for_read_fstat_cb, private_data);
}
else
{
g_vfs_job_failed_from_errno (G_VFS_JOB (private_data), -err);
}
}
static gboolean
try_open_for_read (GVfsBackend *backend,
GVfsJobOpenForRead *job,
const char *filename)
{
GVfsBackendNfs *op_backend = G_VFS_BACKEND_NFS (backend);
nfs_open_async (op_backend->ctx, filename, O_RDONLY, open_for_read_cb, job);
return TRUE;
}
static void
read_cb (int err, struct nfs_context *ctx, void *data, void *private_data)
{
GVfsJob *job = G_VFS_JOB (private_data);
if (err >= 0)
{
GVfsJobRead *op_job = G_VFS_JOB_READ (job);
memcpy (op_job->buffer, data, err);
g_vfs_job_read_set_size (op_job, err);
g_vfs_job_succeeded (job);
}
else
{
g_vfs_job_failed_from_errno (job, -err);
}
}
static gboolean
try_read (GVfsBackend *backend,
GVfsJobRead *job,
GVfsBackendHandle _handle,
char *buffer,
gsize bytes_requested)
{
GVfsBackendNfs *op_backend = G_VFS_BACKEND_NFS (backend);
struct nfsfh *fh = _handle;
nfs_read_async (op_backend->ctx, fh, bytes_requested, read_cb, job);
return TRUE;
}
static const char *
set_type_from_mode (GFileInfo *info, uint64_t mode)
{
GFileType type = G_FILE_TYPE_UNKNOWN;
const char *mimetype = NULL;
if (S_ISREG (mode))
type = G_FILE_TYPE_REGULAR;
else if (S_ISDIR (mode))
{
type = G_FILE_TYPE_DIRECTORY;
mimetype = "inode/directory";
}
else if (S_ISFIFO (mode))
{
type = G_FILE_TYPE_SPECIAL;
mimetype = "inode/fifo";
}
else if (S_ISSOCK (mode))
{
type = G_FILE_TYPE_SPECIAL;
mimetype = "inode/socket";
}
else if (S_ISCHR (mode))
{
type = G_FILE_TYPE_SPECIAL;
mimetype = "inode/chardevice";
}
else if (S_ISBLK (mode))
{
type = G_FILE_TYPE_SPECIAL;
mimetype = "inode/blockdevice";
}
else if (S_ISLNK (mode))
{
type = G_FILE_TYPE_SYMBOLIC_LINK;
g_file_info_set_is_symlink (info, TRUE);
mimetype = "inode/symlink";
}
g_file_info_set_file_type (info, type);
return mimetype;
}
static void
set_name_info (GFileInfo *info,
const char *mimetype,
const char *basename,
gboolean is_directory,
GFileAttributeMatcher *matcher)
{
char *free_mimetype = NULL;
g_file_info_set_name (info, basename);
if (basename[0] == '.')
g_file_info_set_is_hidden (info, TRUE);
if (basename[strlen (basename) -1] == '~')
g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP, TRUE);
if (g_file_attribute_matcher_matches (matcher,
G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) ||
g_file_attribute_matcher_matches (matcher,
G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME))
{
char *edit_name = gvfs_file_info_populate_names_as_local (info, basename);
g_free (edit_name);
}
if (mimetype == NULL)
{
if (basename)
{
free_mimetype = g_content_type_guess (basename, NULL, 0, NULL);
mimetype = free_mimetype;
}
else
mimetype = "application/octet-stream";
}
g_file_info_set_content_type (info, mimetype);
g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, mimetype);
if (g_file_attribute_matcher_matches (matcher,
G_FILE_ATTRIBUTE_STANDARD_ICON) ||
g_file_attribute_matcher_matches (matcher,
G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON))
{
GIcon *icon = NULL;
GIcon *symbolic_icon = NULL;
if (is_directory)
{
icon = g_themed_icon_new ("folder");
symbolic_icon = g_themed_icon_new ("folder-symbolic");
}
else if (mimetype)
{
icon = g_content_type_get_icon (mimetype);
symbolic_icon = g_content_type_get_symbolic_icon (mimetype);
}
if (icon == NULL)
icon = g_themed_icon_new ("text-x-generic");
if (symbolic_icon == NULL)
symbolic_icon = g_themed_icon_new ("text-x-generic-symbolic");
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);
}
if (free_mimetype)
g_free (free_mimetype);
}
static void
set_info_from_stat (GFileInfo *info, struct nfs_stat_64 *st, GFileAttributeMatcher *matcher)
{
g_file_info_set_size (info, st->nfs_size);
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE, st->nfs_used);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_MODE, st->nfs_mode);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_UID, st->nfs_uid);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_GID, st->nfs_gid);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_NLINK, st->nfs_nlink);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_DEVICE, st->nfs_dev);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_RDEV, st->nfs_rdev);
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_UNIX_INODE, st->nfs_ino);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_BLOCK_SIZE, st->nfs_blksize);
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_UNIX_BLOCKS, st->nfs_blocks);
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_ACCESS, st->nfs_atime);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_ACCESS_USEC, st->nfs_atime_nsec / 1000);
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED, st->nfs_mtime);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, st->nfs_mtime_nsec / 1000);
g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CHANGED, st->nfs_ctime);
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC, st->nfs_ctime_nsec / 1000);
g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE);
}
static void
query_info_on_read_cb (int err,
struct nfs_context *ctx,
void *data, void *private_data)
{
GVfsJob *job = G_VFS_JOB (private_data);
if (err == 0)
{
GVfsJobQueryInfoRead *op_job = G_VFS_JOB_QUERY_INFO_READ (job);
struct nfs_stat_64 *st = data;
set_info_from_stat (op_job->file_info, st, op_job->attribute_matcher);
set_type_from_mode (op_job->file_info, st->nfs_mode);
g_vfs_job_succeeded (job);
}
else
{
g_vfs_job_failed_from_errno (job, -err);
}
}
static gboolean