src/server/plugins/postgresql/vfs.c

changeset 385
a1f4cb076d2f
parent 382
9e2289c77b04
child 387
f5caf41b4db6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/plugins/postgresql/vfs.c	Sat Sep 24 16:26:10 2022 +0200
@@ -0,0 +1,1013 @@
+/*
+ * 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 to fail due to 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(&tparts) - 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