/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* $Rev$ $Date$ */ #ifndef tuscany_tinycdb_hpp #define tuscany_tinycdb_hpp #include #include #include #include #include "string.hpp" #include "list.hpp" #include "value.hpp" #include "monad.hpp" #include "../../modules/scheme/eval.hpp" namespace tuscany { namespace tinycdb { /** * A reallocatable buffer. */ class buffer { public: operator void*() const throw() { return buf; } operator unsigned char*() const throw() { return (unsigned char*)buf; } operator char*() const throw() { return (char*)buf; } private: buffer(const unsigned int size, void* buf) : size(size), buf(buf) { } unsigned int size; void* buf; friend const buffer mkbuffer(const unsigned int sz); friend const buffer mkbuffer(const buffer& b, const unsigned int newsz); friend const bool free(const buffer& b); }; /** * Make a new buffer. */ const buffer mkbuffer(const unsigned int sz) { return buffer(sz, malloc(sz)); } /** * Make a new buffer by reallocating an existing one. */ const buffer mkbuffer(const buffer& b, const unsigned int sz) { if (sz <= b.size) return b; return buffer(sz, realloc(b.buf, sz)); } /** * Free a buffer. */ const bool free(const buffer&b) { ::free(b.buf); return true; } /** * Convert a database name to an absolute path. */ const string absdbname(const string& name) { if (length(name) == 0 || c_str(name)[0] == '/') return name; char cwd[512]; if (getcwd(cwd, sizeof(cwd)) == NULL) return name; return string(cwd) + "/" + name; } /** * Represents a TinyCDB connection. */ class TinyCDB { public: TinyCDB() : owner(false), fd(-1) { st.st_ino = 0; } TinyCDB(const string& name) : owner(true), name(absdbname(name)), fd(-1) { debug(name, "tinycdb::tinycdb::name"); st.st_ino = 0; } TinyCDB(const TinyCDB& c) : owner(false), name(c.name), fd(c.fd) { debug("tinycdb::tinycdb::copy"); st.st_ino = c.st.st_ino; } const TinyCDB& operator=(const TinyCDB& c) { debug("tinycdb::tinycdb::operator="); if(this == &c) return *this; owner = false; name = c.name; fd = c.fd; st.st_ino = c.st.st_ino; return *this; } ~TinyCDB() { if (!owner) return; if (fd == -1) return; close(fd); } private: bool owner; string name; int fd; struct stat st; friend const string dbname(const TinyCDB& cdb); friend const failable cdbopen(TinyCDB& cdb); friend const failable cdbclose(TinyCDB& cdb); }; /** * Return the name of the database. */ const string dbname(const TinyCDB& cdb) { return cdb.name; } /** * Open a database. */ const failable cdbopen(TinyCDB& cdb) { // Get database file serial number struct stat st; const int s = stat(c_str(cdb.name), &st); if (s == -1) return mkfailure(string("Couldn't tinycdb read database stat: ") + cdb.name); // Open database for the first time if (cdb.fd == -1) { cdb.fd = open(c_str(cdb.name), O_RDONLY); if (cdb.fd == -1) return mkfailure(string("Couldn't open tinycdb database file: ") + cdb.name); debug(cdb.fd, "tinycdb::open::fd"); cdb.st = st; return cdb.fd; } // Close and reopen database after a change if (st.st_ino != cdb.st.st_ino) { // Close current fd close(cdb.fd); // Reopen database const int newfd = open(c_str(cdb.name), O_RDONLY); if (newfd == -1) return mkfailure(string("Couldn't open tinycdb database file: ") + cdb.name); if (newfd == cdb.fd) { debug(cdb.fd, "tinycdb::open::fd"); cdb.st = st; return cdb.fd; } // We got a different fd, dup it to the current fd then close it if (fcntl(newfd, F_DUPFD, cdb.fd) == -1) return mkfailure(string("Couldn't dup tinycdb database file handle: ") + cdb.name); close(newfd); debug(cdb.fd, "tinycdb::open::fd"); cdb.st = st; return cdb.fd; } // No change, just return the current fd return cdb.fd; } /** * Close a database. */ const failable cdbclose(TinyCDB& cdb) { close(cdb.fd); cdb.fd = -1; return true; } /** * Rewrite a database. The given update function is passed each entry, and * can return true to let the entry added to the new db, false to skip the * entry, or a failure. */ const failable rewrite(const lambda(buffer& buf, const unsigned int klen, const unsigned int vlen)>& update, const lambda(struct cdb_make&)>& finish, buffer& buf, const int tmpfd, TinyCDB& cdb) { // Initialize new db structure struct cdb_make cdbm; cdb_make_start(&cdbm, tmpfd); // Open existing db failable ffd = cdbopen(cdb); if (!hasContent(ffd)) return mkfailure(ffd); const int fd = content(ffd); // Read the db header unsigned int pos = 0; if (lseek(fd, 0, SEEK_SET) != 0) return mkfailure("Couldn't seek to tinycdb database start"); if (::read(fd, buf, 2048) != 2048) return mkfailure("Couldn't read tinycdb database header"); pos += 2048; unsigned int eod = cdb_unpack(buf); debug(pos, "tinycdb::rewrite::eod"); // Read and add the existing entries while(pos < eod) { if (eod - pos < 8) return mkfailure("Invalid tinycdb database format, couldn't read entry header"); if (::read(fd, buf, 8) != 8) return mkfailure("Couldn't read tinycdb entry header"); pos += 8; unsigned int klen = cdb_unpack(buf); unsigned int vlen = cdb_unpack(((unsigned char*)buf) + 4); unsigned int elen = klen + vlen; // Read existing entry buf = mkbuffer(buf, elen); if (eod - pos < elen) return mkfailure("Invalid tinycdb database format, couldn't read entry"); if ((unsigned int)::read(fd, buf, elen) != elen) return mkfailure("Couldn't read tinycdb entry"); pos += elen; // Apply the update function to the entry debug(string((char*)buf, klen), "tinycdb::rewrite::existing key"); debug(string(((char*)buf) + klen, vlen), "tinycdb::rewrite::existing value"); const failable u = update(buf, klen, vlen); if (!hasContent(u)) return u; // Skip the entry if the update function returned false if (u == false) continue; // Add the entry to the new db if (cdb_make_add(&cdbm, buf, klen, ((unsigned char*)buf)+klen, vlen) == -1) return mkfailure("Couldn'tt add tinycdb entry"); } if (pos != eod) return mkfailure("Invalid tinycdb database format"); // Call the finish function const failable f = finish(cdbm); if (!hasContent(f)) return f; // Save the new db if (cdb_make_finish(&cdbm) == -1) return mkfailure("Couldn't save tinycdb database"); return true; } const failable rewrite(const lambda(buffer& buf, const unsigned int klen, const unsigned int vlen)>& update, const lambda(struct cdb_make&)>& finish, TinyCDB& cdb) { // Create a new temporary db file string tmpname = dbname(cdb) + ".XXXXXX"; int tmpfd = mkstemp(const_cast(c_str(tmpname))); if (tmpfd == -1) return mkfailure("Couldn't create temporary tinycdb database"); // Rewrite the db, apply the update function to each entry buffer buf = mkbuffer(2048); const failable r = rewrite(update, finish, buf, tmpfd, cdb); if (!hasContent(r)) { close(tmpfd); free(buf); return r; } // Atomically replace the db and reopen it in read mode if (rename(c_str(tmpname), c_str(dbname(cdb))) == -1) return mkfailure("Couldn't rename temporary tinycdb database"); cdbclose(cdb); failable ffd = cdbopen(cdb); if (!hasContent(ffd)) return mkfailure(ffd); return true; } /** * Post a new item to the database. */ struct postUpdate { const string ks; postUpdate(const string& ks) : ks(ks) { } const failable operator()(buffer& buf, const unsigned int klen, unused const unsigned int vlen) const { if (ks == string((char*)buf, klen)) return mkfailure("Key already exists in tinycdb database"); return true; } }; struct postFinish { const string ks; const string vs; postFinish(const string& ks, const string& vs) : ks(ks), vs(vs) { } const failable operator()(struct cdb_make& cdbm) const { if (cdb_make_add(&cdbm, c_str(ks), (unsigned int)length(ks), c_str(vs), (unsigned int)length(vs)) == -1) return mkfailure(string("Couldn't add tinycdb entry: ") + ks); return true; } }; const failable post(const value& key, const value& val, TinyCDB& cdb) { debug(key, "tinycdb::post::key"); debug(val, "tinycdb::post::value"); debug(dbname(cdb), "tinycdb::post::dbname"); const string ks(scheme::writeValue(key)); const string vs(scheme::writeValue(val)); // Process each entry and detect existing key const lambda(buffer& buf, const unsigned int klen, const unsigned int vlen)> update = postUpdate(ks); // Add the new entry to the db const lambda(struct cdb_make& cdbm)> finish = postFinish(ks, vs); // Rewrite the db const failable r = rewrite(update, finish, cdb); debug(r, "tinycdb::post::result"); return r; } /** * Update an item in the database. If the item doesn't exist it is added. */ struct putUpdate { const string ks; putUpdate(const string& ks) : ks(ks) { } const failable operator()(buffer& buf, const unsigned int klen, unused const unsigned int vlen) const { if (ks == string((char*)buf, klen)) return false; return true; } }; struct putFinish { const string ks; const string vs; putFinish(const string& ks, const string& vs) : ks(ks), vs(vs) { } const failable operator()(struct cdb_make& cdbm) const { if (cdb_make_add(&cdbm, c_str(ks), (unsigned int)length(ks), c_str(vs), (unsigned int)length(vs)) == -1) return mkfailure(string("Couldn't add tinycdb entry: ") + ks); return true; } }; const failable put(const value& key, const value& val, TinyCDB& cdb) { debug(key, "tinycdb::put::key"); debug(val, "tinycdb::put::value"); debug(dbname(cdb), "tinycdb::put::dbname"); const string ks(scheme::writeValue(key)); const string vs(scheme::writeValue(val)); // Process each entry and skip existing key const lambda(buffer& buf, const unsigned int klen, const unsigned int vlen)> update = putUpdate(ks); // Add the new entry to the db const lambda(struct cdb_make& cdbm)> finish = putFinish(ks, vs); // Rewrite the db const failable r = rewrite(update, finish, cdb); debug(r, "tinycdb::put::result"); return r; } /** * Get an item from the database. */ const failable get(const value& key, TinyCDB& cdb) { debug(key, "tinycdb::get::key"); debug(dbname(cdb), "tinycdb::get::dbname"); const failable ffd = cdbopen(cdb); if (!hasContent(ffd)) return mkfailure(ffd); const int fd = content(ffd); const string ks(scheme::writeValue(key)); cdbi_t vlen; if (cdb_seek(fd, c_str(ks), (unsigned int)length(ks), &vlen) <= 0) { ostringstream os; os << "Couldn't get tinycdb entry: " << key; return mkfailure(str(os), 404, false); } char* data = gc_cnew(vlen + 1); cdb_bread(fd, data, vlen); data[vlen] = '\0'; const value val(scheme::readValue(string(data))); debug(val, "tinycdb::get::result"); return val; } /** * Delete an item from the database */ struct delUpdate { const string ks; delUpdate(const string& ks) : ks(ks) { } const failable operator()(buffer& buf, const unsigned int klen, unused const unsigned int vlen) const { if (ks == string((char*)buf, klen)) return false; return true; } }; struct delFinish { delFinish() { } const failable operator()(unused struct cdb_make& cdbm) const { return true; } }; const failable del(const value& key, TinyCDB& cdb) { debug(key, "tinycdb::delete::key"); debug(dbname(cdb), "tinycdb::delete::dbname"); const string ks(scheme::writeValue(key)); // Process each entry and skip existing key const lambda(buffer& buf, const unsigned int klen, const unsigned int vlen)> update = delUpdate(ks); // Nothing to do to finish const lambda(struct cdb_make& cdbm)> finish = delFinish(); // Rewrite the db const failable r = rewrite(update, finish, cdb); debug(r, "tinycdb::delete::result"); return r; } } } #endif /* tuscany_tinycdb_hpp */