Logo Search packages:      
Sourcecode: unionfs-fuse version File versions  Download package

unionfs.c

/*
*
* This is offered under a BSD-style license. This means you can use the code for whatever you
* desire in any way you may want but you MUST NOT forget to give me appropriate credits when
* spreading your work which is based on mine. Something like "original implementation by Radek
* Podgorny" should be fine.
*
* License: BSD-style license
* Copyright: Radek Podgorny <radek@podgorny.cz>,
*            Bernd Schubert <bernd-schubert@gmx.de>
*/

#ifdef linux
      // For pread()/pwrite()
      #define _XOPEN_SOURCE 500
#endif

#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/time.h>
#ifdef linux
      #include <sys/vfs.h>
#else
      #include <sys/statvfs.h>
#endif

#ifdef HAVE_SETXATTR
      #include <sys/xattr.h>
#endif

#include "unionfs.h"
#include "opts.h"
#include "stats.h"
#include "debug.h"
#include "findbranch.h"
#include "general.h"

#include "unlink.h"
#include "rmdir.h"
#include "readdir.h"
#include "cow.h"


static struct fuse_opt unionfs_opts[] = {
      FUSE_OPT_KEY("--help", KEY_HELP),
      FUSE_OPT_KEY("--version", KEY_VERSION),
      FUSE_OPT_KEY("-h", KEY_HELP),
      FUSE_OPT_KEY("-V", KEY_VERSION),
      FUSE_OPT_KEY("stats", KEY_STATS),
      FUSE_OPT_KEY("cow", KEY_COW),
      FUSE_OPT_KEY("noinitgroups", KEY_NOINITGROUPS),
      FUSE_OPT_KEY("statfs_omit_ro", KEY_STATFS_OMIT_RO),
      FUSE_OPT_KEY("chroot=%s,", KEY_CHROOT),
      FUSE_OPT_KEY("max_files=%s", KEY_MAX_FILES),
      FUSE_OPT_END
};


static int unionfs_chmod(const char *path, mode_t mode) {
      DBG_IN();

      int i = find_rw_branch_cow(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = chmod(p, mode);
      if (res == -1) return -errno;

      return 0;
}

static int unionfs_chown(const char *path, uid_t uid, gid_t gid) {
      DBG_IN();

      int i = find_rw_branch_cow(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = lchown(p, uid, gid);
      if (res == -1) return -errno;

      return 0;
}

/**
 * unionfs implementation of the create call
 * libfuse will call this to create regular files
 */
static int unionfs_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
      DBG_IN();

      int i = find_rw_branch_cutlast(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      // NOTE: We should do:
      //       Create the file with mode=0 first, otherwise we might create
      //       a file as root + x-bit + suid bit set, which might be used for
      //       security racing!
      int res = open(p, fi->flags, 0);
      if (res == -1) return -errno;

      set_owner(p); // no error check, since creating the file succeeded

      // NOW, that the file has the proper owner we may set the requested mode
      fchmod(res, mode);

      fi->fh = res;
      remove_hidden(path, i);

      return 0;
}


/**
 * flush may be called multiple times for an open file, this must not really
 * close the file. This is important if used on a network filesystem like NFS
 * which flush the data/metadata on close()
 */
static int unionfs_flush(const char *path, struct fuse_file_info *fi) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) return 0;

      int fd = dup(fi->fh);

      if (fd == -1) {
            // What to do now?
            if (fsync(fi->fh) == -1) return -EIO;

            return -errno;
      }

      int res = close(fd);
      if (res == -1) return -errno;

      return 0;
}

/**
 * Just a stub. This method is optional and can safely be left unimplemented
 */
static int unionfs_fsync(const char *path, int isdatasync, struct fuse_file_info *fi) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) return 0;

      int res;
      if (isdatasync) {
#if _POSIX_SYNCHRONIZED_IO + 0 > 0
            res = fdatasync(fi->fh);
#else
            res = fsync(fi->fh);
#endif
      } else {
            res = fsync(fi->fh);
      }

      if (res == -1)  return -errno;

      return 0;
}

static int unionfs_getattr(const char *path, struct stat *stbuf) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) {
            memset(stbuf, 0, sizeof(stbuf));
            stbuf->st_mode = S_IFREG | 0444;
            stbuf->st_nlink = 1;
            stbuf->st_size = STATS_SIZE;
            return 0;
      }

      int i = find_rorw_branch(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = lstat(p, stbuf);
      if (res == -1) return -errno;

      /* This is a workaround for broken gnu find implementations. Actually,
       * n_links is not defined at all for directories by posix. However, it
       * seems to be common for filesystems to set it to one if the actual value
       * is unknown. Since nlink_t is unsigned and since these broken implementations
       * always substract 2 (for . and ..) this will cause an underflow, setting
       * it to max(nlink_t).
       */
      if (S_ISDIR(stbuf->st_mode)) stbuf->st_nlink = 1;

      return 0;
}

/**
 * init method
 * called before first access to the filesystem
 */
static void * unionfs_init(struct fuse_conn_info *conn) {
      // just to prevent the compiler complaining about unused variables
      (void) conn->max_readahead;

      // we only now (from unionfs_init) may go into the chroot, since otherwise
      // fuse_main() will fail to open /dev/fuse and to call mount
      if (uopt.chroot) {
            int res = chroot(uopt.chroot);
            if (res) {
                  usyslog("Chdir to %s failed: %s ! Aborting!\n",
                         uopt.chroot, strerror(errno));
                  exit(1);
            }
      }
      return NULL;
}

static int unionfs_link(const char *from, const char *to) {
      DBG_IN();

      // hardlinks do not work across different filesystems so we need a copy of from first
      int i = find_rw_branch_cow(from);
      if (i == -1) return -errno;

      // FIXME, we actually MUST COW to i
      int j = find_rw_branch_cutlast(to);
      if (j == -1) return -errno;

      char f[PATHLEN_MAX], t[PATHLEN_MAX];
      snprintf(f, PATHLEN_MAX, "%s%s", uopt.branches[i].path, from);
      snprintf(t, PATHLEN_MAX, "%s%s", uopt.branches[j].path, to);

      int res = link(f, t);
      if (res == -1) return -errno;

      // no need for set_owner(), since owner and permissions are copied over by link()

      remove_hidden(to, i); // remove hide file (if any)
      return 0;
}

/**
 * unionfs mkdir() implementation
 *
 * NOTE: Never delete whiteouts directories here, since this will just
 *       make already hidden sub-branches visible again.
 */
static int unionfs_mkdir(const char *path, mode_t mode) {
      DBG_IN();

      int i = find_rw_branch_cutlast(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = mkdir(p, 0);
      if (res == -1) return -errno;

      set_owner(p); // no error check, since creating the file succeeded
        // NOW, that the file has the proper owner we may set the requested mode
        chmod(p, mode);

      return 0;
}

static int unionfs_mknod(const char *path, mode_t mode, dev_t rdev) {
      DBG_IN();

      int i = find_rw_branch_cutlast(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int file_type = mode & S_IFMT;
      int file_perm = mode & (S_PROT_MASK);

      int res = -1;
      if ((file_type) == S_IFREG) {
            // under FreeBSD, only the super-user can create ordinary files using mknod
            // Actually this workaround should not be required any more
            // since we now have the unionfs_create() method
            // So can we remove it?

            usyslog (LOG_INFO, "deprecated mknod workaround, tell the unionfs-fuse authors if you see this!\n");

            res = creat(p, 0);
            if (res > 0 && close(res) == -1) usyslog(LOG_WARNING, "Warning, cannot close file\n");
      } else {
            res = mknod(p, file_type, rdev);
      }

      if (res == -1) return -errno;

      set_owner(p); // no error check, since creating the file succeeded
      // NOW, that the file has the proper owner we may set the requested mode
      chmod(p, file_perm);

      remove_hidden(path, i);

      return 0;
}

static int unionfs_open(const char *path, struct fuse_file_info *fi) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) {
            if ((fi->flags & 3) == O_RDONLY) {
                  // This makes exec() fail
                  //fi->direct_io = 1;
                  return 0;
            }
            return -EACCES;
      }

      int i;
      if (fi->flags & (O_WRONLY | O_RDWR)) {
            i = find_rw_branch_cutlast(path);
      } else {
            i = find_rorw_branch(path);
      }

      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int fd = open(p, fi->flags);
      if (fd == -1) return -errno;

      if (fi->flags & (O_WRONLY | O_RDWR)) {
            // There might have been a hide file, but since we successfully
            // wrote to the real file, a hide file must not exist anymore
            remove_hidden(path, i);
      }

      // This makes exec() fail
      //fi->direct_io = 1;
      fi->fh = (unsigned long)fd;

      return 0;
}

static int unionfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) {
            char out[STATS_SIZE] = "";
            stats_sprint(&stats, out);

            off_t s = size;
            if (offset < (off_t) strlen(out)) {
                  if (s > (off_t) strlen(out) - offset) s = strlen(out)-offset;
                  memcpy(buf, out+offset, s);
            } else {
                  s = 0;
            }

            return s;
      }

      int res = pread(fi->fh, buf, size, offset);

      if (res == -1) return -errno;

      if (uopt.stats_enabled) stats_add_read(&stats, size);

      return res;
}

static int unionfs_readlink(const char *path, char *buf, size_t size) {
      DBG_IN();

      int i = find_rorw_branch(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = readlink(p, buf, size - 1);

      if (res == -1) return -errno;

      buf[res] = '\0';

      return 0;
}

static int unionfs_release(const char *path, struct fuse_file_info *fi) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) return 0;

      int res = close(fi->fh);
      if (res == -1) return -errno;

      return 0;
}

/**
 * unionfs rename function
 * TODO: If we rename a directory on a read-only branch, we need to copy over
 *       all files to the renamed directory on the read-write branch.
 */
static int unionfs_rename(const char *from, const char *to) {
      DBG_IN();

      bool is_dir = false; // is 'from' a file or directory

      int j = find_rw_branch_cutlast(to);
      if (j == -1) return -errno;

      int i = find_rorw_branch(from);
      if (i == -1) return -errno;

      if (!uopt.branches[i].rw) {
            i = find_rw_branch_cow(from);
            if (i == -1) return -errno;
      }

      if (i != j) {
            usyslog(LOG_ERR, "%s: from and to are on different writable branches %d vs %d, which"
                   "is not supported yet.\n", __func__, i, j);
            return -EXDEV;
      }

      char f[PATHLEN_MAX], t[PATHLEN_MAX];
      snprintf(f, PATHLEN_MAX, "%s%s", uopt.branches[i].path, from);
      snprintf(t, PATHLEN_MAX, "%s%s", uopt.branches[i].path, to);

      filetype_t ftype = path_is_dir(f);
      if (ftype == NOT_EXISTING)
            return -ENOENT;
      else if (ftype == IS_DIR)
            is_dir = true;

      int res;
      if (!uopt.branches[i].rw) {
            // since original file is on a read-only branch, we copied the from file to a writable branch,
            // but since we will rename from, we also need to hide the from file on the read-only branch
            if (is_dir)
                  res = hide_dir(from, i);
            else
                  res = hide_file(from, i);
            if (res) return -errno;
      }

      res = rename(f, t);

      if (res == -1) {
            int err = errno; // unlink() might overwrite errno
            // if from was on a read-only branch we copied it, but now rename failed so we need to delete it
            if (!uopt.branches[i].rw) {
                  if (unlink(f))
                        usyslog(LOG_ERR, "%s: cow of %s succeeded, but rename() failed and now "
                               "also unlink()  failed\n", __func__, from);

                  if (remove_hidden(from, i))
                        usyslog(LOG_ERR, "%s: cow of %s succeeded, but rename() failed and now "
                               "also removing the whiteout  failed\n", __func__, from);
            }
            return -err;
      }

      if (uopt.branches[i].rw) {
            // A lower branch still *might* have a file called 'from', we need to delete this.
            // We only need to do this if we have been on a rw-branch, since we created
            // a whiteout for read-only branches anyway.
            if (is_dir)
                  maybe_whiteout(from, i, WHITEOUT_DIR);
            else
                  maybe_whiteout(from, i, WHITEOUT_FILE);
      }

      remove_hidden(to, i); // remove hide file (if any)
      return 0;
}

/**
 * Wrapper function to convert the result of statfs() to statvfs()
 * libfuse uses statvfs, since it conforms to POSIX. Unfortunately,
 * glibc's statvfs parses /proc/mounts, which then results in reading
 * the filesystem itself again - which would result in a deadlock.
 * TODO: BSD/MacOSX
 */
static int statvfs_local(const char *path, struct statvfs *stbuf) {
#ifdef linux
      /* glibc's statvfs walks /proc/mounts and stats entries found there
       * in order to extract their mount flags, which may deadlock if they
       * are mounted under the unionfs. As a result, we have to do this
       * ourselves.
       */
      struct statfs stfs;
      int res = statfs(path, &stfs);
      if (res == -1) return res;

      memset(stbuf, 0, sizeof(*stbuf));
      stbuf->f_bsize = stfs.f_bsize;
      if (stfs.f_frsize)
            stbuf->f_frsize = stfs.f_frsize;
      else
            stbuf->f_frsize = stfs.f_bsize;
      stbuf->f_blocks = stfs.f_blocks;
      stbuf->f_bfree = stfs.f_bfree;
      stbuf->f_bavail = stfs.f_bavail;
      stbuf->f_files = stfs.f_files;
      stbuf->f_ffree = stfs.f_ffree;
      stbuf->f_favail = stfs.f_ffree; /* nobody knows */
      stbuf->f_fsid = stfs.f_fsid.__val[0];

      /* We don't worry about flags, exactly because this would
       * require reading /proc/mounts, and avoiding that and the
       * resulting deadlocks is exactly what we're trying to avoid
       * by doing this rather than using statvfs.
       */
      stbuf->f_flag = 0;
      stbuf->f_namemax = stfs.f_namelen;

      return 0;
#else
      return statvfs(path, stbuf);
#endif
}



/**
 * statvs implementation
 * TODO: fsid: It would be optimal, if we would store a once generated random
 *           fsid. But what if the same branch with the fsid used for different 
 *           unions? Is the present way ok for most cases?
 */
static int unionfs_statfs(const char *path, struct statvfs *stbuf) {
      (void)path;

      DBG_IN();

      int first = 1;

      dev_t devno[uopt.nbranches];

      int i = 0;
      for (i = 0; i < uopt.nbranches; i++) {
            struct statvfs stb;
            int res = statvfs_local(uopt.branches[i].path, &stb);
            if (res == -1) continue;

            struct stat st;
            res = stat(uopt.branches[i].path, &st);
            if (res == -1) continue;
            devno[i] = st.st_dev;

            if (first) {
                  memcpy(stbuf, &stb, sizeof(*stbuf));
                  first = 0;
                  stbuf->f_fsid = stb.f_fsid << 8;
                  continue;
            }

            // Eliminate same devices
            int j = 0;
            for (j = 0; j < i; j ++) {
                  if (st.st_dev == devno[j]) break;
            }

            if (j == i) {
                  // Filesystem can have different block sizes -> normalize to first's block size
                  double ratio = (double)stb.f_bsize / (double)stbuf->f_bsize;

                  if (uopt.branches[i].rw) {
                        stbuf->f_blocks += stb.f_blocks * ratio;
                        stbuf->f_bfree += stb.f_bfree * ratio;
                        stbuf->f_bavail += stb.f_bavail * ratio;

                        stbuf->f_files += stb.f_files;
                        stbuf->f_ffree += stb.f_ffree;
                        stbuf->f_favail += stb.f_favail;
                  } else if (!uopt.statfs_omit_ro) {
                        // omitting the RO branches is not correct regarding
                        // the block counts but it actually fixes the 
                        // percentage of free space. so, let the user decide.
                        stbuf->f_blocks += stb.f_blocks * ratio;
                        stbuf->f_files  += stb.f_files;
                  }

                  if (!stb.f_flag & ST_RDONLY) stbuf->f_flag &= ~ST_RDONLY;
                  if (!stb.f_flag & ST_NOSUID) stbuf->f_flag &= ~ST_NOSUID;

                  if (stb.f_namemax < stbuf->f_namemax) stbuf->f_namemax = stb.f_namemax;

                  // we don't care about overflows, the fsid just should be different
                  // from other fsids
                  stbuf->f_fsid += stb.f_fsid;
            }
      }

      return 0;
}

static int unionfs_symlink(const char *from, const char *to) {
      DBG_IN();

      int i = find_rw_branch_cutlast(to);
      if (i == -1) return -errno;

      char t[PATHLEN_MAX];
      snprintf(t, PATHLEN_MAX, "%s%s", uopt.branches[i].path, to);

      int res = symlink(from, t);
      if (res == -1) return -errno;

      set_owner(t); // no error check, since creating the file succeeded

      remove_hidden(to, i); // remove hide file (if any)
      return 0;
}

static int unionfs_truncate(const char *path, off_t size) {
      DBG_IN();

      int i = find_rw_branch_cow(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = truncate(p, size);

      if (res == -1) return -errno;

      return 0;
}

static int unionfs_utimens(const char *path, const struct timespec ts[2]) {
      DBG_IN();

      if (uopt.stats_enabled && strcmp(path, STATS_FILENAME) == 0) return 0;

      int i = find_rw_branch_cow(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      struct timeval tv[2];
        tv[0].tv_sec  = ts[0].tv_sec;
        tv[0].tv_usec = ts[0].tv_nsec / 1000;
        tv[1].tv_sec  = ts[1].tv_sec;
        tv[1].tv_usec = ts[1].tv_nsec / 1000;

      int res = utimes(p, tv);

      if (res == -1) return -errno;

      return 0;
}

static int unionfs_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
      (void)path;

      DBG_IN();

      int res = pwrite(fi->fh, buf, size, offset);
      if (res == -1) return -errno;

      if (uopt.stats_enabled) stats_add_written(&stats, size);

      return res;
}

#ifdef HAVE_SETXATTR
static int unionfs_getxattr(const char *path, const char *name, char *value, size_t size) {
      DBG_IN();

      int i = find_rorw_branch(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = lgetxattr(p, name, value, size);

      if (res == -1) return -errno;

      return res;
}

static int unionfs_listxattr(const char *path, char *list, size_t size) {
      DBG_IN();

      int i = find_rorw_branch(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = llistxattr(p, list, size);

      if (res == -1) return -errno;

      return res;
}

static int unionfs_removexattr(const char *path, const char *name) {
      DBG_IN();

      int i = find_rw_branch_cow(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = lremovexattr(p, name);

      if (res == -1) return -errno;

      return res;
}

static int unionfs_setxattr(const char *path, const char *name, const char *value, size_t size, int flags) {
      DBG_IN();

      int i = find_rw_branch_cow(path);
      if (i == -1) return -errno;

      char p[PATHLEN_MAX];
      snprintf(p, PATHLEN_MAX, "%s%s", uopt.branches[i].path, path);

      int res = lsetxattr(p, name, value, size, flags);

      if (res == -1) return -errno;

      return res;
}
#endif // HAVE_SETXATTR

static struct fuse_operations unionfs_oper = {
      .chmod      = unionfs_chmod,
      .chown      = unionfs_chown,
      .create     = unionfs_create,
      .flush      = unionfs_flush,
      .fsync      = unionfs_fsync,
      .getattr    = unionfs_getattr,
      .init       = unionfs_init,
      .link       = unionfs_link,
      .mkdir      = unionfs_mkdir,
      .mknod      = unionfs_mknod,
      .open = unionfs_open,
      .read = unionfs_read,
      .readlink   = unionfs_readlink,
      .readdir    = unionfs_readdir,
      .release    = unionfs_release,
      .rename     = unionfs_rename,
      .rmdir      = unionfs_rmdir,
      .statfs     = unionfs_statfs,
      .symlink    = unionfs_symlink,
      .truncate   = unionfs_truncate,
      .unlink     = unionfs_unlink,
      .utimens    = unionfs_utimens,
      .write      = unionfs_write,
#ifdef HAVE_SETXATTR
      .getxattr         = unionfs_getxattr,
      .listxattr        = unionfs_listxattr,
      .removexattr      = unionfs_removexattr,
      .setxattr         = unionfs_setxattr,
#endif
};

int main(int argc, char *argv[]) {
      struct fuse_args args = FUSE_ARGS_INIT(argc, argv);

      int res = debug_init();
      if (res != 0) return res;

      uopt_init();

      if (fuse_opt_parse(&args, NULL, unionfs_opts, unionfs_opt_proc) == -1) return 1;

      if (!uopt.doexit) {
            if (uopt.nbranches == 0) {
                  printf("You need to specify at least one branch!\n");
                  return 1;
            }

            if (uopt.stats_enabled) stats_init(&stats);
      }

      // enable fuse permission checks, we need to set this, even we we are
      // not root, since we don't have our own access() function
      if (fuse_opt_add_arg(&args, "-odefault_permissions")) {
            fprintf(stderr, "Severe failure, can't enable permssion checks, aborting!\n");
            exit(1);
      }
      unionfs_post_opts();

      umask(0);
      res = fuse_main(args.argc, args.argv, &unionfs_oper, NULL);
      return uopt.doexit ? uopt.retval : res;
}

Generated by  Doxygen 1.6.0   Back to index