# HG changeset patch # User Olaf Wintermann # Date 1651344278 -7200 # Node ID 60870dbac94f4e9202bb9eede1fc60d7e01d468f # Parent 09676b559091cac9e862af052ee4df165a9d8dc6 add basic pg proppatch implementation diff -r 09676b559091 -r 60870dbac94f doc/development/postgresql_vfs.sql --- a/doc/development/postgresql_vfs.sql Tue Apr 26 18:21:25 2022 +0200 +++ b/doc/development/postgresql_vfs.sql Sat Apr 30 20:44:38 2022 +0200 @@ -22,7 +22,9 @@ pname text not null, lang text, nsdeflist text, - pvalue text + pvalue text, + + unique(resource_id, xmlns, pname) ); create type property_name as ( diff -r 09676b559091 -r 60870dbac94f src/server/plugins/postgresql/pgtest.c --- a/src/server/plugins/postgresql/pgtest.c Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/plugins/postgresql/pgtest.c Sat Apr 30 20:44:38 2022 +0200 @@ -94,6 +94,7 @@ ucx_test_register(suite, test_pg_prepare_tests); ucx_test_register(suite, test_pg_webdav_propfind); ucx_test_register(suite, test_pg_webdav_propfind_allprop); + ucx_test_register(suite, test_pg_webdav_proppatch_set); PGresult *result = PQexec(test_connection, "BEGIN"); PQclear(result); @@ -204,6 +205,7 @@ void test_multistatus_destroy(TestMultistatus *ms) { + if(!ms) return; xmlFreeDoc(ms->doc); ucx_mempool_destroy(ms->mp); } @@ -481,6 +483,7 @@ UCX_TEST_BEGIN; vfs_mkdir(vfs, "/propfind"); + vfs_mkdir(vfs, "/proppatch"); SYS_FILE f1; int64_t res1_id, res2_id; @@ -506,6 +509,10 @@ UCX_TEST_ASSERT(f1, "res4 create failed"); vfs_close(f1); + f1 = vfs_open(vfs, "/proppatch/pp1", O_WRONLY|O_CREAT); + UCX_TEST_ASSERT(f1, "pp1 create failed"); + vfs_close(f1); + // 2 properties for res1 char idstr[32]; snprintf(idstr, 32, "%" PRId64, res1_id); @@ -801,3 +808,42 @@ UCX_TEST_END; } +UCX_TEST(test_pg_webdav_proppatch_set) { + Session *sn; + Request *rq; + TestIOStream *st; + pblock *pb; + + UCX_TEST_BEGIN; + + // test data: + // + // /propfind/ + // /propfind/res1 (2 properties: test, prop2) + // /propfind/res2 (1 property: test) + // /propfind/res3 + // /propfind/sub + // /propfind/sub/res4 + + int ret; + TestResponse *r1; + TestProperty *p; + // Test 1 + init_test_webdav_method(&sn, &rq, &st, &pb, "PROPPATCH", "/proppatch/pp1", PG_TEST_PROPPATCH1); + rq->davCollection = create_test_pgdav(sn, rq); + + ret = webdav_proppatch(pb, sn, rq); + UCX_TEST_ASSERT(ret == REQ_PROCEED, "proppatch1 failed"); + + printf("\n\n%.*s\n", (int)st->buf->size, st->buf->space); + + TestMultistatus *ms = test_parse_multistatus(st->buf->space, st->buf->size); + UCX_TEST_ASSERT(ms, "proppatch1 response is not valid xml"); + + testutil_destroy_session(sn); + test_multistatus_destroy(ms); + testutil_iostream_destroy(st); + + + UCX_TEST_END; +} diff -r 09676b559091 -r 60870dbac94f src/server/plugins/postgresql/pgtest.h --- a/src/server/plugins/postgresql/pgtest.h Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/plugins/postgresql/pgtest.h Sat Apr 30 20:44:38 2022 +0200 @@ -59,6 +59,7 @@ UCX_TEST(test_pg_prepare_tests); UCX_TEST(test_pg_webdav_propfind); UCX_TEST(test_pg_webdav_propfind_allprop); +UCX_TEST(test_pg_webdav_proppatch_set); /* --------------------------- PROPFIND --------------------------- */ @@ -96,6 +97,12 @@ \ " +#define PG_TEST_PROPPATCH1 " \ + \ + \ + testname \ + \ + " #ifdef __cplusplus } diff -r 09676b559091 -r 60870dbac94f src/server/plugins/postgresql/webdav.c --- a/src/server/plugins/postgresql/webdav.c Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/plugins/postgresql/webdav.c Sat Apr 30 20:44:38 2022 +0200 @@ -30,10 +30,12 @@ #include "vfs.h" #include "../../util/util.h" +#include "../../util/pblock.h" #include #include + static WebdavBackend pg_webdav_backend = { pg_dav_propfind_init, pg_dav_propfind_do, @@ -237,6 +239,28 @@ ) p on r.resource_id = p.resource_id\n\ order by replace(ppath, '/', chr(1)), resource_id;"; +// proppatch: set property +// params: $1: resource_id +// $2: xmlns prefix +// $3: xmlns href +// $4: property name +// $5: lang attribute value +// $6: namespace list string +// $7: property value +static const char *sql_proppatch_set = "\ +insert into Property(resource_id, prefix, xmlns, pname, lang, nsdeflist, pvalue)\n\ +values($1, $2, $3, $4, $5, $6, $7)\n\ +on conflict (resource_id, xmlns, pname) do\n\ +update set prefix=$2, lang=$5, nsdeflist=$6, pvalue=$7;"; + +// proppatch: remove property +// params: $1: resource_id +// $2: xmlns href +// $3: property name +static const char *sql_proppatch_remove = "\ +delete from Property where resource_id = $1 and xmlns = $2 and pname = $3"; + + WebdavBackend* pg_webdav_create(Session *sn, Request *rq, pblock *pb) { // resourcepool is required char *resource_pool = pblock_findval("resourcepool", pb); @@ -561,7 +585,7 @@ property->namespace = namespace; if(!lang_isnull) { - property->lang = (xmlChar*)pool_strdup(pool, lang); + property->lang = pool_strdup(pool, lang); } if(!pvalue_isnull) { @@ -595,6 +619,184 @@ return 0; } +enum PgDavProp { + PG_DAV_PROPPATCH_NOT_ALLOWED = 0, + PG_DAV_CREATIONDATE, + PG_DAV_DISPLAYNAME, + PG_DAV_DEADPROP +}; +/* + * checks if the property can be manipulated + */ +static enum PgDavProp proppatch_check_dav_prop(const char *name) { + if(!strcmp(name, "getlastmodified")) { + return PG_DAV_PROPPATCH_NOT_ALLOWED; + } else if(!strcmp(name, "getcontentlength")) { + return PG_DAV_PROPPATCH_NOT_ALLOWED; + } else if(!strcmp(name, "resourcetype")) { + return PG_DAV_PROPPATCH_NOT_ALLOWED; + } else if(!strcmp(name, "getetag")) { + return PG_DAV_PROPPATCH_NOT_ALLOWED; + } else if(!strcmp(name, "creationdate")) { + return PG_DAV_CREATIONDATE; + } else if(!strcmp(name, "displayname")) { + return PG_DAV_DISPLAYNAME; + } + return PG_DAV_DEADPROP; +} + +typedef struct { + WebdavProperty *creationdate; + WebdavProperty *displayname; + int error; +} PgProppatchOpResult; + +typedef int(*pg_proppatch_func)(PgWebdavBackend*, WebdavProppatchRequest*, WebdavResource*, WebdavProperty*, void*); + +/* + * This function iterates the property list 'plist', + * analyses if any DAV: property is in the list + * and calls opfunc for the each property + * + * If the property list contains the properties creationdate or displayname, + * the pointers to these properties will be stored in the result structure + */ +static PgProppatchOpResult pg_proppatch_op( + PgWebdavBackend *pgdav, + WebdavProppatchRequest *request, + WebdavResource *response, + WebdavPList **plist, + enum PgDavProp forbidden_extra, + pg_proppatch_func opfunc, + void *op_userdata) +{ + PgProppatchOpResult result; + result.creationdate = NULL; + result.displayname = NULL; + result.error = 0; + + WebdavPListIterator i = webdav_plist_iterator(plist); + WebdavPList *cur; + while(webdav_plist_iterator_next(&i, &cur)) { + WebdavProperty *property = cur->property; + WSNamespace *ns = property->namespace; + if(!ns) { + continue; // maybe we should abort + } + + // check if the property is a DAV: property that requires special + // handling + // get* properties can't be manipulated + // some properties can't be removed + if(!strcmp((const char*)ns->href, "DAV:")) { + const char *name = property->name; + enum PgDavProp davprop = proppatch_check_dav_prop(name); + if(davprop != PG_DAV_DEADPROP) { + if(davprop == PG_DAV_PROPPATCH_NOT_ALLOWED || davprop == forbidden_extra) { + response->addproperty(response, property, 409); + } else if(davprop == PG_DAV_CREATIONDATE) { + result.creationdate = property; + } else if(davprop == PG_DAV_DISPLAYNAME) { + result.displayname = property; + } + webdav_plist_iterator_remove_current(&i); + continue; + } + } + + // call op func (set, remove specific code) + if(opfunc(pgdav, request, response, property, op_userdata)) { + result.error = 1; + break; + } + + webdav_plist_iterator_remove_current(&i); + } + + return result; +} + +static int pg_dav_set_property( + PgWebdavBackend *pgdav, + WebdavProppatchRequest *request, + WebdavResource *response, + WebdavProperty *property, + void *userdata) +{ + pool_handle_t *pool = request->sn->pool; + WSNamespace *ns = property->namespace; + char *resource_id_str = userdata; + int ret = 0; + + // convert the property value to WSXmlData + // property->vtype == WS_VALUE_XML_NODE should always be true + WSXmlData *property_value = property->vtype == WS_VALUE_XML_NODE ? wsxml_node2data(pool, property->value.node) : NULL; + char *value_str = property_value ? property_value->data : NULL; + + // TODO: convert WebdavNSList to a string + + // exec sql + const char* params[7] = { resource_id_str, (const char*)ns->prefix, (const char*)ns->href, property->name, NULL, NULL, value_str}; + PGresult *result = PQexecParams( + pgdav->connection, + sql_proppatch_set, + 7, // number of parameters + NULL, + params, // parameter value + NULL, + NULL, + 0); // 0: result in text format + + if(PQresultStatus(result) != PGRES_COMMAND_OK) { + response->addproperty(response, property, 500); + //printf(PQerrorMessage(pgdav->connection)); + //fflush(stdout); + ret = 1; + } else { + response->addproperty(response, property, 200); + } + PQclear(result); + if(value_str) pool_free(pool, value_str); + + return ret; +} + + +static int pg_dav_remove_property( + PgWebdavBackend *pgdav, + WebdavProppatchRequest *request, + WebdavResource *response, + WebdavProperty *property, + void *userdata) +{ + pool_handle_t *pool = request->sn->pool; + WSNamespace *ns = property->namespace; + char *resource_id_str = userdata; + int ret = 0; + + // exec sql + const char* params[3] = { resource_id_str, (const char*)ns->href, property->name }; + PGresult *result = PQexecParams( + pgdav->connection, + sql_proppatch_remove, + 3, // number of parameters + NULL, + params, // parameter value + NULL, + NULL, + 0); // 0: result in text format + + if(PQresultStatus(result) != PGRES_COMMAND_OK) { + response->addproperty(response, property, 500); + //printf(PQerrorMessage(pgdav->connection)); + //fflush(stdout); + ret = 1; + } + PQclear(result); + + return ret; +} + int pg_dav_proppatch_do( WebdavProppatchRequest *request, WebdavResource *response, @@ -602,7 +804,70 @@ WebdavPList **out_set, WebdavPList **out_remove) { - return 1; + PgWebdavBackend *pgdav = request->dav->instance; + pool_handle_t *pool = request->sn->pool; + char *path = pblock_findkeyval(pb_key_path, request->rq->vars); + + // check if the resource exists, we also need the resource_id + int64_t parent_id; + int64_t resource_id; + const char *resourcename; + WSBool iscollection; + int res_errno = 0; + int err = pg_resolve_path( + pgdav->connection, + path, + &parent_id, + &resource_id, + NULL, // OID + &resourcename, + &iscollection, + NULL, // stat + &res_errno); + + if(err) { + return 1; + } + + // because proppatch must be atomic and we have multiple sql + // queries and other backends that do stuff that could fail + // we need the possibility to reverse all changes + // we use a transaction savepoint for this + PGresult *result = PQexec(pgdav->connection, "savepoint proppatch;"); + ExecStatusType execStatus = PQresultStatus(result); + PQclear(result); + if(execStatus != PGRES_COMMAND_OK) { + return 1; + } + + char resource_id_str[32]; + snprintf(resource_id_str, 32, "%" PRId64, resource_id); + + int ret = 0; + PgProppatchOpResult set_res = pg_proppatch_op( + pgdav, + request, + response, + out_set, + PG_DAV_PROPPATCH_NOT_ALLOWED, + pg_dav_set_property, + resource_id_str); + if(set_res.error) { + return 1; + } + PgProppatchOpResult rm_res = pg_proppatch_op( + pgdav, + request, + response, + out_remove, + PG_DAV_CREATIONDATE, // creationdate can't be removed + pg_dav_remove_property, + resource_id_str); + if(rm_res.error) { + return 1; + } + + return ret; } int pg_dav_proppatch_finish( @@ -611,5 +876,15 @@ VFSFile *file, WSBool commit) { - return 1; + PgWebdavBackend *pgdav = request->dav->instance; + int ret = 0; + if(!commit) { + PGresult *result = PQexec(pgdav->connection, "rollback to savepoint proppatch;"); + if(PQresultStatus(result) != PGRES_COMMAND_OK) { + log_ereport(LOG_FAILURE, "pg_dav_proppatch_finish: rollback failed: %s", PQerrorMessage(pgdav->connection)); + ret = 1; + } + PQclear(result); + } + return ret; } diff -r 09676b559091 -r 60870dbac94f src/server/public/webdav.h --- a/src/server/public/webdav.h Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/public/webdav.h Sat Apr 30 20:44:38 2022 +0200 @@ -476,6 +476,10 @@ WSXmlNode *node, int *error); +WSXmlData* wsxml_node2data( + pool_handle_t *pool, + WSXmlNode *node); + #ifdef __cplusplus } #endif diff -r 09676b559091 -r 60870dbac94f src/server/util/writer.c --- a/src/server/util/writer.c Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/util/writer.c Sat Apr 30 20:44:38 2022 +0200 @@ -34,6 +34,16 @@ void writer_init(Writer *w, SYS_NETFD fd, char *buf, size_t len) { w->fd = fd; + w->write = (wr_writefunc)net_write; + w->buffer = buf; + w->size = len; + w->pos = 0; + w->error = 0; +} + +void writer_init_with_stream(Writer *w, void *stream, wr_writefunc writefunc, char *buf, size_t len) { + w->fd = stream; + w->write = writefunc; w->buffer = buf; w->size = len; w->pos = 0; @@ -49,7 +59,7 @@ size_t len = w->pos; while(len > 0) { - ssize_t r = net_write(w->fd, w->buffer + pos, len); + ssize_t r = w->write(w->fd, w->buffer + pos, len); if(r <= 0) { break; } @@ -105,3 +115,8 @@ w->buffer[w->pos++] = c; return 0; } + +int writer_fwrite(const void *s, size_t size, size_t nelem, Writer *out) { + int w = writer_put(out, s, size*nelem); + return w/size; +} diff -r 09676b559091 -r 60870dbac94f src/server/util/writer.h --- a/src/server/util/writer.h Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/util/writer.h Sat Apr 30 20:44:38 2022 +0200 @@ -36,16 +36,23 @@ extern "C" { #endif +typedef ssize_t (*wr_writefunc)(void *, const char *, size_t); + typedef struct Writer { - SYS_NETFD fd; + void *fd; + wr_writefunc write; char *buffer; size_t size; size_t pos; int error; } Writer; + + void writer_init(Writer *w, SYS_NETFD fd, char *buf, size_t len); +void writer_init_with_stream(Writer *w, void *stream, wr_writefunc writefunc, char *buf, size_t len); + int writer_flush(Writer *w); int writer_put(Writer *w, const char *s, size_t len); @@ -54,6 +61,8 @@ int writer_putc(Writer *w, char c); +int writer_fwrite(const void *s, size_t size, size_t nelem, Writer *w); + #ifdef __cplusplus } #endif diff -r 09676b559091 -r 60870dbac94f src/server/webdav/webdav.c --- a/src/server/webdav/webdav.c Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/webdav/webdav.c Sat Apr 30 20:44:38 2022 +0200 @@ -1126,7 +1126,7 @@ WSNamespace *ns = cur->property->namespace; if(ns && !strcmp((const char*)ns->href, "DAV:")) { const char *name = cur->property->name; - WSBool remove_prop = TRUE; + WSBool remove_prop = removefromlist; if(!strcmp(name, "getlastmodified")) { ret.getlastmodified = 1; } else if(!strcmp(name, "getcontentlength")) { diff -r 09676b559091 -r 60870dbac94f src/server/webdav/xml.c --- a/src/server/webdav/xml.c Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/webdav/xml.c Sat Apr 30 20:44:38 2022 +0200 @@ -32,6 +32,7 @@ #include #include +#include #include "../util/util.h" @@ -313,6 +314,49 @@ } +static ssize_t buf_writefunc(void *buf, const void *s, size_t len) { + int w = ucx_buffer_write(s, 1, len, buf); + return w == 0 ? IO_ERROR : w; +} + +WSXmlData* wsxml_node2data( + pool_handle_t *pool, + WSXmlNode *node) +{ + UcxBuffer *buf = ucx_buffer_new(NULL, 1024, UCX_BUFFER_AUTOEXTEND); + if(!buf) { + return NULL; + } + + int error = 0; + WebdavNSList *nslist = wsxml_get_required_namespaces(pool, node, &error); + if(error) { + return NULL; + } + + Writer writer; + char buffer[512]; + writer_init_with_stream(&writer, buf, buf_writefunc, buffer, 512); + + WSXmlData *data = NULL; + if(!wsxml_write_nodes_without_nsdef(pool, &writer, node) && !writer_flush(&writer)) { + data = pool_malloc(pool, sizeof(WSXmlData)); + if(data) { + data->data = pool_malloc(pool, buf->size + 1); + if(data->data) { + memcpy(data->data, buf->space, buf->size); + data->data[buf->size] = '\0'; + data->length = buf->size; + data->namespaces = nslist; + } + } + } + + ucx_buffer_free(buf); + + return data; +} + /***************************************************************************** * Non public functions *****************************************************************************/ diff -r 09676b559091 -r 60870dbac94f src/server/webdav/xml.h --- a/src/server/webdav/xml.h Tue Apr 26 18:21:25 2022 +0200 +++ b/src/server/webdav/xml.h Sat Apr 30 20:44:38 2022 +0200 @@ -33,6 +33,7 @@ #include #include +#include #include "../util/writer.h"