summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Walters <walters@verbum.org>2016-02-10 12:42:54 +0100
committerColin Walters <walters@verbum.org>2016-02-10 13:11:25 +0100
commite9ccdd2d007801ef25cc7283188942d791889c27 (patch)
treeb3d3c0d68d0b016c9a00fadd5b0589ed917c1d0d
parent5adafd767406820cce260c567a1b936610e8d67a (diff)
downloadostree-e9ccdd2d007801ef25cc7283188942d791889c27.tar.gz
Import rofiles-fuse
While it's not strictly tied to OSTree, let's move https://github.com/cgwalters/rofiles-fuse in here because: - It's *very* useful in concert with OSTree - It's tiny - We can reuse OSTree's test, documentation, etc. infrastructure One thing to consider also is that at some point we could experiment with writing a FUSE filesystem for OSTree. This could internalize a better equivalent of `--link-checkout-speedup`, but on the other hand, the cost of walking filesystem trees for these types of operations is really quite small. But if we did decide to do more FUSE things in OSTree, this is a step towards that too.
-rw-r--r--Makefile-man.am4
-rw-r--r--Makefile-tests.am5
-rw-r--r--Makefile.am1
-rw-r--r--configure.ac13
-rw-r--r--man/rofiles-fuse.xml104
-rw-r--r--src/rofiles-fuse/Makefile-inc.am23
-rw-r--r--src/rofiles-fuse/README.md48
-rw-r--r--src/rofiles-fuse/main.c598
-rwxr-xr-xtests/test-rofiles-fuse.sh74
9 files changed, 870 insertions, 0 deletions
diff --git a/Makefile-man.am b/Makefile-man.am
index f70a577c..a6090bf4 100644
--- a/Makefile-man.am
+++ b/Makefile-man.am
@@ -21,6 +21,10 @@ if ENABLE_MAN
man1_files = ostree.1 ostree-admin-cleanup.1 ostree-admin-config-diff.1 ostree-admin-deploy.1 ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1 ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1 ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 ostree-commit.1 ostree-gpg-sign.1 ostree-config.1 ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1 ostree-prune.1 ostree-pull-local.1 ostree-pull.1 ostree-refs.1 ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 ostree-show.1 ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1
+if BUILDOPT_FUSE
+man1_files += rofiles-fuse.1
+endif
+
man5_files = ostree.repo.5 ostree.repo-config.5
man1_MANS = $(addprefix man/,$(man1_files))
diff --git a/Makefile-tests.am b/Makefile-tests.am
index 9f3feef6..b0466840 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -61,6 +61,11 @@ testfiles = test-basic \
test-auto-summary \
test-prune \
$(NULL)
+
+if BUILDOPT_FUSE
+testfiles += test-rofiles-fuse
+endif
+
insttest_SCRIPTS = $(addprefix tests/,$(testfiles:=.sh))
# This one uses corrupt-repo-ref.js
diff --git a/Makefile.am b/Makefile.am
index da904da2..e1c942d7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -68,6 +68,7 @@ include Makefile-otutil.am
include Makefile-libostree.am
include Makefile-ostree.am
include Makefile-switchroot.am
+include src/rofiles-fuse/Makefile-inc.am
include Makefile-tests.am
include Makefile-boot.am
include Makefile-man.am
diff --git a/configure.ac b/configure.ac
index 03cc746a..6a34c0d4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -117,6 +117,8 @@ AS_IF([ test x$have_gpgme = xno ], [
OSTREE_FEATURES="$OSTREE_FEATURES +gpgme"
LIBARCHIVE_DEPENDENCY="libarchive >= 2.8.0"
+# What's in RHEL7.2.
+FUSE_DEPENDENCY="fuse >= 2.9.2"
# check for gtk-doc
m4_ifdef([GTK_DOC_CHECK], [
@@ -194,6 +196,16 @@ AS_IF([ test x$with_selinux != xno ], [
if test x$with_selinux != xno; then OSTREE_FEATURES="$OSTREE_FEATURES +selinux"; fi
AM_CONDITIONAL(USE_SELINUX, test $with_selinux != no)
+# Enabled by default because I think people should use it.
+AC_ARG_ENABLE(rofiles-fuse,
+ [AS_HELP_STRING([--enable-rofiles-fuse],
+ [generate rofiles-fuse helper [default=yes]])],,
+ enable_rofiles_fuse=yes)
+AS_IF([ test $enable_rofiles_fuse != xno ], [
+ PKG_CHECK_MODULES(BUILDOPT_FUSE, $FUSE_DEPENDENCY)
+], [enable_rofiles_fuse=no])
+AM_CONDITIONAL(BUILDOPT_FUSE, test x$enable_rofiles_fuse = xyes)
+
AC_ARG_WITH(dracut,
AS_HELP_STRING([--with-dracut],
[Install dracut module (default: no)]),,
@@ -248,6 +260,7 @@ echo "
introspection: $found_introspection
+ rofiles-fuse: $enable_rofiles_fuse
libsoup (retrieve remote HTTP repositories): $with_soup
libsoup TLS client certs: $have_libsoup_client_certs
SELinux: $with_selinux
diff --git a/man/rofiles-fuse.xml b/man/rofiles-fuse.xml
new file mode 100644
index 00000000..3d282c00
--- /dev/null
+++ b/man/rofiles-fuse.xml
@@ -0,0 +1,104 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<!--
+Copyright 2016 Colin Walters <walters@verbum.org>
+
+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., 59 Temple Place - Suite 330,
+Boston, MA 02111-1307, USA.
+-->
+
+<refentry id="ostree">
+
+ <refentryinfo>
+ <title>rofiles-fuse</title>
+ <productname>rofiles-fuse</productname>
+
+ <authorgroup>
+ <author>
+ <contrib>Developer</contrib>
+ <firstname>Colin</firstname>
+ <surname>Walters</surname>
+ <email>walters@verbum.org</email>
+ </author>
+ </authorgroup>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>rofiles-fuse</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>rofiles-fuse</refname>
+ <refpurpose>Use FUSE to create a view where directories are writable, files are immutable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>rofiles-fuse SRCDIR MNTPOINT</command>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ Creating a checkout from an OSTree repository by default
+ uses hard links, which means an in-place mutation to any
+ file corrupts the repository and all checkouts. This can be
+ problematic if one wishes to run arbitrary programs against
+ such a checkout. For example, RPM <literal>%post</literal>
+ scripts or equivalent.
+ </para>
+
+ <para>
+ In the case where one wants to create a tree commit derived
+ from other content, using <command>rofiles-fuse</command> in
+ concert with <command>ostree commit
+ --link-checkout-speedup</command> (or the underlying API)
+ can ensure that only new files are checksummed.
+ </para>
+
+ </refsect1>
+
+ <refsect1>
+ <title>Example: Update an OSTree commit</title>
+ <programlisting>
+# Initialize a checkout and mount
+$ ostree --repo=repo checkout somebranch branch-checkout
+$ mkdir mnt
+$ rofiles-fuse branch-checkout mnt
+
+# Now, arbitrary changes to mnt/ are reflected in branch-checkout
+$ echo somenewcontent > mnt/anewfile
+$ mkdir mnt/anewdir
+$ rm mnt/someoriginalcontent -rf
+
+# Commit and cleanup
+$ fusermount -u mnt
+$ ostree --repo=repo commit --link-checkout-speedup -b somebranch -s 'Commit new content' --tree=dir=branch-checkout
+$ rm mnt branch-checkout -rf
+ </programlisting>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>ostree</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/src/rofiles-fuse/Makefile-inc.am b/src/rofiles-fuse/Makefile-inc.am
new file mode 100644
index 00000000..5510a2bd
--- /dev/null
+++ b/src/rofiles-fuse/Makefile-inc.am
@@ -0,0 +1,23 @@
+# Copyright (C) 2016 Colin Walters <walters@verbum.org>
+#
+# 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., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+bin_PROGRAMS += rofiles-fuse
+
+rofiles_fuse_SOURCES = src/rofiles-fuse/main.c
+
+rofiles_fuse_CFLAGS = $(AM_CFLAGS) -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 $(BUILDOPT_FUSE_CFLAGS) $(OT_INTERNAL_GIO_UNIX_CFLAGS) -I$(srcdir)/libglnx $(NULL)
+rofiles_fuse_LDADD = libglnx.la $(BUILDOPT_FUSE_LIBS) $(OT_INTERNAL_GIO_UNIX_LIBS)
diff --git a/src/rofiles-fuse/README.md b/src/rofiles-fuse/README.md
new file mode 100644
index 00000000..1f18afcc
--- /dev/null
+++ b/src/rofiles-fuse/README.md
@@ -0,0 +1,48 @@
+rofiles-fuse
+============
+
+Create a mountpoint that represents an underlying directory hierarchy,
+but where non-directory inodes cannot have content or xattrs changed.
+Files can still be unlinked, and new ones created.
+
+This filesystem is designed for OSTree and other systems that create
+"hardlink farms", i.e. filesystem trees deduplicated via hardlinks.
+
+Normally with hard links, if you change one, you change them all.
+
+There are two approaches to dealing with that:
+ - Copy on write: implemented by BTRFS, overlayfs, and http://linux-vserver.org/util-vserver:Vhashify
+ - Make them read-only: what this FUSE mount does
+
+Usage
+=====
+
+Let's say that you have immutable data in `/srv/backups/20150410`, and
+you want to update it with a new version, storing the result in
+`/srv/backups/20150411`. Further assume that all software operating
+on the directory does the "create tempfile and `rename()`" dance
+rather than in-place edits.
+
+ $ mkdir -p /srv/backups/mnt # Ensure we have a mount point
+ $ cp -al /srv/backups/20150410 /srv/backups/20150411
+ $ rofiles-fuse /srv/backups/20150411 /srv/backups/mnt
+
+Now we have a "rofiles" mount at `/srv/backups/mnt`. If we try this:
+
+ $ echo new doc content > /srv/backups/mnt/document
+ bash: /srv/backups/mnt/document: Read-only file system
+
+It failed because the `>` redirection operator will try to truncate
+the existing file. If instead we create `document.tmp` and then
+rename it atomically over the old one, it will work:
+
+ $ echo new doc content > /srv/backups/mnt/document.tmp
+ $ mv /srv/backups/mnt/document.tmp /srv/backups/mnt/document
+
+Let's unmount:
+
+ $ fusermount -u /srv/backups/mnt
+
+Now we have two directories `/srv/backups/20150410`
+`/srv/backups/20150411` which share all file storage except for the
+new document.
diff --git a/src/rofiles-fuse/main.c b/src/rofiles-fuse/main.c
new file mode 100644
index 00000000..004ad3d2
--- /dev/null
+++ b/src/rofiles-fuse/main.c
@@ -0,0 +1,598 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2015,2016 Colin Walters <walters@verbum.org>
+ *
+ * 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., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#define FUSE_USE_VERSION 26
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <stdio.h>
+#include <strings.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/xattr.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <fuse.h>
+
+#include <glib.h>
+
+#include "libglnx.h"
+
+// Global to store our read-write path
+static int basefd = -1;
+static GHashTable *created_devino_hash = NULL;
+
+static inline const char *
+ENSURE_RELPATH (const char *path)
+{
+ return path + strspn (path, "/");
+}
+
+typedef struct {
+ dev_t dev;
+ ino_t ino;
+} DevIno;
+
+static guint
+devino_hash (gconstpointer a)
+{
+ DevIno *a_i = (gpointer)a;
+ return (guint) (a_i->dev + a_i->ino);
+}
+
+static int
+devino_equal (gconstpointer a,
+ gconstpointer b)
+{
+ DevIno *a_i = (gpointer)a;
+ DevIno *b_i = (gpointer)b;
+ return a_i->dev == b_i->dev
+ && a_i->ino == b_i->ino;
+}
+
+static gboolean
+devino_set_contains (dev_t dev, ino_t ino)
+{
+ DevIno devino = { dev, ino };
+ return g_hash_table_contains (created_devino_hash, &devino);
+}
+
+static gboolean
+devino_set_insert (dev_t dev, ino_t ino)
+{
+ DevIno *devino = g_new (DevIno, 1);
+ devino->dev = dev;
+ devino->ino = ino;
+ return g_hash_table_add (created_devino_hash, devino);
+}
+
+static gboolean
+devino_set_remove (dev_t dev, ino_t ino)
+{
+ DevIno devino = { dev, ino };
+ return g_hash_table_remove (created_devino_hash, &devino);
+}
+
+static int
+callback_getattr (const char *path, struct stat *st_data)
+{
+ path = ENSURE_RELPATH (path);
+ if (!*path)
+ {
+ if (fstat (basefd, st_data) == -1)
+ return -errno;
+ }
+ else
+ {
+ if (fstatat (basefd, path, st_data, AT_SYMLINK_NOFOLLOW) == -1)
+ return -errno;
+ }
+ return 0;
+}
+
+static int
+callback_readlink (const char *path, char *buf, size_t size)
+{
+ int r;
+
+ path = ENSURE_RELPATH (path);
+
+ /* Note FUSE wants the string to be always nul-terminated, even if
+ * truncated.
+ */
+ r = readlinkat (basefd, path, buf, size - 1);
+ if (r == -1)
+ return -errno;
+ buf[r] = '\0';
+ return 0;
+}
+
+static int
+callback_readdir (const char *path, void *buf, fuse_fill_dir_t filler,
+ off_t offset, struct fuse_file_info *fi)
+{
+ DIR *dp;
+ struct dirent *de;
+ int dfd;
+
+ path = ENSURE_RELPATH (path);
+
+ if (!*path)
+ {
+ dfd = fcntl (basefd, F_DUPFD_CLOEXEC, 3);
+ lseek (dfd, 0, SEEK_SET);
+ }
+ else
+ {
+ dfd = openat (basefd, path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+ if (dfd == -1)
+ return -errno;
+ }
+
+ /* Transfers ownership of fd */
+ dp = fdopendir (dfd);
+ if (dp == NULL)
+ return -errno;
+
+ while ((de = readdir (dp)) != NULL)
+ {
+ struct stat st;
+ memset (&st, 0, sizeof (st));
+ st.st_ino = de->d_ino;
+ st.st_mode = de->d_type << 12;
+ if (filler (buf, de->d_name, &st, 0))
+ break;
+ }
+
+ (void) closedir (dp);
+ return 0;
+}
+
+static int
+callback_mknod (const char *path, mode_t mode, dev_t rdev)
+{
+ return -EROFS;
+}
+
+static int
+callback_mkdir (const char *path, mode_t mode)
+{
+ path = ENSURE_RELPATH (path);
+ if (mkdirat (basefd, path, mode) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_unlink (const char *path)
+{
+ struct stat stbuf;
+ path = ENSURE_RELPATH (path);
+
+ if (fstatat (basefd, path, &stbuf, AT_SYMLINK_NOFOLLOW) == 0)
+ {
+ if (!S_ISDIR (stbuf.st_mode))
+ devino_set_remove (stbuf.st_dev, stbuf.st_ino);
+ }
+
+ if (unlinkat (basefd, path, 0) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_rmdir (const char *path)
+{
+ path = ENSURE_RELPATH (path);
+ if (unlinkat (basefd, path, AT_REMOVEDIR) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_symlink (const char *from, const char *to)
+{
+ struct stat stbuf;
+
+ to = ENSURE_RELPATH (to);
+
+ if (symlinkat (from, basefd, to) == -1)
+ return -errno;
+
+ if (fstatat (basefd, to, &stbuf, AT_SYMLINK_NOFOLLOW) == -1)
+ {
+ fprintf (stderr, "Failed to find newly created symlink '%s': %s\n",
+ to, g_strerror (errno));
+ exit (1);
+ }
+ return 0;
+}
+
+static int
+callback_rename (const char *from, const char *to)
+{
+ from = ENSURE_RELPATH (from);
+ to = ENSURE_RELPATH (to);
+ if (renameat (basefd, from, basefd, to) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_link (const char *from, const char *to)
+{
+ from = ENSURE_RELPATH (from);
+ to = ENSURE_RELPATH (to);
+ if (linkat (basefd, from, basefd, to, 0) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+can_write (const char *path)
+{
+ struct stat stbuf;
+ if (fstatat (basefd, path, &stbuf, 0) == -1)
+ {
+ if (errno == ENOENT)
+ return 0;
+ else
+ return -errno;
+ }
+ if (devino_set_contains (stbuf.st_dev, stbuf.st_ino))
+ return -EROFS;
+ return 0;
+}
+
+#define VERIFY_WRITE(path) do { \
+ int r = can_write (path); \
+ if (r != 0) \
+ return r; \
+ } while (0)
+
+static int
+callback_chmod (const char *path, mode_t mode)
+{
+ path = ENSURE_RELPATH (path);
+ VERIFY_WRITE(path);
+ if (fchmodat (basefd, path, mode, 0) != 0)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_chown (const char *path, uid_t uid, gid_t gid)
+{
+ path = ENSURE_RELPATH (path);
+ VERIFY_WRITE(path);
+ if (fchownat (basefd, path, uid, gid, 0) != 0)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_truncate (const char *path, off_t size)
+{
+ glnx_fd_close int fd = -1;
+
+ path = ENSURE_RELPATH (path);
+ VERIFY_WRITE(path);
+
+ fd = openat (basefd, path, O_RDWR | O_CREAT);
+ if (fd == -1)
+ return -errno;
+
+ if (ftruncate (fd, size) == -1)
+ return -errno;
+
+ return 0;
+}
+
+static int
+callback_utime (const char *path, struct utimbuf *buf)
+{
+ struct timespec ts[2];
+
+ path = ENSURE_RELPATH (path);
+
+ ts[0].tv_sec = buf->actime;
+ ts[0].tv_nsec = UTIME_OMIT;
+ ts[1].tv_sec = buf->modtime;
+ ts[1].tv_nsec = UTIME_OMIT;
+
+ if (utimensat (basefd, path, ts, AT_SYMLINK_NOFOLLOW) == -1)
+ return -errno;
+
+ return 0;
+}
+
+static int
+do_open (const char *path, mode_t mode, struct fuse_file_info *finfo)
+{
+ const int flags = finfo->flags & O_ACCMODE;
+ int fd;
+ struct stat stbuf;
+
+ /* Support read only opens */
+ G_STATIC_ASSERT (O_RDONLY == 0);
+
+ path = ENSURE_RELPATH (path);
+
+ if (flags == 0)
+ fd = openat (basefd, path, flags);
+ else
+ {
+ const int forced_excl_flags = flags | O_CREAT | O_EXCL;
+ /* Do an exclusive open, don't allow writable fds for existing
+ files */
+ fd = openat (basefd, path, forced_excl_flags, mode);
+ /* If they didn't specify O_EXCL, give them EROFS if the file
+ * exists.
+ */
+ if (fd == -1 && (flags & O_EXCL) == 0)
+ {
+ if (errno == EEXIST)
+ errno = EROFS;
+ }
+ else if (fd != -1)
+ {
+ if (fstat (fd, &stbuf) == -1)
+ return -errno;
+ devino_set_insert (stbuf.st_dev, stbuf.st_ino);
+ }
+ }
+
+ if (fd == -1)
+ return -errno;
+
+ finfo->fh = fd;
+
+ return 0;
+}
+
+static int
+callback_open (const char *path, struct fuse_file_info *finfo)
+{
+ return do_open (path, 0, finfo);
+}
+
+static int
+callback_create(const char *path, mode_t mode, struct fuse_file_info *finfo)
+{
+ return do_open (path, mode, finfo);
+}
+
+static int
+callback_read (const char *path, char *buf, size_t size, off_t offset,
+ struct fuse_file_info *finfo)
+{
+ int r;
+ r = pread (finfo->fh, buf, size, offset);
+ if (r == -1)
+ return -errno;
+ return r;
+}
+
+static int
+callback_write (const char *path, const char *buf, size_t size, off_t offset,
+ struct fuse_file_info *finfo)
+{
+ int r;
+ r = pwrite (finfo->fh, buf, size, offset);
+ if (r == -1)
+ return -errno;
+ return r;
+}
+
+static int
+callback_statfs (const char *path, struct statvfs *st_buf)
+{
+ if (fstatvfs (basefd, st_buf) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_release (const char *path, struct fuse_file_info *finfo)
+{
+ (void) close (finfo->fh);
+ return 0;
+}
+
+static int
+callback_fsync (const char *path, int crap, struct fuse_file_info *finfo)
+{
+ if (fsync (finfo->fh) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_access (const char *path, int mode)
+{
+ path = ENSURE_RELPATH (path);
+
+ /* Apparently at least GNU coreutils rm calls `faccessat(W_OK)`
+ * before trying to do an unlink. So...we'll just lie about
+ * writable access here.
+ */
+ if (faccessat (basefd, path, mode, 0) == -1)
+ return -errno;
+ return 0;
+}
+
+static int
+callback_setxattr (const char *path, const char *name, const char *value,
+ size_t size, int flags)
+{
+ return -ENOTSUP;
+}
+
+static int
+callback_getxattr (const char *path, const char *name, char *value,
+ size_t size)
+{
+ return -ENOTSUP;
+}
+
+/*
+ * List the supported extended attributes.
+ */
+static int
+callback_listxattr (const char *path, char *list, size_t size)
+{
+ return -ENOTSUP;
+
+}
+
+/*
+ * Remove an extended attribute.
+ */
+static int
+callback_removexattr (const char *path, const char *name)
+{
+ return -ENOTSUP;
+
+}
+
+struct fuse_operations callback_oper = {
+ .getattr = callback_getattr,
+ .readlink = callback_readlink,
+ .readdir = callback_readdir,
+ .mknod = callback_mknod,
+ .mkdir = callback_mkdir,
+ .symlink = callback_symlink,
+ .unlink = callback_unlink,
+ .rmdir = callback_rmdir,
+ .rename = callback_rename,
+ .link = callback_link,
+ .chmod = callback_chmod,
+ .chown = callback_chown,
+ .truncate = callback_truncate,
+ .utime = callback_utime,
+ .create = callback_create,
+ .open = callback_open,
+ .read = callback_read,
+ .write = callback_write,
+ .statfs = callback_statfs,
+ .release = callback_release,
+ .fsync = callback_fsync,
+ .access = callback_access,
+
+ /* Extended attributes support for userland interaction */
+ .setxattr = callback_setxattr,
+ .getxattr = callback_getxattr,
+ .listxattr = callback_listxattr,
+ .removexattr = callback_removexattr
+};
+
+enum
+{
+ KEY_HELP,
+ KEY_VERSION,
+};
+
+static void
+usage (const char *progname)
+{
+ fprintf (stdout,
+ "usage: %s basepath mountpoint [options]\n"
+ "\n"
+ " Makes basepath visible at mountpoint such that files are read-only, directories are writable\n"
+ "\n"
+ "general options:\n"
+ " -o opt,[opt...] mount options\n"
+ " -h --help print help\n"
+ "\n", progname);
+}
+
+static int
+rofs_parse_opt (void *data, const char *arg, int key,
+ struct fuse_args *outargs)
+{
+ (void) data;
+
+ switch (key)
+ {
+ case FUSE_OPT_KEY_NONOPT:
+ if (basefd == -1)
+ {
+ basefd = openat (AT_FDCWD, arg, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+ if (basefd == -1)
+ {
+ perror ("openat");
+ exit (1);
+ }
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ case FUSE_OPT_KEY_OPT:
+ return 1;
+ case KEY_HELP:
+ usage (outargs->argv[0]);
+ exit (0);
+ default:
+ fprintf (stderr, "see `%s -h' for usage\n", outargs->argv[0]);
+ exit (1);
+ }
+ return 1;
+}
+
+static struct fuse_opt rofs_opts[] = {
+ FUSE_OPT_KEY ("-h", KEY_HELP),
+ FUSE_OPT_KEY ("--help", KEY_HELP),
+ FUSE_OPT_KEY ("-V", KEY_VERSION),
+ FUSE_OPT_KEY ("--version", KEY_VERSION),
+ FUSE_OPT_END
+};
+
+int
+main (int argc, char *argv[])
+{
+ struct fuse_args args = FUSE_ARGS_INIT (argc, argv);
+ int res;
+
+ res = fuse_opt_parse (&args, &basefd, rofs_opts, rofs_parse_opt);
+ if (res != 0)
+ {
+ fprintf (stderr, "Invalid arguments\n");
+ fprintf (stderr, "see `%s -h' for usage\n", argv[0]);
+ exit (1);
+ }
+ if (basefd == -1)
+ {
+ fprintf (stderr, "Missing basepath\n");
+ fprintf (stderr, "see `%s -h' for usage\n", argv[0]);
+ exit (1);
+ }
+
+ created_devino_hash = g_hash_table_new_full (devino_hash, devino_equal, g_free, NULL);
+
+ fuse_main (args.argc, args.argv, &callback_oper, NULL);
+
+ return 0;
+}
diff --git a/tests/test-rofiles-fuse.sh b/tests/test-rofiles-fuse.sh
new file mode 100755
index 00000000..24ee2648
--- /dev/null
+++ b/tests/test-rofiles-fuse.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+#
+# Copyright (C) 2016 Colin Walters <walters@verbum.org>
+#
+# 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., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -euo pipefail
+
+echo "1..5"
+
+. $(dirname $0)/libtest.sh
+setup_test_repository "bare-user"
+
+mkdir mnt
+
+$OSTREE checkout test2 checkout-test2
+
+rofiles-fuse checkout-test2 mnt
+cleanup_fuse() {
+ fusermount -u ${test_tmpdir}/mnt || true
+}
+trap cleanup_fuse EXIT
+assert_file_has_content mnt/firstfile first
+echo "ok mount"
+
+if cp /dev/null mnt/firstfile 2>err.txt; then
+ assert_not_reached "inplace mutation"
+fi
+assert_file_has_content err.txt "Read-only file system"
+assert_file_has_content mnt/firstfile first
+assert_file_has_content checkout-test2/firstfile first
+
+echo "ok failed inplace mutation"
+
+echo anewfile-for-fuse > mnt/anewfile-for-fuse
+assert_file_has_content mnt/anewfile-for-fuse anewfile-for-fuse
+assert_file_has_content checkout-test2/anewfile-for-fuse anewfile-for-fuse
+
+mkdir mnt/newfusedir
+for i in $(seq 5); do
+ echo ${i}-morenewfuse-${i} > mnt/newfusedir/test-morenewfuse.${i}
+done
+assert_file_has_content checkout-test2/newfusedir/test-morenewfuse.3 3-morenewfuse-3
+
+echo "ok new content"
+
+rm mnt/baz/cow
+assert_not_has_file checkout-test2/baz/cow
+rm mnt/baz/another -rf
+assert_not_has_dir checkout-test2/baz/another
+
+echo "ok deletion"
+
+ostree --repo=repo commit -b test2 -s fromfuse --link-checkout-speedup --tree=dir=checkout-test2
+
+echo "ok commit"
+
+
+
+
+