root/share/examples/puffs/pgfs/pgfs_puffs.c
/*      $NetBSD: pgfs_puffs.c,v 1.5 2014/10/18 07:11:07 snj Exp $       */

/*-
 * Copyright (c)2010,2011 YAMAMOTO Takashi,
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * puffs node ops and fs ops.
 */

#include <sys/cdefs.h>
#ifndef lint
__RCSID("$NetBSD: pgfs_puffs.c,v 1.5 2014/10/18 07:11:07 snj Exp $");
#endif /* not lint */

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <puffs.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <util.h>

#include <libpq-fe.h>
#include <libpq/libpq-fs.h>     /* INV_* */

#include "pgfs.h"
#include "pgfs_db.h"
#include "pgfs_subs.h"
#include "pgfs_debug.h"

static fileid_t
cookie_to_fileid(puffs_cookie_t cookie)
{

        return (fileid_t)(uintptr_t)cookie;
}

static puffs_cookie_t
fileid_to_cookie(fileid_t id)
{
        puffs_cookie_t cookie = (puffs_cookie_t)(uintptr_t)id;

        /* XXX not true for 32-bit ports */
        assert(cookie_to_fileid(cookie) == id);
        return cookie;
}

puffs_cookie_t
pgfs_root_cookie(void)
{

        return fileid_to_cookie(PGFS_ROOT_FILEID);
}

int
pgfs_node_getattr(struct puffs_usermount *pu, puffs_cookie_t opc,
    struct vattr *va, const struct puffs_cred *pcr)
{
        struct Xconn *xc;
        struct fileid_lock_handle *lock;
        fileid_t fileid = cookie_to_fileid(opc);
        int error;

        DPRINTF("%llu\n", fileid);
        lock = fileid_lock(fileid, puffs_cc_getcc(pu));
retry:
        xc = begin_readonly(pu, "getattr");
        error = getattr(xc, fileid, va, GETATTR_ALL);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        goto done;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
done:
        fileid_unlock(lock);
        return error;
}

#define PGFS_DIRCOOKIE_DOT      0       /* . entry */
#define PGFS_DIRCOOKIE_DOTDOT   1       /* .. entry */
#define PGFS_DIRCOOKIE_EOD      2       /* end of directory */

int
pgfs_node_readdir(struct puffs_usermount *pu, puffs_cookie_t opc,
    struct dirent *dent, off_t *readoff, size_t *reslen,
    const struct puffs_cred *pcr, int *eofflag, off_t *cookies,
    size_t *ncookies)
{
        fileid_t parent_fileid;
        fileid_t child_fileid;
        uint64_t cookie;
        uint64_t nextcookie;
        uint64_t offset;
        struct Xconn *xc = NULL;
        static const Oid types[] = {
                TEXTOID,        /* name */
                INT8OID,        /* cookie */
                INT8OID,        /* nextcookie */
                INT8OID,        /* child_fileid */
        };
        const char *name;
        char *nametofree = NULL;
        struct fetchstatus s;
        int error;
        bool fetching;
        bool bufferfull;

        parent_fileid = cookie_to_fileid(opc);
        offset = *readoff;
        DPRINTF("%llu %" PRIu64 "\n", parent_fileid, offset);
        *ncookies = 0;
        fetching = false;
next:
        if (offset == PGFS_DIRCOOKIE_DOT) {
                name = ".";
                child_fileid = parent_fileid;
                cookie = offset;
                nextcookie = PGFS_DIRCOOKIE_DOTDOT;
                goto store_and_next;
        }
        if (offset == PGFS_DIRCOOKIE_DOTDOT) {
                if (parent_fileid != PGFS_ROOT_FILEID) {
                        if (xc == NULL) {
                                xc = begin(pu, "readdir1");
                        }
                        error = lookupp(xc, parent_fileid, &child_fileid);
                        if (error != 0) {
                                rollback(xc);
                                return error;
                        }
                } else {
                        child_fileid = parent_fileid;
                }
                name = "..";
                cookie = offset;
                nextcookie = PGFS_DIRCOOKIE_EOD + 1;
                goto store_and_next;
        }
        if (offset == PGFS_DIRCOOKIE_EOD) {
                *eofflag = 1;
                goto done;
        }
        /* offset > PGFS_DIRCOOKIE_EOD; normal entries */
        if (xc == NULL) {
                xc = begin(pu, "readdir2");
        }
        if (!fetching) {
                static struct cmd *c;

                /*
                 * a simpler query like "ORDER BY name OFFSET :offset - 3"
                 * would work well for most of cases.  however, it doesn't for
                 * applications which expect readdir cookies are kept valid
                 * even after unlink of other entries in the directory.
                 * eg. cvs, bonnie++
                 *
                 * 2::int8 == PGFS_DIRCOOKIE_EOD
                 */
                CREATECMD(c,
                        "SELECT name, cookie, "
                        "lead(cookie, 1, 2::int8) OVER (ORDER BY cookie), "
                        "child_fileid "
                        "FROM dirent "
                        "WHERE parent_fileid = $1 "
                        "AND cookie >= $2 "
                        "ORDER BY cookie", INT8OID, INT8OID);
                error = sendcmd(xc, c, parent_fileid, offset);
                if (error != 0) {
                        rollback(xc);
                        return error;
                }
                fetching = true;
                fetchinit(&s, xc);
        }
        /*
         * fetch and process an entry
         */
        error = FETCHNEXT(&s, types, &nametofree, &cookie, &nextcookie,
            &child_fileid);
        if (error == ENOENT) {
                DPRINTF("ENOENT\n");
                if (offset == PGFS_DIRCOOKIE_EOD + 1) {
                        DPRINTF("empty directory\n");
                        *eofflag = 1;
                        goto done;
                }
                fetchdone(&s);
                rollback(xc);
                return EINVAL;
        }
        if (error != 0) {
                DPRINTF("error %d\n", error);
                fetchdone(&s);
                rollback(xc);
                return error;
        }
        if (offset != cookie && offset != PGFS_DIRCOOKIE_EOD + 1) {
                free(nametofree);
                fetchdone(&s);
                rollback(xc);
                return EINVAL;
        }
        name = nametofree;
store_and_next:
        /*
         * store an entry and continue processing unless the result buffer
         * is full.
         */
        bufferfull = !puffs_nextdent(&dent, name, child_fileid, DT_UNKNOWN,
            reslen);
        free(nametofree);
        nametofree = NULL;
        if (bufferfull) {
                *eofflag = 0;
                goto done;
        }
        PUFFS_STORE_DCOOKIE(cookies, ncookies, cookie);
        offset = nextcookie;
        *readoff = offset;
        goto next;
done:
        /*
         * cleanup and update atime of the directory.
         */
        assert(nametofree == NULL);
        if (fetching) {
                fetchdone(&s);
                fetching = false;
        }
        if (xc == NULL) {
retry:
                xc = begin(pu, "readdir3");
        }
        error = update_atime(xc, parent_fileid);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_lookup(struct puffs_usermount *pu, puffs_cookie_t opc,
    struct puffs_newinfo *pni, const struct puffs_cn *pcn)
{
        struct vattr dva;
        struct vattr cva;
        struct puffs_cred * const pcr = pcn->pcn_cred;
        fileid_t parent_fileid;
        const char *name;
        fileid_t child_fileid;
        struct Xconn *xc;
        mode_t access_mode;
        int error;
        int saved_error;

        parent_fileid = cookie_to_fileid(opc);
        name = pcn->pcn_name;
        DPRINTF("%llu %s\n", parent_fileid, name);
        assert(strcmp(name, ".")); /* . is handled by framework */
retry:
        xc = begin_readonly(pu, "lookup");
        error = getattr(xc, parent_fileid, &dva,
            GETATTR_TYPE|GETATTR_MODE|GETATTR_UID|GETATTR_GID);
        if (error != 0) {
                goto got_error;
        }
        access_mode = PUFFS_VEXEC;
        if ((pcn->pcn_flags & NAMEI_ISLASTCN) != 0 &&
            pcn->pcn_nameiop != NAMEI_LOOKUP) {
                access_mode |= PUFFS_VWRITE;
        }
        error = puffs_access(dva.va_type, dva.va_mode, dva.va_uid, dva.va_gid,
            access_mode, pcr);
        if (error != 0) {
                goto commit_and_return;
        }
        if (!strcmp(name, "..")) {
                error = lookupp(xc, parent_fileid, &child_fileid);
                if (error != 0) {
                        goto got_error;
                }
        } else {
                static struct cmd *c;
                static const Oid types[] = { INT8OID, };
                struct fetchstatus s;

                CREATECMD(c, "SELECT child_fileid "
                        "FROM dirent "
                        "WHERE parent_fileid = $1 AND name = $2",
                        INT8OID, TEXTOID);
                error = sendcmd(xc, c, parent_fileid, name);
                if (error != 0) {
                        DPRINTF("sendcmd %d\n", error);
                        goto got_error;
                }
                fetchinit(&s, xc);
                error = FETCHNEXT(&s, types, &child_fileid);
                fetchdone(&s);
                if (error == ENOENT) {
                        goto commit_and_return;
                }
                if (error != 0) {
                        goto got_error;
                }
        }
        error = getattr(xc, child_fileid, &cva, GETATTR_TYPE|GETATTR_SIZE);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        puffs_newinfo_setcookie(pni, fileid_to_cookie(child_fileid));
        puffs_newinfo_setvtype(pni, cva.va_type);
        puffs_newinfo_setsize(pni, cva.va_size);
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
commit_and_return:
        saved_error = error;
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        return saved_error;
}

int
pgfs_node_mkdir(struct puffs_usermount *pu, puffs_cookie_t opc,
    struct puffs_newinfo *pni, const struct puffs_cn *pcn,
    const struct vattr *va)
{
        struct Xconn *xc;
        fileid_t parent_fileid = cookie_to_fileid(opc);
        fileid_t new_fileid;
        struct puffs_cred * const pcr = pcn->pcn_cred;
        uid_t uid;
        gid_t gid;
        int error;

        DPRINTF("%llu %s\n", parent_fileid, pcn->pcn_name);
        if (puffs_cred_getuid(pcr, &uid) == -1 ||
            puffs_cred_getgid(pcr, &gid) == -1) {
                return errno;
        }
retry:
        xc = begin(pu, "mkdir");
        error = mklinkfile(xc, parent_fileid, pcn->pcn_name, VDIR,
            va->va_mode, uid, gid, &new_fileid);
        if (error == 0) {
                error = update_nlink(xc, parent_fileid, 1);
        }
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        puffs_newinfo_setcookie(pni, fileid_to_cookie(new_fileid));
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_create(struct puffs_usermount *pu, puffs_cookie_t opc,
    struct puffs_newinfo *pni, const struct puffs_cn *pcn,
    const struct vattr *va)
{
        struct Xconn *xc;
        fileid_t parent_fileid = cookie_to_fileid(opc);
        fileid_t new_fileid;
        struct puffs_cred * const pcr = pcn->pcn_cred;
        uid_t uid;
        gid_t gid;
        int error;

        DPRINTF("%llu %s\n", parent_fileid, pcn->pcn_name);
        if (puffs_cred_getuid(pcr, &uid) == -1 ||
            puffs_cred_getgid(pcr, &gid) == -1) {
                return errno;
        }
retry:
        xc = begin(pu, "create");
        error = mklinkfile_lo(xc, parent_fileid, pcn->pcn_name, VREG,
            va->va_mode,
            uid, gid, &new_fileid, NULL);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        puffs_newinfo_setcookie(pni, fileid_to_cookie(new_fileid));
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_write(struct puffs_usermount *pu, puffs_cookie_t opc,
    uint8_t *buf, off_t offset, size_t *resid,
    const struct puffs_cred *pcr, int ioflags)
{
        struct Xconn *xc;
        struct fileid_lock_handle *lock;
        fileid_t fileid = cookie_to_fileid(opc);
        size_t resultlen;
        int fd;
        int error;

        if ((ioflags & PUFFS_IO_APPEND) != 0) {
                DPRINTF("%llu append sz %zu\n", fileid, *resid);
        } else {
                DPRINTF("%llu off %" PRIu64 " sz %zu\n", fileid,
                    (uint64_t)offset, *resid);
        }
        lock = fileid_lock(fileid, puffs_cc_getcc(pu));
retry:
        xc = begin(pu, "write");
        error = update_mctime(xc, fileid);
        if (error != 0) {
                goto got_error;
        }
        error = lo_open_by_fileid(xc, fileid, INV_WRITE, &fd);
        if (error != 0) {
                goto got_error;
        }
        if ((ioflags & PUFFS_IO_APPEND) != 0) {
                int32_t off;

                error = my_lo_lseek(xc, fd, 0, SEEK_END, &off);
                if (error != 0) {
                        goto got_error;
                }
                offset = off;
        }
        if (offset < 0) {                       /* negative offset */
                error = EINVAL;
                goto got_error;
        }
        if ((uint64_t)(INT64_MAX - offset) < *resid ||  /* int64 overflow */
            INT_MAX < offset + *resid) {        /* our max filesize */
                error = EFBIG;
                goto got_error;
        }
        if ((ioflags & PUFFS_IO_APPEND) == 0) {
                error = my_lo_lseek(xc, fd, offset, SEEK_SET, NULL);
                if (error != 0) {
                        goto got_error;
                }
        }
        error = my_lo_write(xc, fd, (const char *)buf, *resid, &resultlen);
        if (error != 0) {
                goto got_error;
        }
        assert(*resid >= resultlen);
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        *resid -= resultlen;
        DPRINTF("resid %zu\n", *resid);
        goto done;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
done:
        fileid_unlock(lock);
        return error;
}

int
pgfs_node_read(struct puffs_usermount *pu, puffs_cookie_t opc,
    uint8_t *buf, off_t offset, size_t *resid,
    const struct puffs_cred *pcr, int ioflags)
{
        struct Xconn *xc;
        fileid_t fileid = cookie_to_fileid(opc);
        size_t resultlen;
        int fd;
        int error;

        DPRINTF("%llu off %" PRIu64 " sz %zu\n",
            fileid, (uint64_t)offset, *resid);
retry:
        xc = begin(pu, "read");
        /*
         * try to update atime first as it's prone to conflict with other
         * transactions.  eg. read-ahead requests can conflict each other.
         * we don't want to retry my_lo_read as it's expensive.
         *
         * XXX probably worth to implement noatime mount option.
         */
        error = update_atime(xc, fileid);
        if (error != 0) {
                goto got_error;
        }
        error = lo_open_by_fileid(xc, fileid, INV_READ, &fd);
        if (error != 0) {
                goto got_error;
        }
        error = my_lo_lseek(xc, fd, offset, SEEK_SET, NULL);
        if (error != 0) {
                goto got_error;
        }
        error = my_lo_read(xc, fd, buf, *resid, &resultlen);
        if (error != 0) {
                goto got_error;
        }
        assert(*resid >= resultlen);
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        *resid -= resultlen;
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_link(struct puffs_usermount *pu, puffs_cookie_t dir_opc,
    puffs_cookie_t targ_opc, const struct puffs_cn *pcn)
{
        struct Xconn *xc;
        fileid_t dir_fileid = cookie_to_fileid(dir_opc);
        fileid_t targ_fileid = cookie_to_fileid(targ_opc);
        struct vattr va;
        int error;

        DPRINTF("%llu %llu %s\n", dir_fileid, targ_fileid, pcn->pcn_name);
retry:
        xc = begin(pu, "link");
        error = getattr(xc, targ_fileid, &va, GETATTR_TYPE);
        if (error != 0) {
                goto got_error;
        }
        if (va.va_type == VDIR) {
                error = EPERM;
                goto got_error;
        }
        error = linkfile(xc, dir_fileid, pcn->pcn_name, targ_fileid);
        if (error != 0) {
                goto got_error;
        }
        error = update_ctime(xc, targ_fileid);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_remove(struct puffs_usermount *pu, puffs_cookie_t opc,
    puffs_cookie_t targ, const struct puffs_cn *pcn)
{
        struct Xconn *xc;
        fileid_t fileid = cookie_to_fileid(opc);
        fileid_t targ_fileid = cookie_to_fileid(targ);
        struct vattr va;
        int error;

retry:
        xc = begin(pu, "remove");
        error = getattr(xc, targ_fileid, &va, GETATTR_TYPE);
        if (error != 0) {
                goto got_error;
        }
        if (va.va_type == VDIR) {
                error = EPERM;
                goto got_error;
        }
        error = unlinkfile(xc, fileid, pcn->pcn_name, targ_fileid);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        puffs_setback(puffs_cc_getcc(pu), PUFFS_SETBACK_INACT_N2);
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_rmdir(struct puffs_usermount *pu, puffs_cookie_t opc,
    puffs_cookie_t targ, const struct puffs_cn *pcn)
{
        struct Xconn *xc;
        fileid_t parent_fileid = cookie_to_fileid(opc);
        fileid_t targ_fileid = cookie_to_fileid(targ);
        struct vattr va;
        bool empty;
        int error;

retry:
        xc = begin(pu, "rmdir");
        error = getattr(xc, targ_fileid, &va, GETATTR_TYPE);
        if (error != 0) {
                goto got_error;
        }
        if (va.va_type != VDIR) {
                error = ENOTDIR;
                goto got_error;
        }
        error = isempty(xc, targ_fileid, &empty);
        if (error != 0) {
                goto got_error;
        }
        if (!empty) {
                error = ENOTEMPTY;
                goto got_error;
        }
        error = unlinkfile(xc, parent_fileid, pcn->pcn_name, targ_fileid);
        if (error == 0) {
                error = update_nlink(xc, parent_fileid, -1);
        }
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        puffs_setback(puffs_cc_getcc(pu), PUFFS_SETBACK_INACT_N2);
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_inactive(struct puffs_usermount *pu, puffs_cookie_t opc)
{
        struct Xconn *xc;
        fileid_t fileid = cookie_to_fileid(opc);
        int error;

        /*
         * XXX
         * probably this should be handed to the separate "reaper" context
         * because lo_unlink() can be too expensive to execute synchronously.
         * however, the puffs_cc API doesn't provide a way to create a worker
         * context.
         */

        DPRINTF("%llu\n", fileid);
retry:
        xc = begin(pu, "inactive");
        error = cleanupfile(xc, fileid);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_setattr(struct puffs_usermount *pu, puffs_cookie_t opc,
    const struct vattr *va, const struct puffs_cred *pcr)
{
        struct Xconn *xc;
        struct fileid_lock_handle *lock;
        fileid_t fileid = cookie_to_fileid(opc);
        struct vattr ova;
        unsigned int attrs;
        int error;

        DPRINTF("%llu\n", fileid);
        if (va->va_flags != (u_long)PUFFS_VNOVAL) {
                return EOPNOTSUPP;
        }
        attrs = 0;
        if (va->va_uid != (uid_t)PUFFS_VNOVAL ||
            va->va_gid != (gid_t)PUFFS_VNOVAL) {
                attrs |= GETATTR_UID|GETATTR_GID|GETATTR_MODE;
        }
        if (va->va_mode != (mode_t)PUFFS_VNOVAL) {
                attrs |= GETATTR_TYPE|GETATTR_UID|GETATTR_GID;
        }
        if (va->va_atime.tv_sec != PUFFS_VNOVAL ||
            va->va_mtime.tv_sec != PUFFS_VNOVAL ||
            va->va_ctime.tv_sec != PUFFS_VNOVAL) {
                attrs |= GETATTR_UID|GETATTR_GID|GETATTR_MODE;
        }
        lock = fileid_lock(fileid, puffs_cc_getcc(pu));
retry:
        xc = begin(pu, "setattr");
        error = getattr(xc, fileid, &ova, attrs);
        if (error != 0) {
                goto got_error;
        }
        if (va->va_uid != (uid_t)PUFFS_VNOVAL ||
            va->va_gid != (gid_t)PUFFS_VNOVAL) {
                static struct cmd *c;
                uint64_t newuid =
                    va->va_uid != (uid_t)PUFFS_VNOVAL ? va->va_uid : ova.va_uid;
                uint64_t newgid =
                    va->va_gid != (gid_t)PUFFS_VNOVAL ? va->va_gid : ova.va_gid;

                error = puffs_access_chown(ova.va_uid, ova.va_gid,
                    newuid, newgid, pcr);
                if (error != 0) {
                        goto got_error;
                }
                CREATECMD(c,
                        "UPDATE file "
                        "SET uid = $1, gid = $2 "
                        "WHERE fileid = $3", INT8OID, INT8OID, INT8OID);
                error = simplecmd(xc, c, newuid, newgid, fileid);
                if (error != 0) {
                        goto got_error;
                }
                ova.va_uid = newuid;
                ova.va_gid = newgid;
        }
        if (va->va_mode != (mode_t)PUFFS_VNOVAL) {
                static struct cmd *c;
                uint64_t newmode = va->va_mode;

                error = puffs_access_chmod(ova.va_uid, ova.va_gid, ova.va_type,
                    newmode, pcr);
                if (error != 0) {
                        goto got_error;
                }
                CREATECMD(c,
                        "UPDATE file "
                        "SET mode = $1 "
                        "WHERE fileid = $2", INT8OID, INT8OID);
                error = simplecmd(xc, c, newmode, fileid);
                if (error != 0) {
                        goto got_error;
                }
                ova.va_mode = newmode;
        }
        if (va->va_atime.tv_sec != PUFFS_VNOVAL ||
            va->va_mtime.tv_sec != PUFFS_VNOVAL ||
            va->va_ctime.tv_sec != PUFFS_VNOVAL ||
            va->va_birthtime.tv_sec != PUFFS_VNOVAL) {
                error = puffs_access_times(ova.va_uid, ova.va_gid, ova.va_mode,
                    (va->va_vaflags & VA_UTIMES_NULL) != 0, pcr);
                if (error != 0) {
                        goto got_error;
                }
                if (va->va_atime.tv_sec != PUFFS_VNOVAL) {
                        static struct cmd *c;
                        char *ts;

                        error = timespec_to_pgtimestamp(&va->va_atime, &ts);
                        if (error != 0) {
                                goto got_error;
                        }
                        CREATECMD(c,
                                "UPDATE file "
                                "SET atime = $1 "
                                "WHERE fileid = $2", TIMESTAMPTZOID, INT8OID);
                        error = simplecmd(xc, c, ts, fileid);
                        free(ts);
                        if (error != 0) {
                                goto got_error;
                        }
                }
                if (va->va_mtime.tv_sec != PUFFS_VNOVAL) {
                        static struct cmd *c;
                        char *ts;

                        error = timespec_to_pgtimestamp(&va->va_mtime, &ts);
                        if (error != 0) {
                                goto got_error;
                        }
                        CREATECMD(c,
                                "UPDATE file "
                                "SET mtime = $1 "
                                "WHERE fileid = $2", TIMESTAMPTZOID, INT8OID);
                        error = simplecmd(xc, c, ts, fileid);
                        free(ts);
                        if (error != 0) {
                                goto got_error;
                        }
                }
                if (va->va_ctime.tv_sec != PUFFS_VNOVAL) {
                        static struct cmd *c;
                        char *ts;

                        error = timespec_to_pgtimestamp(&va->va_ctime, &ts);
                        if (error != 0) {
                                goto got_error;
                        }
                        CREATECMD(c,
                                "UPDATE file "
                                "SET ctime = $1 "
                                "WHERE fileid = $2", TIMESTAMPTZOID, INT8OID);
                        error = simplecmd(xc, c, ts, fileid);
                        free(ts);
                        if (error != 0) {
                                goto got_error;
                        }
                }
                if (va->va_birthtime.tv_sec != PUFFS_VNOVAL) {
                        static struct cmd *c;
                        char *ts;

                        error = timespec_to_pgtimestamp(&va->va_birthtime, &ts);
                        if (error != 0) {
                                goto got_error;
                        }
                        CREATECMD(c,
                                "UPDATE file "
                                "SET btime = $1 "
                                "WHERE fileid = $2", TIMESTAMPTZOID, INT8OID);
                        error = simplecmd(xc, c, ts, fileid);
                        free(ts);
                        if (error != 0) {
                                goto got_error;
                        }
                }
        }
        if (va->va_size != (uint64_t)PUFFS_VNOVAL) {
                int fd;

                if (va->va_size > INT_MAX) {
                        error = EFBIG;
                        goto got_error;
                }
                error = lo_open_by_fileid(xc, fileid, INV_READ|INV_WRITE, &fd);
                if (error != 0) {
                        goto got_error;
                }
                error = my_lo_truncate(xc, fd, va->va_size);
                if (error != 0) {
                        goto got_error;
                }
                error = my_lo_close(xc, fd);
                if (error != 0) {
                        goto got_error;
                }
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        goto done;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
done:
        fileid_unlock(lock);
        return error;
}

int
pgfs_node_rename(struct puffs_usermount *pu, puffs_cookie_t src_dir,
    puffs_cookie_t src, const struct puffs_cn *pcn_src,
    puffs_cookie_t targ_dir, puffs_cookie_t targ,
    const struct puffs_cn *pcn_targ)
{
        struct Xconn *xc;
        fileid_t fileid_src_dir = cookie_to_fileid(src_dir);
        fileid_t fileid_src = cookie_to_fileid(src);
        fileid_t fileid_targ_dir = cookie_to_fileid(targ_dir);
        fileid_t fileid_targ = cookie_to_fileid(targ);
        struct vattr va_src;
        struct vattr va_targ;
        int error;

        DPRINTF("%llu %llu %llu %llu\n", fileid_src_dir, fileid_src,
            fileid_targ_dir, fileid_targ);
retry:
        xc = begin(pu, "rename");
        error = getattr(xc, fileid_src, &va_src, GETATTR_TYPE);
        if (error != 0) {
                goto got_error;
        }
        if (va_src.va_type == VDIR) {
                error = check_path(xc, fileid_src, fileid_targ_dir);
                if (error != 0) {
                        goto got_error;
                }
        }
        if (fileid_targ != 0) {
                error = getattr(xc, fileid_targ, &va_targ,
                    GETATTR_TYPE|GETATTR_NLINK);
                if (error != 0) {
                        goto got_error;
                }
                if (va_src.va_type == VDIR) {
                        if (va_targ.va_type != VDIR) {
                                error = ENOTDIR;
                                goto got_error;
                        }
                        if (va_targ.va_nlink != 2) {
                                error = ENOTEMPTY;
                                goto got_error;
                        }
                } else if (va_targ.va_type == VDIR) {
                        error = EISDIR;
                        goto got_error;
                }
                error = unlinkfile(xc, fileid_targ_dir, pcn_targ->pcn_name,
                    fileid_targ);
                if (error == 0 && va_targ.va_type == VDIR) {
                        error = update_nlink(xc, fileid_targ_dir, -1);
                }
                if (error != 0) {
                        goto got_error;
                }
        }
        error = linkfile(xc, fileid_targ_dir, pcn_targ->pcn_name, fileid_src);
        if (error == 0 && va_src.va_type == VDIR) {
                error = update_nlink(xc, fileid_targ_dir, 1);
        }
        if (error != 0) {
                goto got_error;
        }
        /* XXX ctime? */
        error = unlinkfile(xc, fileid_src_dir, pcn_src->pcn_name, fileid_src);
        if (error == 0 && va_src.va_type == VDIR) {
                error = update_nlink(xc, fileid_src_dir, -1);
        }
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_symlink(struct puffs_usermount *pu, puffs_cookie_t opc,
    struct puffs_newinfo *pni, const struct puffs_cn *pcn,
    const struct vattr *va, const char *target)
{
        struct Xconn *xc;
        struct puffs_cred *pcr = pcn->pcn_cred;
        fileid_t parent_fileid = cookie_to_fileid(opc);
        fileid_t new_fileid;
        size_t resultlen;
        size_t targetlen;
        uid_t uid;
        gid_t gid;
        int loid;
        int fd;
        int error;

        DPRINTF("%llu %s %s\n", parent_fileid, pcn->pcn_name, target);
        if (puffs_cred_getuid(pcr, &uid) == -1 ||
            puffs_cred_getgid(pcr, &gid) == -1) {
                return errno;
        }
retry:
        xc = begin(pu, "symlink");
        error = mklinkfile_lo(xc, parent_fileid, pcn->pcn_name, VLNK,
            va->va_mode, uid, gid, &new_fileid, &loid);
        if (error != 0) {
                goto got_error;
        }
        error = my_lo_open(xc, loid, INV_WRITE, &fd);
        if (error != 0) {
                goto got_error;
        }
        targetlen = strlen(target);
        error = my_lo_write(xc, fd, target, targetlen, &resultlen);
        if (error != 0) {
                goto got_error;
        }
        if (resultlen != targetlen) {
                error = ENOSPC; /* XXX */
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        puffs_newinfo_setcookie(pni, fileid_to_cookie(new_fileid));
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_readlink(struct puffs_usermount *pu, puffs_cookie_t opc,
    const struct puffs_cred *pcr, char *buf, size_t *buflenp)
{
        fileid_t fileid = cookie_to_fileid(opc);
        struct Xconn *xc;
        size_t resultlen;
        int fd;
        int error;

        DPRINTF("%llu\n", fileid);
        xc = begin_readonly(pu, "readlink");
        error = lo_open_by_fileid(xc, fileid, INV_READ, &fd);
        if (error != 0) {
                rollback(xc);
                return error;
        }
        error = my_lo_read(xc, fd, buf, *buflenp, &resultlen);
        if (error != 0) {
                rollback(xc);
                return error;
        }
        assert(resultlen <= *buflenp);
        error = commit(xc);
        if (error != 0) {
                return error;
        }
        *buflenp = resultlen;
        return 0;
}

int
pgfs_node_access(struct puffs_usermount *pu, puffs_cookie_t opc,
    int mode, const struct puffs_cred *pcr)
{
        struct Xconn *xc;
        fileid_t fileid = cookie_to_fileid(opc);
        struct vattr va;
        int error;

        DPRINTF("%llu\n", fileid);
retry:
        xc = begin_readonly(pu, "access");
        error = getattr(xc, fileid, &va,
            GETATTR_TYPE|GETATTR_MODE|GETATTR_UID|GETATTR_GID);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        return puffs_access(va.va_type, va.va_mode, va.va_uid, va.va_gid, mode,
            pcr);
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}

int
pgfs_node_fsync(struct puffs_usermount *pu, puffs_cookie_t opc,
    const struct puffs_cred *pcr, int flags, off_t offlo, off_t offhi)
{
        fileid_t fileid = cookie_to_fileid(opc);

        DPRINTF("%llu\n", fileid);
        return flush_xacts(pu);
}

int
pgfs_fs_statvfs(struct puffs_usermount *pu, struct statvfs *sbp)
{
        struct Xconn *xc;
        uint64_t nfiles;
        uint64_t bytes;
        uint64_t lo_bytes;
        static struct cmd *c_nfiles;
        static struct cmd *c_bytes;
        static struct cmd *c_lobytes;
        static const Oid types[] = { INT8OID, };
        struct fetchstatus s;
        int error;

retry:
        xc = begin_readonly(pu, "statvfs");
        /*
         * use an estimate which we can retrieve quickly, instead of
         * "SELECT count(*) from file".
         */
        CREATECMD_NOPARAM(c_nfiles,
                "SELECT reltuples::int8 "
                "FROM pg_class c LEFT JOIN pg_namespace n "
                "ON (n.oid=c.relnamespace) "
                "WHERE n.nspname = 'pgfs' AND c.relname = 'file'");
        CREATECMD_NOPARAM(c_bytes,
                "SELECT sum(pg_total_relation_size(c.oid))::int8 "
                "FROM pg_class c LEFT JOIN pg_namespace n "
                "ON (n.oid=c.relnamespace) "
                "WHERE n.nspname = 'pgfs'");
        /*
         * the following is not correct if someone else is using large objects
         * in the same database.  we don't bother to join with datafork it as
         * it's too expensive for the little benefit.
         */
        CREATECMD_NOPARAM(c_lobytes,
                "SELECT pg_total_relation_size('pg_largeobject')::int8");
        error = sendcmd(xc, c_nfiles);
        if (error != 0) {
                goto got_error;
        }
        fetchinit(&s, xc);
        error = FETCHNEXT(&s, types, &nfiles);
        fetchdone(&s);
        if (error != 0) {
                goto got_error;
        }
        error = sendcmd(xc, c_bytes);
        if (error != 0) {
                goto got_error;
        }
        fetchinit(&s, xc);
        error = FETCHNEXT(&s, types, &bytes);
        fetchdone(&s);
        if (error != 0) {
                goto got_error;
        }
        error = sendcmd(xc, c_lobytes);
        if (error != 0) {
                goto got_error;
        }
        fetchinit(&s, xc);
        error = FETCHNEXT(&s, types, &lo_bytes);
        fetchdone(&s);
        if (error != 0) {
                goto got_error;
        }
        error = commit(xc);
        if (error != 0) {
                goto got_error;
        }
        /*
         * XXX fill f_blocks and f_files with meaningless large values.
         * there are no easy way to provide meaningful values for them
         * esp. with tablespaces.
         */
        sbp->f_bsize = LOBLKSIZE;
        sbp->f_frsize = LOBLKSIZE;
        sbp->f_blocks = INT64_MAX / 100 / sbp->f_frsize;
        sbp->f_bfree = sbp->f_blocks - howmany(bytes + lo_bytes, sbp->f_frsize);
        sbp->f_bavail = sbp->f_bfree;
        sbp->f_bresvd = 0;
        sbp->f_files = INT_MAX;
        sbp->f_ffree = sbp->f_files - nfiles;
        sbp->f_favail = sbp->f_ffree;
        sbp->f_fresvd = 0;
        return 0;
got_error:
        rollback(xc);
        if (error == EAGAIN) {
                goto retry;
        }
        return error;
}