src/server/plugins/postgresql/vfs.c

Tue, 13 Aug 2024 19:59:42 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Tue, 13 Aug 2024 19:59:42 +0200
changeset 545
720893ec7d48
parent 464
0a29110b94ec
permissions
-rw-r--r--

new linux event_send implementation, replace event pipes with eventfd

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2022 Olaf Wintermann. 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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.
 */

#include "vfs.h"
#include "config.h"

#include <inttypes.h>

#include "../../util/util.h"
#include "../../util/pblock.h"

static VFS pg_vfs_class = {
    pg_vfs_open,
    pg_vfs_stat,
    pg_vfs_fstat,
    pg_vfs_opendir,
    pg_vfs_fdopendir,
    pg_vfs_mkdir,
    pg_vfs_unlink,
    pg_vfs_rmdir
};

static VFS_IO pg_vfs_io_class = {
    pg_vfs_io_read,
    pg_vfs_io_write,
    pg_vfs_io_pread,
    pg_vfs_io_pwrite,
    pg_vfs_io_seek,
    pg_vfs_io_close,
    NULL, // no pg aio implementation yet
    NULL,
    pg_vfs_io_getetag
};

static VFS_DIRIO pg_vfs_dirio_class = {
    pg_vfs_dirio_readdir,
    pg_vfs_dirio_close
};


/*
 * SQL Queries
 */

// Resolves a path into resource_id and parent_id
// params: $1: path string
static const char *sql_resolve_path = 
    "with recursive resolvepath as (\n\
        select\n\
            resource_id,\n\
            parent_id,\n\
            '' as fullpath,\n\
            resoid,\n\
            iscollection,\n\
            lastmodified,\n\
            creationdate,\n\
            contentlength,\n\
            etag,\n\
            regexp_split_to_array($1, '/') as pathelm,\n\
            1 as pathdepth\n\
        from Resource\n\
        where resource_id = $2\n\
        union\n\
        select\n\
            r.resource_id,\n\
            r.parent_id,\n\
            p.fullpath || '/' || r.nodename,\n\
            r.resoid,\n\
            r.iscollection,\n\
            r.lastmodified,\n\
            r.creationdate,\n\
            r.contentlength,\n\
            r.etag,\n\
            p.pathelm,\n\
            p.pathdepth + 1\n\
        from Resource r\n\
        inner join resolvepath p on r.parent_id = p.resource_id\n\
        where p.pathelm[p.pathdepth+1] = r.nodename\n\
    )\n\
    select resource_id, parent_id, fullpath, resoid, iscollection, lastmodified, creationdate, contentlength, etag from resolvepath\n\
    where fullpath = $1 ;";

// Same as sql_resolve_path, but it returns the root collection
// params: $1: path string (should be '/')
static const char *sql_get_root = "select resource_id, parent_id, $1 as fullpath, resoid, true as iscollection, lastmodified, creationdate, contentlength, etag from Resource where resource_id = $2;";

// Get all children of a specific collection
// params: $1: parent resource_id
static const char *sql_get_children = "select resource_id, nodename, iscollection, lastmodified, creationdate, contentlength, etag from Resource where parent_id = $1;";

// Get resource
// params: $1: resource_id
static const char *sql_get_resource = "select resource_id, nodename, iscollection, lastmodified, creationdate, contentlength, etag from Resource where resource_id = $1;";

// Create resource
// params: $1: parent_id
//         $2: node name
static const char *sql_create_resource =
        "insert into Resource (parent_id, nodename, iscollection, lastmodified, creationdate, contentlength, resoid) values\n\
        ($1, $2, false, now(), now(), 0, lo_creat(-1))\n\
        returning resource_id, resoid, lastmodified, creationdate;";

// Create collection
// params: $1: parent_id
//         $2: node name
static const char *sql_create_collection =
        "insert into Resource (parent_id, nodename, iscollection, lastmodified, creationdate, contentlength) values\n\
        ($1, $2, true, now(), now(), 0)\n\
        returning resource_id, lastmodified, creationdate;";

// Update resource metadata
// params: $1: resource_id
//         $2: contentlength
static const char *sql_update_resource = "update Resource set contentlength = $2, lastmodified = now(), etag = gen_random_uuid() where resource_id = $1;";

// Delete a resource
// params: $1: resource_id
static const char *sql_delete_res = "delete from Resource where parent_id is not null and resource_id = $1;";


void* pg_vfs_init(ServerConfiguration *cfg, pool_handle_t *pool, WSConfigNode *config) {
    return pg_init_repo(cfg, pool, config);
}

VFS* pg_vfs_create(Session *sn, Request *rq, pblock *pb, void *initData) {
    PgRepository *repo = initData;
    
    char *resource_pool;
    if(repo) {
        resource_pool = repo->resourcepool.ptr;
    } else {
        // resourcepool is required
        resource_pool = pblock_findval("resourcepool", pb);
        if(!resource_pool) {
            log_ereport(LOG_MISCONFIG, "pg_vfs_create: missing resourcepool parameter");
            return NULL;
        }
    }
    
    // get the resource first (most likely failure reason is misconfig)
    ResourceData *resdata = resourcepool_lookup(sn, rq, resource_pool, 0);
    if(!resdata) {
        log_ereport(LOG_MISCONFIG, "postgresql vfs: resource pool %s not found", resource_pool);
        return NULL;
    }
    // resdata will be freed automatically when the request is finished
    
    return pg_vfs_create_from_resourcedata(sn, rq, repo, resdata);
}

VFS* pg_vfs_create_from_resourcedata(Session *sn, Request *rq, PgRepository *repo, ResourceData *resdata) {
    // Create a new VFS object and a separate instance object
    // VFS contains fptrs that can be copied from pg_vfs_class
    // instance contains request specific data (PGconn)
    VFS *vfs = pool_malloc(sn->pool, sizeof(VFS));
    if(!vfs) {
        return NULL;
    }
    
    PgVFS *vfs_priv = pool_malloc(sn->pool, sizeof(PgVFS));
    if(!vfs_priv) {
        pool_free(sn->pool, vfs);
        return NULL;
    }
    vfs_priv->connection = resdata->data;
    vfs_priv->pg_resource = resdata;
    vfs_priv->root_resource_id = repo->root_resource_id;
    snprintf(vfs_priv->root_resource_id_str, 32, "%" PRId64, repo->root_resource_id);
    
    memcpy(vfs, &pg_vfs_class, sizeof(VFS));
    vfs->flags = 0;
    vfs->instance = vfs_priv;
    
    return vfs;
}


int pg_resolve_path(
        PGconn *connection,
        const char *path,
        const char *root_id,
        int64_t *parent_id,
        int64_t *resource_id,
        Oid *oid,
        const char **resource_name,
        WSBool *iscollection,
        struct stat *s,
        char *etag,
        int *res_errno)
{
    // basic path validation
    if(!path) return 1;
    size_t pathlen = strlen(path);
    if(pathlen == 0) return 1;
    if(path[0] != '/') {
        return 1;
    }
    
    char *pathf = NULL;
    if(pathlen > 1 && path[pathlen-1] == '/') {
        pathf = malloc(pathlen);
        memcpy(pathf, path, pathlen);
        pathf[pathlen-1] = 0; // remove trailing '/'
        path = pathf;
    }
    
    // get last node of path
    *resource_name = util_resource_name(path);
    
    const char *sql = pathlen == 1 ? sql_get_root : sql_resolve_path;
    const char* params[2] = { path, root_id };
    PGresult *result = PQexecParams(
            connection,
            sql,
            2,     // number of parameters
            NULL,
            params, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
      
    if(pathf) {
        free(pathf);
    }
    
    if(!result) return 1;
    
    int ret = 1;
    //int nfields = PQnfields(result);
    int nrows = PQntuples(result);
    if(nrows == 1) {
        char *resource_id_str = PQgetvalue(result, 0, 0);
        char *parent_id_str = PQgetvalue(result, 0, 1);
        char *iscol = PQgetvalue(result, 0, 4);
        char *lastmodified = PQgetvalue(result, 0, 5);
        char *creationdate = PQgetvalue(result, 0, 6);
        char *contentlength = PQgetvalue(result, 0, 7);
        char *res_etag = PQgetvalue(result, 0, 8);
        if(resource_id_str && parent_id_str) {
            if(util_strtoint(resource_id_str, resource_id)) {
                ret = 0; // success
            }
            // optionally get parent_id
            util_strtoint(parent_id_str, parent_id);
        }
        
        if(oid) {
            char *resoid = PQgetvalue(result, 0, 3);
            int64_t roid;
            if(resoid && util_strtoint(resoid, &roid)) {
                *oid = roid;
            }
        }
        
        if(iscollection && iscol) {
            *iscollection = iscol[0] == 't' ? TRUE : FALSE;
        }
        
        if(s) {
            pg_set_stat(s, iscol, lastmodified, creationdate, contentlength);
        }
        
        if(etag) {
            size_t etag_len = strlen(res_etag);
            if(etag_len < PG_ETAG_MAXLEN)
            memcpy(etag, res_etag, etag_len+1);
        }
    } else if(res_errno) {
        *res_errno = ENOENT;
    }
    
    PQclear(result);
    
    return ret;
}


void pg_set_stat(
        struct stat *s,
        const char *iscollection,
        const char *lastmodified,
        const char *creationdate,
        const char *contentlength)
{
    memset(s, 0, sizeof(struct stat));
    if(iscollection) {
        WSBool iscol = iscollection[0] == 't' ? TRUE : FALSE;
        if(iscol) {
            s->st_mode |= 0x4000;
        }
    }
    s->st_mtime = pg_convert_timestamp(lastmodified);  
    
    if(contentlength) {
        int64_t len;
        if(util_strtoint(contentlength, &len)) {
            s->st_size = len;
        }
    }
}

static int pg_create_res(
        PgVFS *pg,
        const char *resparentid_str,
        const char *nodename,
        int64_t *new_resource_id,
        Oid *oid,
        const char **resource_name,
        struct stat *s)
{
    const char* params[2] = { resparentid_str, nodename };
    PGresult *result = PQexecParams(
            pg->connection,
            sql_create_resource,
            2,     // number of parameters
            NULL,
            params, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
    
    if(!result) return 1;
    
    int ret = 1;
    if(PQntuples(result) == 1) {
        // sql insert succesful
        ret = 0;
        
        char *id_str = PQgetvalue(result, 0, 0);
        char *oid_str = PQgetvalue(result, 0, 1);
        char *lastmodified = PQgetvalue(result, 0, 2);
        char *creationdate = PQgetvalue(result, 0, 3);
        
        if(new_resource_id) {
            if(!id_str || !util_strtoint(id_str, new_resource_id)) {
                ret = 1; // shouldn't happen
                log_ereport(LOG_FAILURE, "Postgresql VFS: sql_create_resource: Could not convert resource_id to int");
            }
        }
        if(oid) {
            int64_t i;
            if(!oid_str || !util_strtoint(oid_str, &i)) {
                ret = 1; // shouldn't happen
                log_ereport(LOG_FAILURE, "Postgresql VFS: sql_create_resource: Could not convert oid to int");
            } else {
                *oid = i;
            }
        }
        if(resource_name) {
            *resource_name = nodename;
        }
        if(s) {
            pg_set_stat(s, 0, lastmodified, creationdate, NULL);
        }
    }
    
    PQclear(result);
    
    return ret;
}


static int pg_create_col(
        PgVFS *pg,
        const char *resparentid_str,
        const char *nodename,
        int64_t *new_resource_id,
        const char **resource_name,
        struct stat *s)
{
    const char* params[2] = { resparentid_str, nodename };
    PGresult *result = PQexecParams(
            pg->connection,
            sql_create_collection,
            2,     // number of parameters
            NULL,
            params, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
    
    if(!result) return 1;
    
    int ret = 1;
    if(PQntuples(result) == 1) {
        // sql insert succesful
        ret = 0;
        
        char *id_str = PQgetvalue(result, 0, 0);
        char *lastmodified = PQgetvalue(result, 0, 1);
        char *creationdate = PQgetvalue(result, 0, 2);
        
        if(new_resource_id) {
            if(!id_str || !util_strtoint(id_str, new_resource_id)) {
                ret = 1; // shouldn't happen
                log_ereport(LOG_FAILURE, "Postgresql VFS: sql_create_collection: Could not convert resource_id to int");
            }
        }
        if(resource_name) {
            *resource_name = nodename;
        }
        if(s) {
            pg_set_stat(s, 0, lastmodified, creationdate, NULL);
        }
    }
    
    PQclear(result);
    
    return ret;
}

int pg_create_file(
        VFSContext *ctx,
        PgVFS *pg,
        const char *path,
        int64_t *new_resource_id,
        int64_t *res_parent_id,
        Oid *oid,
        const char **resource_name,
        struct stat *s,
        WSBool collection)
{
    char *parent_path = util_parent_path(path);
    if(!parent_path) return 1;
    
    size_t pathlen = strlen(path);
    char *pathf = NULL;
    if(pathlen > 1 && path[pathlen-1] == '/') {
        pathf = malloc(pathlen);
        memcpy(pathf, path, pathlen);
        pathf[pathlen-1] = 0; // remove trailing '/'
        path = pathf;
    }
    
    const char *nodename = util_resource_name(path);
    
    // resolve the parent path
    // if the parent path can't be resolved, we are done
    const char *resname;
    int64_t resource_id, parent_id;
    resource_id = -1;
    parent_id = -1;
    WSBool iscollection;
    Oid unused_oid = 0;
    
    int err = pg_resolve_path(
            pg->connection,
            parent_path,
            pg->root_resource_id_str,
            &parent_id,
            &resource_id,
            &unused_oid,
            &resname,
            &iscollection,
            NULL,
            NULL,
            &ctx->vfs_errno);
    FREE(parent_path);
    if(err) {
        ctx->vfs_errno = ENOENT;
        if(pathf) free(pathf);
        return 1;
    }
    
    // parent path exists, check if it is a collection
    if(!iscollection) {
        if(pathf) free(pathf);
        return 1;
    }
    
    // create new Resource
    char resid_str[32];
    snprintf(resid_str, 32, "%" PRId64, resource_id); // convert parent resource_id to string
    
    int ret;
    if(collection) {
        ret = pg_create_col(pg, resid_str, nodename, new_resource_id, resource_name, s);
    } else {
        ret = pg_create_res(pg, resid_str, nodename, new_resource_id, oid, resource_name, s);
    }
    
    if(pathf) free(pathf);
    
    if(res_parent_id) {
        // resource_id is still the id of the parent from previous pg_resolve_path call
        *res_parent_id = resource_id;
    }
    
    return ret;
}

int pg_remove_res(
        VFSContext *ctx,
        PgVFS *pg,
        int64_t resource_id,
        Oid oid)
{
    // create transaction savepoint
    PGresult *result = PQexec(pg->connection, "savepoint del_res;");
    ExecStatusType execStatus = PQresultStatus(result);
    PQclear(result);
    if(execStatus != PGRES_COMMAND_OK) {
        return 1;
    }
    
    if(oid > 0) {
        if(lo_unlink(pg->connection, oid) != 1) {
            // restore savepoint
            result = PQexec(pg->connection, "rollback to savepoint del_res;");
            PQclear(result);
            return 1; // error
        }
    }
    
    char resid_str[32];
    snprintf(resid_str, 32, "%" PRId64, resource_id);
    
    const char* params[1] = { resid_str };
    result = PQexecParams(
            pg->connection,
            sql_delete_res,
            1,     // number of parameters
            NULL,
            params, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
      
    execStatus = PQresultStatus(result);
    PQclear(result); 
    int ret = 0;
    
    if(execStatus != PGRES_COMMAND_OK) {
        ret = 1;
        // restore savepoint
        result = PQexec(pg->connection, "rollback to savepoint del_res;");
        PQclear(result);
    } else {
        // we don't need the savepoint anymore
        result = PQexec(pg->connection, "release savepoint del_res;");
        PQclear(result);
    }
    
    return ret;
}

int pg_update_resource(PgVFS *pg, int64_t resource_id, int64_t contentlength) {
    char resid_str[32];
    char ctlen_str[32];
    snprintf(resid_str, 32, "%" PRId64, resource_id);
    snprintf(ctlen_str, 32, "%" PRId64, contentlength);
    
    const char* params[2] = { resid_str, ctlen_str };
    PGresult *result = PQexecParams(
            pg->connection,
            sql_update_resource,
            2,     // number of parameters
            NULL,
            params, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
    
    int ret = PQresultStatus(result) == PGRES_COMMAND_OK ? 0 : 1;
    PQclear(result);   
    return ret;
}

/* -------------------------- VFS functions -------------------------- */

SYS_FILE pg_vfs_open(VFSContext *ctx, const char *path, int oflags) {
    VFS *vfs = ctx->vfs;
    PgVFS *pg = vfs->instance;
    
    const char *resname;
    int64_t resource_id, parent_id;
    resource_id = -1;
    parent_id = -1;
    WSBool iscollection;
    struct stat s;
    char etag[PG_ETAG_MAXLEN];
    Oid oid = 0;
    if(pg_resolve_path(pg->connection, path, pg->root_resource_id_str, &parent_id, &resource_id, &oid, &resname, &iscollection, &s, etag, &ctx->vfs_errno)) {
        if((oflags & O_CREAT) == O_CREAT) {
            if(pg_create_file(ctx, pg, path, &resource_id, &parent_id, &oid, &resname, &s, FALSE)) {
                return NULL;
            }
            iscollection = 0;
        } else {
            return NULL;
        }
    }
    
    // store the resource_id in rq->vars
    if(ctx->rq) {
        char *rq_path = pblock_findkeyval(pb_key_path, ctx->rq->vars);
        if(rq_path && !strcmp(rq_path, path)) {
            char *res_id_str = pblock_findval("resource_id", ctx->rq->vars);
            if(!res_id_str) {
                char resource_id_str[32];
                snprintf(resource_id_str, 32, "%" PRId64, resource_id);
                pblock_nvinsert("resource_id",resource_id_str, ctx->rq->vars);
            }
        }
    }
    
    VFSFile *file = pool_malloc(ctx->pool, sizeof(VFSFile));
    if(!file) {
        return NULL;
    }
    PgFile *pgfile = pool_malloc(ctx->pool, sizeof(PgFile));
    if(!pgfile) {
        pool_free(ctx->pool, file);
        return NULL;
    }
    
    int fd = -1;
    if(!iscollection) {
        if (PQstatus(pg->connection) != CONNECTION_OK) {
            fd = -2;
        }
        
        int lo_mode = INV_READ;
        if((oflags & O_RDWR) == O_RDWR) {
            lo_mode = INV_READ|INV_WRITE;
        } else if((oflags & O_WRONLY) == O_WRONLY) {
            lo_mode = INV_WRITE;
        }
        fd = lo_open(pg->connection, oid, lo_mode);
        int err = 0;
        if(fd < 0) {
            err = 1;
        } else if((oflags & O_TRUNC) == O_TRUNC) {
            if(lo_truncate(pg->connection, fd, 0)) {
                lo_close(pg->connection, fd);
                err = 1;
            }
        }
        
        if(err) {
            pool_free(ctx->pool, file);
            pool_free(ctx->pool, pgfile);
            return NULL;
        }
    }
    
    pgfile->iscollection = iscollection;
    pgfile->resource_id = resource_id;
    pgfile->parent_id = parent_id;
    pgfile->oid = oid;
    pgfile->fd = fd;
    pgfile->oflags = oflags;
    pgfile->s = s;
    memcpy(pgfile->etag, etag, PG_ETAG_MAXLEN);
    
    file->ctx = ctx;
    file->io = iscollection ? &pg_vfs_io_class : &pg_vfs_io_class;
    file->fd = -1;
    file->data = pgfile;
    
    return file;
}

int pg_vfs_stat(VFSContext *ctx, const char *path, struct stat *buf) {
    VFS *vfs = ctx->vfs;
    PgVFS *pg = vfs->instance;
    
    int64_t parent_id, resource_id;
    const char *resname;
    WSBool iscollection;
    return pg_resolve_path(pg->connection, path, pg->root_resource_id_str, &parent_id, &resource_id, NULL, &resname, &iscollection, buf, NULL, &ctx->vfs_errno);
}

int pg_vfs_fstat(VFSContext *ctx, SYS_FILE fd, struct stat *buf) {
    PgFile *pgfile = fd->data;
    memcpy(buf, &pgfile->s, sizeof(struct stat));
    return 0;
}

VFS_DIR pg_vfs_opendir(VFSContext *ctx, const char *path) {
    VFSFile *file = pg_vfs_open(ctx, path, O_RDONLY);
    if(!file) return NULL;
    return pg_vfs_fdopendir(ctx, file);
}

VFS_DIR pg_vfs_fdopendir(VFSContext *ctx, SYS_FILE fd) {
    PgFile *pg = fd->data;
    if(!pg->iscollection) {
        ctx->vfs_errno = ENOTDIR;
        return NULL;
    }
    
    VFSDir *dir = pool_malloc(ctx->pool, sizeof(VFSDir));
    if(!dir) {
        fd->io->close(fd);
        ctx->vfs_errno = ENOMEM;
        return NULL;
    }
    
    PgDir *pgdir = pool_malloc(ctx->pool, sizeof(PgDir));
    if(!pgdir) {
        fd->io->close(fd);
        pool_free(ctx->pool, dir);
        ctx->vfs_errno = ENOMEM;
        return NULL;
    }
    memset(pgdir, 0, sizeof(PgDir));
    pgdir->file = fd;
    
    dir->ctx = ctx;
    dir->io = &pg_vfs_dirio_class;
    dir->data = pgdir;
    dir->fd = -1;
    
    return dir;
}

int pg_vfs_mkdir(VFSContext *ctx, const char *path) {
    VFS *vfs = ctx->vfs;
    PgVFS *pg = vfs->instance;
    
    const char *resname;
    int64_t resource_id, parent_id;
    resource_id = -1;
    parent_id = -1;
    WSBool iscollection;
    struct stat s;
    Oid oid = 0;
    if(!pg_resolve_path(pg->connection, path, pg->root_resource_id_str, &parent_id, &resource_id, &oid, &resname, &iscollection, &s, NULL, &ctx->vfs_errno)) {
        ctx->vfs_errno = EEXIST;
        return 1;
    }
    
    if(pg_create_file(ctx, pg, path, NULL, NULL, NULL, NULL, NULL, TRUE)) {
        return 1;
    }
    
    return 0;
}

int pg_vfs_unlink(VFSContext *ctx, const char *path) {
    VFS *vfs = ctx->vfs;
    PgVFS *pg = vfs->instance;
    
    const char *resname;
    int64_t resource_id, parent_id;
    resource_id = -1;
    parent_id = -1;
    WSBool iscollection;
    Oid oid = 0;
    if(pg_resolve_path(pg->connection, path, pg->root_resource_id_str, &parent_id, &resource_id, &oid, &resname, &iscollection, NULL, NULL, &ctx->vfs_errno)) {
        return 1;
    }
    
    if(iscollection) {
        ctx->vfs_errno = EISDIR;
        return 1;
    }
    
    return pg_remove_res(ctx, pg, resource_id, oid);
}

int pg_vfs_rmdir(VFSContext *ctx, const char *path) {
    VFS *vfs = ctx->vfs;
    PgVFS *pg = vfs->instance;
    
    const char *resname;
    int64_t resource_id, parent_id;
    resource_id = -1;
    parent_id = -1;
    WSBool iscollection;
    if(pg_resolve_path(pg->connection, path, pg->root_resource_id_str, &parent_id, &resource_id, NULL, &resname, &iscollection, NULL, NULL, &ctx->vfs_errno)) {
        return 1;
    }
    
    if(!iscollection) {
        ctx->vfs_errno = ENOTDIR;
        return 1;
    }
    
    return pg_remove_res(ctx, pg, resource_id, 0);
}


/* -------------------------- VFS_IO functions -------------------------- */

ssize_t pg_vfs_io_read(SYS_FILE fd, void *buf, size_t nbyte) {
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    if(pg->fd < 0) return-1;
    return lo_read(pgvfs->connection, pg->fd, buf, nbyte);
}

ssize_t pg_vfs_io_write(SYS_FILE fd, const void *buf, size_t nbyte) {
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    if(pg->fd < 0) return-1;
    return lo_write(pgvfs->connection, pg->fd, buf, nbyte);
}

ssize_t pg_vfs_io_pread(SYS_FILE fd, void *buf, size_t nbyte, off_t offset) {
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    if(pg->fd < 0) return-1;
    if(lo_lseek64(pgvfs->connection, pg->fd, offset, SEEK_SET) == -1) {
        return -1;
    }
    return lo_read(pgvfs->connection, pg->fd, buf, nbyte);
}

ssize_t pg_vfs_io_pwrite(SYS_FILE fd, const void *buf, size_t nbyte, off_t offset) {
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    if(pg->fd < 0) return-1;
    if(lo_lseek64(pgvfs->connection, pg->fd, offset, SEEK_SET) == -1) {
        return -1;
    }
    return lo_write(pgvfs->connection, pg->fd, buf, nbyte);
}

off_t pg_vfs_io_seek(SYS_FILE fd, off_t offset, int whence) {
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    if(pg->fd < 0) return-1;
    return lo_lseek64(pgvfs->connection, pg->fd, offset, whence);
}

off_t pg_vfs_io_tell(SYS_FILE fd) {
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    if(pg->fd < 0) return-1;
    return lo_tell64(pgvfs->connection, pg->fd);
}

void pg_vfs_io_close(SYS_FILE fd) {
    pool_handle_t *pool = fd->ctx->pool;
    PgVFS *pgvfs = fd->ctx->vfs->instance;
    PgFile *pg = fd->data;
    
    if(pg->fd >= 0) {
        if((pg->oflags & (O_WRONLY|O_RDWR)) > 0) {
            // file modified, update length and lastmodified
            off_t len = pg_vfs_io_seek(fd, 0, SEEK_END);
            if(len < 0) len = 0;
            
            pg_update_resource(pgvfs, pg->resource_id, len);
        }
        
        PgVFS *pgvfs = fd->ctx->vfs->instance;
        lo_close(pgvfs->connection, pg->fd);
    }
    
    pool_free(pool, pg);
    pool_free(pool, fd);
}

const char *pg_vfs_io_getetag(SYS_FILE fd) {
    PgFile *pg = fd->data;
    return pg->etag;
}

/* -------------------------- VFS_DIRIO functions -------------------------- */

static int load_dir(VFSDir *dir, PgDir *pg) {
    VFS *vfs = dir->ctx->vfs;
    PgVFS *pgvfs = vfs->instance;
    PgFile *pgfd = pg->file->data;
    PgDir *pgdir = dir->data;
    
    char resid_param[32];
    snprintf(resid_param, 32, "%" PRId64, pgfd->resource_id);
    
    const char *param = resid_param;
    
    PGresult *result = PQexecParams(
            pgvfs->connection,
            sql_get_children,
            1,                                  // number of parameters
            NULL,
            &param,                             // param: parent resource_id 
            NULL,
            NULL,
            0);                                 // 0: result in text format
    if(!result) return 1;
    
    pgdir->result = result;
    pgdir->nrows = PQntuples(result);
    return 0;
}

int pg_vfs_dirio_readdir(VFS_DIR dir, VFS_ENTRY *entry, int getstat) {
    PgDir *pg = dir->data;
    if(!pg->result) {
        if(load_dir(dir, pg)) {
            return 0;
        }
    }
    
    if(pg->row >= pg->nrows) {
        return 0; // EOF
    }
    
    entry->name = PQgetvalue(pg->result, pg->row, 1);
    entry->stat_errno = 0;
    entry->stat_extra = NULL;
        
    if(getstat) {
        memset(&entry->stat, 0, sizeof(struct stat));
        
        char *iscollection = PQgetvalue(pg->result, pg->row, 2);
        char *lastmodified = PQgetvalue(pg->result, pg->row, 3);
        char *creationdate = PQgetvalue(pg->result, pg->row, 4);
        char *contentlength = PQgetvalue(pg->result, pg->row, 5);
        pg_set_stat(&entry->stat, iscollection, lastmodified, creationdate, contentlength);
    }

    pg->row++;
    return 1;
}

void pg_vfs_dirio_close(VFS_DIR dir) {
    pool_handle_t *pool = dir->ctx->pool;
    PgDir *pg = dir->data;
    if(pg->result) {
        PQclear(pg->result);
    }
    PgFile *pgfile = pg->file->data;
    
    pool_free(pool, pgfile);
    pool_free(pool, pg->file);
    pool_free(pool, pg);
    pool_free(pool, dir);
}

time_t pg_convert_timestamp(const char *timestamp) {
    struct tm tm;
    if(pg_convert_timestamp_tm(timestamp, &tm)) {
        return 0;
    }
#ifdef __FreeBSD__
    return timelocal(&tm);
#else
    return mktime(&tm) - timezone;
#endif
}

int pg_convert_timestamp_tm(const char *timestamp, struct tm *tm) {
    // TODO: this is a very basic implementation that needs some work
    // format yyyy-mm-dd HH:MM:SS
    
    memset(tm, 0, sizeof(struct tm));
    size_t len = timestamp ? strlen(timestamp) : 0;
    if(len < 19) {
        return 1;
    } else if(len > 63) {
        return 1;
    }
    
    char buf[64];
    memcpy(buf, timestamp, len);
    
    char *year_str = buf;
    year_str[4] = '\0';
    
    char *month_str = buf + 5;
    month_str[2] = '\0';
    
    char *day_str = buf + 8;
    day_str[2] = '\0';
    
    char *hour_str = buf + 11;
    hour_str[2] = '\0';
    
    char *minute_str = buf + 14;
    minute_str[2] = '\0';
    
    char *second_str = buf + 17;
    second_str[2] = '\0';
    
    tm->tm_year = atoi(year_str) - 1900;
    tm->tm_mon = atoi(month_str) - 1;
    tm->tm_mday = atoi(day_str);
    tm->tm_hour = atoi(hour_str);
    tm->tm_min = atoi(minute_str);
    tm->tm_sec = atoi(second_str);
    
    return 0;
}

mercurial