From db2292603f76f088c46642c22badbe51937b4a72 Mon Sep 17 00:00:00 2001 From: dgsga <181612+dgsga@users.noreply.github.com> Date: Sat, 14 Oct 2023 17:05:22 +0100 Subject: [PATCH] Introduce experimental SQLite CNID backend, GitHub #1177 Based on the Netatalk MySQL CNID backend by Ralph Boehme. Adapted for SQLite by Christopher Kobayashi. Ported to Netatalk 3 by dgsga. Touched up for Netatalk 4 by Daniel Markstedt. --- .github/workflows/build.yml | 15 +- etc/afpd/afp_options.c | 3 + etc/cnid_dbd/cmd_dbd.c | 5 +- etc/cnid_dbd/meson.build | 4 + include/atalk/cnid_sqlite_private.h | 20 + libatalk/cnid/cnid_init.c | 8 + libatalk/cnid/meson.build | 6 + libatalk/cnid/sqlite/cnid_sqlite.c | 939 ++++++++++++++++++++++++++++ libatalk/cnid/sqlite/meson.build | 9 + meson.build | 12 + meson_config.h | 3 + meson_options.txt | 6 + 12 files changed, 1027 insertions(+), 3 deletions(-) create mode 100644 include/atalk/cnid_sqlite_private.h create mode 100644 libatalk/cnid/sqlite/cnid_sqlite.c create mode 100644 libatalk/cnid/sqlite/meson.build diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5135cdf80e..454a6e35df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,6 +74,7 @@ jobs: perl \ pkgconfig \ rpcsvc-proto-dev \ + sqlite-dev \ talloc-dev \ tracker \ tracker-dev \ @@ -127,6 +128,7 @@ jobs: perl \ pkgconfig \ rpcsvc-proto \ + sqlite \ talloc \ tinysparql \ unicode-character-database @@ -183,6 +185,7 @@ jobs: libldap2-dev \ libmariadb-dev \ libpam0g-dev \ + libsqlite3-dev \ libtalloc-dev \ libtirpc-dev \ libtracker-sparql-3.0-dev \ @@ -253,6 +256,7 @@ jobs: perl \ perl-Net-DBus \ quota-devel \ + sqlite-devel \ systemd \ systemtap-sdt-devel \ tracker \ @@ -314,6 +318,7 @@ jobs: pam-devel \ perl \ pkg-config \ + sqlite3-devel \ systemd \ systemtap-sdt-devel \ tcpd-devel \ @@ -424,7 +429,7 @@ jobs: - uses: actions/checkout@v4 - name: Install dependencies run: | - brew install berkeley-db cmark-gfm docbook-xsl libxslt meson mysql talloc + brew install berkeley-db cmark-gfm docbook-xsl libxslt meson mysql sqlite talloc - name: Configure run: | meson setup build \ @@ -482,6 +487,7 @@ jobs: py39-gdbm \ py39-sqlite3 \ py39-tkinter \ + sqlite \ talloc \ tracker3 run: | @@ -525,6 +531,7 @@ jobs: p5-Net-DBus \ perl5 \ pkgconf \ + sqlite3 \ talloc \ tracker3 run: | @@ -580,6 +587,7 @@ jobs: p5-Net-DBus \ perl \ pkg-config \ + sqlite3 \ talloc \ tex-unicode-data run: | @@ -635,6 +643,7 @@ jobs: openpam \ p5-Net-DBus \ pkgconf \ + sqlite \ tracker3 run: | set -e @@ -682,6 +691,7 @@ jobs: libxslt \ meson \ mysql-client \ + sqlite3 \ talloc run: | set -e @@ -728,7 +738,8 @@ jobs: libgcrypt \ ninja \ pkg-config \ - python/pip + python/pip \ + sqlite-3 pip install meson run: | set -e diff --git a/etc/afpd/afp_options.c b/etc/afpd/afp_options.c index 233a172a5f..7d9facddd9 100644 --- a/etc/afpd/afp_options.c +++ b/etc/afpd/afp_options.c @@ -83,6 +83,9 @@ static void show_version( void ) #endif #ifdef CNID_BACKEND_MYSQL printf( "mysql " ); +#endif +#ifdef CNID_BACKEND_SQLITE + printf( "sqlite " ); #endif puts( "" ); } diff --git a/etc/cnid_dbd/cmd_dbd.c b/etc/cnid_dbd/cmd_dbd.c index aeeccc1c34..e0f8c4a1ac 100644 --- a/etc/cnid_dbd/cmd_dbd.c +++ b/etc/cnid_dbd/cmd_dbd.c @@ -247,7 +247,10 @@ int main(int argc, char **argv) } /* open volume */ - if (STRCMP(vol->v_cnidscheme, != , "dbd") && STRCMP(vol->v_cnidscheme, != , "mysql")) { + if (STRCMP(vol->v_cnidscheme, != , "dbd") + && STRCMP(vol->v_cnidscheme, != , "mysql") + && STRCMP(vol->v_cnidscheme, != , "sqlite") + ) { dbd_log(LOGSTD, "\"%s\" isn't a \"dbd\" CNID volume", vol->v_path); exit(EXIT_FAILURE); } diff --git a/etc/cnid_dbd/meson.build b/etc/cnid_dbd/meson.build index 9d746e18db..de6e6c6a63 100644 --- a/etc/cnid_dbd/meson.build +++ b/etc/cnid_dbd/meson.build @@ -23,6 +23,10 @@ if use_dbd_backend cnid_dbd_deps += mysqlclient endif + if use_sqlite_backend + cnid_dbd_deps += sqlite_deps + endif + cnid_metad_sources = ['cnid_metad.c', 'usockfd.c', 'db_param.c'] dbd_sources = [ diff --git a/include/atalk/cnid_sqlite_private.h b/include/atalk/cnid_sqlite_private.h new file mode 100644 index 0000000000..6c78568c68 --- /dev/null +++ b/include/atalk/cnid_sqlite_private.h @@ -0,0 +1,20 @@ +#ifndef _ATALK_CNID_SQLITE_PRIVATE_H +#define _ATALK_CNID_SQLITE_PRIVATE_H 1 + +#include +#include + +#define CNID_SQLITE_FLAG_DEPLETED (1 << 0) /* CNID set overflowed */ + +typedef struct CNID_sqlite_private { + struct vol *vol; + uint32_t cnid_sqlite_flags; + sqlite3 *cnid_sqlite_con; + char *cnid_sqlite_voluuid_str; + cnid_t cnid_sqlite_hint; + sqlite3_stmt *cnid_lookup_stmt; + sqlite3_stmt *cnid_add_stmt; + sqlite3_stmt *cnid_put_stmt; +} CNID_sqlite_private; + +#endif diff --git a/libatalk/cnid/cnid_init.c b/libatalk/cnid/cnid_init.c index 3a7669d715..dad7820824 100644 --- a/libatalk/cnid/cnid_init.c +++ b/libatalk/cnid/cnid_init.c @@ -44,6 +44,10 @@ extern struct _cnid_module cnid_dbd_module; extern struct _cnid_module cnid_mysql_module; #endif +#ifdef CNID_BACKEND_SQLITE +extern struct _cnid_module cnid_sqlite_module; +#endif + void cnid_init(void) { #ifdef CNID_BACKEND_LAST @@ -57,4 +61,8 @@ void cnid_init(void) #ifdef CNID_BACKEND_MYSQL cnid_register(&cnid_mysql_module); #endif + +#ifdef CNID_BACKEND_SQLITE + cnid_register(&cnid_sqlite_module); +#endif } diff --git a/libatalk/cnid/meson.build b/libatalk/cnid/meson.build index c9a5b5e5c1..261bd0494c 100644 --- a/libatalk/cnid/meson.build +++ b/libatalk/cnid/meson.build @@ -14,6 +14,12 @@ if use_mysql_backend libcnid_deps += mysql_deps endif +if use_sqlite_backend + subdir('sqlite') + libcnid_libs += libcnid_sqlite + libcnid_deps += sqlite_deps +endif + cnid_sources = ['cnid_init.c', 'cnid.c'] libcnid = static_library( diff --git a/libatalk/cnid/sqlite/cnid_sqlite.c b/libatalk/cnid/sqlite/cnid_sqlite.c new file mode 100644 index 0000000000..d08f56389e --- /dev/null +++ b/libatalk/cnid/sqlite/cnid_sqlite.c @@ -0,0 +1,939 @@ +/* + * Copyright (C) Ralph Boehme 2013 + * All Rights Reserved. See COPYING. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#undef _FORTIFY_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +sqlite3_stmt *stmt = NULL; + +/* + * Prepared statement parameters + */ +static char stmt_param_name[MAXPATHLEN]; +static uint32_t stmt_param_name_len; +static uint64_t stmt_param_id; +static uint64_t stmt_param_did; +static uint64_t stmt_param_dev; +static uint64_t stmt_param_ino; + +/* + * lookup result parameters + */ +static uint64_t lookup_result_id; +static uint64_t lookup_result_did; +static char lookup_result_name[MAXPATHLEN]; +static unsigned long lookup_result_name_len; +static uint64_t lookup_result_dev; +static uint64_t lookup_result_ino; + +static int init_prepared_stmt_lookup(CNID_sqlite_private * db) +{ + EC_INIT; + char *sql = NULL; + + EC_ZERO(sqlite3_finalize(db->cnid_lookup_stmt)); + EC_NEG1(asprintf + (&sql, + "SELECT Id,Did,Name,DevNo,InodeNo FROM `%s` " + "WHERE (Name=? AND Did=?) OR (DevNo=? AND InodeNo=?)", + db->cnid_sqlite_voluuid_str)); + EC_ZERO_LOG(sqlite3_prepare_v2(db->cnid_sqlite_con, sql, strlen(sql), &db->cnid_lookup_stmt, NULL)); + + EC_ZERO_LOG(sqlite3_bind_text(db->cnid_lookup_stmt, 1, stmt_param_name, strlen(stmt_param_name), SQLITE_STATIC) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_lookup_stmt, 2, stmt_param_did) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_lookup_stmt, 3, stmt_param_dev) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_lookup_stmt, 4, stmt_param_ino) ); + +EC_CLEANUP: + if (sql) + free(sql); + EC_EXIT; +} + +static int init_prepared_stmt_add(CNID_sqlite_private * db) +{ + EC_INIT; + char *sql = NULL; + + EC_ZERO(sqlite3_finalize(db->cnid_add_stmt)); + EC_NEG1(ret = asprintf(&sql, + "INSERT INTO `%s` (Name,Did,DevNo,InodeNo) VALUES(?,?,?,?,?)", + db->cnid_sqlite_voluuid_str) ); + + EC_ZERO_LOG(sqlite3_prepare_v2 + (db->cnid_sqlite_con, sql, strlen(sql), &db->cnid_put_stmt, NULL)); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_add_stmt, + 1, stmt_param_id) ); + EC_ZERO_LOG(sqlite3_bind_text(db->cnid_add_stmt, + 2, stmt_param_name, strlen(stmt_param_name), SQLITE_STATIC) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_add_stmt, + 3, stmt_param_did) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_add_stmt, + 4, stmt_param_dev) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_add_stmt, + 5, stmt_param_ino) ); + +EC_CLEANUP: + if (sql) + free(sql); + EC_EXIT; +} + +static int init_prepared_stmt_put(CNID_sqlite_private * db) +{ + EC_INIT; + char *sql = NULL; + + EC_ZERO(sqlite3_finalize(db->cnid_put_stmt)); + EC_NEG1(asprintf(&sql, + "INSERT INTO `%s` (Id,Name,Did,DevNo,InodeNo) VALUES(?,?,?,?,?)", + db->cnid_sqlite_voluuid_str)); + + EC_ZERO_LOG(sqlite3_prepare_v2 + (db->cnid_sqlite_con, sql, strlen(sql), &db->cnid_put_stmt, NULL)); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_put_stmt, + 1, stmt_param_id) ); + EC_ZERO_LOG(sqlite3_bind_text(db->cnid_put_stmt, + 2, stmt_param_name, strlen(stmt_param_name), SQLITE_STATIC) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_put_stmt, + 3, stmt_param_did) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_put_stmt, + 4, stmt_param_dev) ); + EC_ZERO_LOG(sqlite3_bind_int64(db->cnid_put_stmt, + 5, stmt_param_ino) ); + +EC_CLEANUP: + if (sql) + free(sql); + EC_EXIT; +} + +static int init_prepared_stmt(CNID_sqlite_private * db) +{ + EC_INIT; + + EC_ZERO(init_prepared_stmt_lookup(db)); + EC_ZERO(init_prepared_stmt_add(db)); + EC_ZERO(init_prepared_stmt_put(db)); + +EC_CLEANUP: + EC_EXIT; +} + +static void close_prepared_stmt(CNID_sqlite_private * db) +{ + sqlite3_finalize(db->cnid_lookup_stmt); + sqlite3_finalize(db->cnid_add_stmt); + sqlite3_finalize(db->cnid_put_stmt); +} + +static int cnid_sqlite_execute(sqlite3 * con, char *fmt, ...) +{ + char *sql = NULL; + va_list ap; + int rv; + + va_start(ap, fmt); + if (vasprintf(&sql, fmt, ap) == -1) + return -1; + va_end(ap); + + LOG(log_maxdebug, logtype_cnid, "SQL: %s", sql); + + rv = sqlite3_exec(con, sql, NULL, NULL, NULL); + + if (rv) { + LOG(log_info, logtype_cnid, + "sqlite query \"%s\", error: %s", sql, sqlite3_errmsg(con)); + errno = CNID_ERR_DB; + } + free(sql); + return rv; +} + +int cnid_sqlite_delete(struct _cnid_db *cdb, const cnid_t id) +{ + EC_INIT; + CNID_sqlite_private *db; + + if (!cdb || !(db = cdb->cnid_db_private) || !id) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_delete: Parameter error"); + errno = CNID_ERR_PARAM; + EC_FAIL; + } + + LOG(log_debug, logtype_cnid, + "cnid_sqlite_delete(%" PRIu32 "): BEGIN", ntohl(id)); + + EC_NEG1(cnid_sqlite_execute(db->cnid_sqlite_con, + "DELETE FROM `%s` WHERE Id=%" PRIu32, + db->cnid_sqlite_voluuid_str, + ntohl(id))); + + LOG(log_debug, logtype_cnid, + "cnid_sqlite_delete(%" PRIu32 "): END", ntohl(id)); + +EC_CLEANUP: + EC_EXIT; +} + +void cnid_sqlite_close(struct _cnid_db *cdb) +{ + CNID_sqlite_private *db; + + if (!cdb) { + LOG(log_error, logtype_cnid, + "cnid_close called with NULL argument !"); + return; + } + + if ((db = cdb->cnid_db_private) != NULL) { + LOG(log_debug, logtype_cnid, + "closing database connection for volume '%s'", + cdb->cnid_db_vol->v_localname); + + free(db->cnid_sqlite_voluuid_str); + + close_prepared_stmt(db); + + if (db->cnid_sqlite_con) { + sqlite3_close(db->cnid_sqlite_con); + sqlite3_shutdown(); + } + free(db); + } + + free(cdb); + + return; +} + +int cnid_sqlite_update(struct _cnid_db *cdb, + cnid_t id, + const struct stat *st, + cnid_t did, const char *name, size_t len) +{ + EC_INIT; + CNID_sqlite_private *db; + cnid_t update_id; + + if (!cdb || !(db = cdb->cnid_db_private) || !id || !st || !name) { + LOG(log_error, logtype_cnid, + "cnid_update: Parameter error"); + errno = CNID_ERR_PARAM; + EC_FAIL; + } + + if (len > MAXPATHLEN) { + LOG(log_error, logtype_cnid, + "cnid_update: Path name is too long"); + errno = CNID_ERR_PATH; + EC_FAIL; + } + + uint64_t dev = st->st_dev; + uint64_t ino = st->st_ino; + + do { + EC_NEG1(cnid_sqlite_execute(db->cnid_sqlite_con, + "DELETE FROM `%s` WHERE Id=%" + PRIu32, + db->cnid_sqlite_voluuid_str, + ntohl(id))); + EC_NEG1(cnid_sqlite_execute + (db->cnid_sqlite_con, + "DELETE FROM `%s` WHERE Did=%" PRIu32 + " AND Name='%s'", db->cnid_sqlite_voluuid_str, + ntohl(did), name)); + EC_NEG1(cnid_sqlite_execute + (db->cnid_sqlite_con, + "DELETE FROM `%s` WHERE DevNo=%" PRIu64 + " AND InodeNo=%" PRIu64, + db->cnid_sqlite_voluuid_str, dev, ino)); + + stmt_param_id = ntohl(id); + strncpy(stmt_param_name, name, sizeof(stmt_param_name)-1); + stmt_param_name_len = len; + stmt_param_did = ntohl(did); + stmt_param_dev = dev; + stmt_param_ino = ino; + + if (sqlite3_step(db->cnid_put_stmt) != SQLITE_ROW) { + switch (sqlite3_errcode(db->cnid_sqlite_con)) { +// FIXME: Is this a valid error code for sqlite? +#if 0 + case ER_DUP_ENTRY: + /* + * Race condition: + * between deletion and insert another process + * may have inserted this entry. + */ + continue; +#endif + default: + EC_FAIL; + } + } + update_id = sqlite3_last_insert_rowid(db->cnid_sqlite_con); + } while (update_id != ntohl(id)); + +EC_CLEANUP: + EC_EXIT; +} + +cnid_t cnid_sqlite_lookup(struct _cnid_db *cdb, + const struct stat *st, + cnid_t did, const char *name, size_t len) +{ + EC_INIT; + + CNID_sqlite_private *db; + cnid_t id = CNID_INVALID; + bool have_result = false; + uint64_t dev = st->st_dev; + uint64_t ino = st->st_ino; + + if (!cdb || !(db = cdb->cnid_db_private) || !st || !name) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_lookup: Parameter error"); + errno = CNID_ERR_PARAM; + EC_FAIL; + } + + if (len > MAXPATHLEN) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_lookup: Path name is too long"); + errno = CNID_ERR_PATH; + EC_FAIL; + } + + LOG(log_maxdebug, logtype_cnid, + "cnid_sqlite_lookup(did: %" PRIu32 ", name: \"%s\"): START", + ntohl(did), name); + + strncpy(stmt_param_name, name, sizeof(stmt_param_name)-1); + stmt_param_name_len = len; + stmt_param_did = ntohl(did); + stmt_param_dev = dev; + stmt_param_ino = ino; + + + uint64_t retdev, retino; + cnid_t retid, retdid; + char *retname; + int sqlite_return; + + sqlite_return = sqlite3_step(stmt); + if (sqlite_return == SQLITE_DONE) { + /* not found (no rows) */ + LOG(log_debug, logtype_cnid, + "cnid_sqlite_lookup: name: '%s', did: %u is not in the CNID database", + name, ntohl(did)); + errno = CNID_INVALID; + EC_FAIL; + } + /* got at least one row, store result in lookup_result_X */ + lookup_result_id = sqlite3_column_int64(stmt, 1); + lookup_result_did = sqlite3_column_int64(stmt, 2); + snprintf(lookup_result_name, MAXPATHLEN-1, (const char *)sqlite3_column_text(stmt, 3)); + lookup_result_name_len = strlen(lookup_result_name); + lookup_result_dev = sqlite3_column_int64(stmt, 4); + lookup_result_ino = sqlite3_column_int64(stmt, 5); + + sqlite_return = sqlite3_step(stmt); + if (sqlite_return != SQLITE_DONE) { + /* We have more than one row */ + /* a mismatch, delete both and return not found */ + while (sqlite3_step(db->cnid_lookup_stmt) != SQLITE_DONE) { + if (cnid_sqlite_delete + (cdb, htonl((cnid_t) lookup_result_id))) { + LOG(log_error, logtype_cnid, + "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + errno = CNID_ERR_DB; + EC_FAIL; + } + } + errno = CNID_INVALID; + EC_FAIL; + } + + /* handle 3+ matches? */ +#if 0 + default: + errno = CNID_ERR_DB; + EC_FAIL; + } +#endif + + retid = htonl(lookup_result_id); + retdid = htonl(lookup_result_did); + retname = lookup_result_name; + retdev = lookup_result_dev; + retino = lookup_result_ino; + + if (retdid != did || STRCMP(retname, !=, name)) { + LOG(log_debug, logtype_cnid, + "cnid_sqlite_lookup(CNID %" PRIu32 ", DID: %" + PRIu32 + ", name: \"%s\"): server side mv oder reused inode", + ntohl(did), name); + LOG(log_debug, logtype_cnid, + "cnid_sqlite_lookup: server side mv, got hint, updating"); + if (cnid_sqlite_update(cdb, retid, st, did, name, len) != 0) { + LOG(log_error, logtype_cnid, + "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + errno = CNID_ERR_DB; + EC_FAIL; + } + id = retid; + } else if (retdev != dev || retino != ino) { + LOG(log_debug, logtype_cnid, + "cnid_sqlite_lookup(DID:%u, name: \"%s\"): changed dev/ino", + ntohl(did), name); + if (cnid_sqlite_delete(cdb, retid) != 0) { + LOG(log_error, logtype_cnid, + "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + errno = CNID_ERR_DB; + EC_FAIL; + } + errno = CNID_INVALID; + EC_FAIL; + } else { + /* everythings good */ + id = retid; + } + +EC_CLEANUP: + LOG(log_debug, logtype_cnid, "cnid_sqlite_lookup: id: %" PRIu32, + ntohl(id)); + if (have_result) + sqlite3_finalize(db->cnid_lookup_stmt); + if (ret != 0) + id = CNID_INVALID; + return id; +} + +cnid_t cnid_sqlite_add(struct _cnid_db *cdb, + const struct stat *st, + cnid_t did, + const char *name, size_t len, cnid_t hint) +{ + EC_INIT; + CNID_sqlite_private *db; + cnid_t id = CNID_INVALID; + uint64_t lastid; + + if (!cdb || !(db = cdb->cnid_db_private) || !st || !name) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_add: Parameter error"); + errno = CNID_ERR_PARAM; + EC_FAIL; + } + + if (len > MAXPATHLEN) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_add: Path name is too long"); + errno = CNID_ERR_PATH; + EC_FAIL; + } + + uint64_t dev = st->st_dev; + uint64_t ino = st->st_ino; + db->cnid_sqlite_hint = hint; + + LOG(log_maxdebug, logtype_cnid, + "cnid_sqlite_add(did: %" PRIu32 ", name: \"%s\", hint: %" PRIu32 + "): START", ntohl(did), name, ntohl(hint)); + + do { + if ((id = + cnid_sqlite_lookup(cdb, st, did, (char *) name, + len)) == CNID_INVALID) { + if (errno == CNID_ERR_DB) + EC_FAIL; + /* + * If the CNID set overflowed before + * (CNID_SQLITE_FLAG_DEPLETED) ignore the CNID "hint" + * taken from the AppleDouble file + */ + if (!db->cnid_sqlite_hint + || (db-> + cnid_sqlite_flags & + CNID_SQLITE_FLAG_DEPLETED)) { + stmt = db->cnid_add_stmt; + } else { + stmt = db->cnid_put_stmt; + stmt_param_id = ntohl(db->cnid_sqlite_hint); + } + strncpy(stmt_param_name, name, + sizeof(stmt_param_name)-1); + stmt_param_name_len = len; + stmt_param_did = ntohl(did); + stmt_param_dev = dev; + stmt_param_ino = ino; + + ret = sqlite3_step(stmt); + if (ret != SQLITE_ROW) { + EC_FAIL; + } +#if 0 + /* + * Race condition: + * between lookup and insert another process may have inserted + * this entry. + */ + if (db->cnid_sqlite_hint) + db->cnid_sqlite_hint = CNID_INVALID; + continue; + } +#endif + + lastid = sqlite3_last_insert_rowid(db->cnid_sqlite_con); + + if (lastid > 0xffffffff) { /* FIXME: this is wrong */ + /* CNID set is depleted, restart from scratch */ + EC_NEG1(cnid_sqlite_execute + (db->cnid_sqlite_con, + "START TRANSACTION;" + "UPDATE volumes SET Depleted=1 WHERE VolUUID='%s';" + "TRUNCATE TABLE %s;" + "ALTER TABLE %s AUTO_INCREMENT = 17;" + "COMMIT;", + db->cnid_sqlite_voluuid_str, + db->cnid_sqlite_voluuid_str, + db->cnid_sqlite_voluuid_str)); + db->cnid_sqlite_flags |= + CNID_SQLITE_FLAG_DEPLETED; + hint = CNID_INVALID; +#if 0 // again, really wrong + do { + result = + sqlite_store_result(db-> + cnid_sqlite_con); + if (result) + sqlite_free_result(result); + } while (sqlite_next_result + (db->cnid_sqlite_con) == 0); + continue; +#endif + } + + /* Finally assign our result */ + id = htonl((uint32_t) lastid); + } + } while (id == CNID_INVALID); + +EC_CLEANUP: + LOG(log_debug, logtype_cnid, "cnid_sqlite_add: id: %" PRIu32, + ntohl(id)); + + return id; +} + +cnid_t cnid_sqlite_get(struct _cnid_db *cdb, cnid_t did, const char *name, + size_t len) +{ + EC_INIT; + CNID_sqlite_private *db; + cnid_t id = CNID_INVALID; + char *sql = NULL; + sqlite3_stmt *transient_stmt = NULL; + + if (!cdb || !(db = cdb->cnid_db_private) || !name) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_get: Parameter error"); + errno = CNID_ERR_PARAM; + EC_FAIL; + } + + if (len > MAXPATHLEN) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_get: name is too long"); + errno = CNID_ERR_PATH; + return CNID_INVALID; + } + + LOG(log_debug, logtype_cnid, + "cnid_sqlite_get(did: %" PRIu32 ", name: \"%s\"): START", + ntohl(did), name); + + EC_NEG1(ret = asprintf + (&sql, + "SELECT Id FROM `%s` WHERE Name='%s' AND Did=?", + db->cnid_sqlite_voluuid_str, name) ); + EC_ZERO_LOG(sqlite3_prepare_v2 + (db->cnid_sqlite_con, sql, strlen(sql), &transient_stmt, NULL) ); + EC_ZERO_LOG(sqlite3_bind_int64(transient_stmt, + 1, ntohl(did) ) ); + + if (sqlite3_step(transient_stmt) != SQLITE_ROW) { + if (sqlite3_errcode(db->cnid_sqlite_con) != 0) { + LOG(log_error, logtype_cnid, + "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + EC_FAIL; + } + } + + id = htonl(sqlite3_column_int64(transient_stmt, 1)); + +EC_CLEANUP: + LOG(log_debug, logtype_cnid, "cnid_sqlite_get: id: %" PRIu32, + ntohl(id)); + + if (transient_stmt) + sqlite3_finalize(transient_stmt); + + return id; +} + +char *cnid_sqlite_resolve(struct _cnid_db *cdb, cnid_t * id, void *buffer, + size_t len) +{ + EC_INIT; + char *sql = NULL; + sqlite3_stmt *transient_stmt = NULL; + CNID_sqlite_private *db; + + if (!cdb || !(db = cdb->cnid_db_private)) { + LOG(log_error, logtype_cnid, + "cnid_sqlite_get: Parameter error"); + errno = CNID_ERR_PARAM; + EC_FAIL; + } + + EC_NEG1(ret = asprintf + (&sql, + "SELECT Did,Name FROM `%s` WHERE Id=?", + db->cnid_sqlite_voluuid_str) ); + + EC_ZERO_LOG(sqlite3_prepare_v2 + (db->cnid_sqlite_con, sql, strlen(sql), &transient_stmt, NULL) ); + EC_ZERO_LOG(sqlite3_bind_int64(transient_stmt, + 1, ntohl(*id) ) ); + + if (sqlite3_step(transient_stmt) != SQLITE_ROW) { + EC_FAIL; + } + + *id = htonl(sqlite3_column_int64(transient_stmt, 1)); + strncpy(buffer, (const char *)sqlite3_column_text(transient_stmt, 2), len); + +EC_CLEANUP: + if (transient_stmt) + sqlite3_finalize(transient_stmt); + +#if 0 // FIXME: handle this + if (ret != 0) { + *id = CNID_INVALID; + return NULL; + } +#endif + return buffer; +} + +/** + * Caller passes buffer where we will store the db stamp + **/ +int cnid_sqlite_getstamp(struct _cnid_db *cdb, void *buffer, + const size_t len) +{ + EC_INIT; + char *sql = NULL; + sqlite3_stmt *transient_stmt = NULL; + CNID_sqlite_private *db; + + if (!cdb || !(db = cdb->cnid_db_private)) { + LOG(log_error, logtype_cnid, "cnid_find: Parameter error"); + errno = CNID_ERR_PARAM; + return CNID_INVALID; + } + + if (!buffer) { + LOG(log_error, logtype_cnid, "cnid_getstamp: bad buffer"); + return CNID_INVALID; + } + + EC_NEG1(asprintf + (&sql, + "SELECT Stamp FROM volumes WHERE VolPath='%s'", + cdb->cnid_db_vol->v_path)); + + EC_ZERO_LOG(sqlite3_prepare_v2 + (db->cnid_sqlite_con, sql, strlen(sql), &transient_stmt, NULL)); + + if (sqlite3_step(transient_stmt) != SQLITE_ROW) { + LOG(log_error, logtype_cnid, + "Can't get DB stamp for volumes \"%s\"", + cdb->cnid_db_vol->v_path); + EC_FAIL; + } + + strncpy(buffer, (const char *)sqlite3_column_text(transient_stmt, 1), len); + +EC_CLEANUP: + if (transient_stmt) + sqlite3_finalize(transient_stmt); + EC_EXIT; +} + + +int cnid_sqlite_find(struct _cnid_db *cdb, const char *name, size_t namelen, + void *buffer, size_t buflen) +{ + LOG(log_error, logtype_cnid, + "cnid_sqlite_find(\"%s\"): not supported with sqlite CNID backend", + name); + return -1; +} + +cnid_t cnid_sqlite_rebuild_add(struct _cnid_db *cdb, const struct stat *st, + cnid_t did, const char *name, size_t len, + cnid_t hint) +{ + LOG(log_error, logtype_cnid, + "cnid_sqlite_rebuild_add(\"%s\"): not supported with sqlite CNID backend", + name); + return CNID_INVALID; +} + +int cnid_sqlite_wipe(struct _cnid_db *cdb) +{ + EC_INIT; + CNID_sqlite_private *db; + + if (!cdb || !(db = cdb->cnid_db_private)) { + LOG(log_error, logtype_cnid, "cnid_wipe: Parameter error"); + errno = CNID_ERR_PARAM; + return -1; + } + + LOG(log_debug, logtype_cnid, "cnid_dbd_wipe"); + + EC_NEG1(cnid_sqlite_execute(db->cnid_sqlite_con, + "START TRANSACTION;" + "UPDATE volumes SET Depleted=0 WHERE VolUUID='%s';" + "TRUNCATE TABLE `%s`;" + "ALTER TABLE `%s` AUTO_INCREMENT = 17;" + "COMMIT;", + db->cnid_sqlite_voluuid_str, + db->cnid_sqlite_voluuid_str, + db->cnid_sqlite_voluuid_str)); + +EC_CLEANUP: + EC_EXIT; +} + +static struct _cnid_db *cnid_sqlite_new(struct vol *vol) +{ + struct _cnid_db *cdb; + + if ((cdb = + (struct _cnid_db *) calloc(1, + sizeof(struct _cnid_db))) == NULL) + return NULL; + + cdb->cnid_db_vol = vol; + cdb->cnid_db_flags = CNID_FLAG_PERSISTENT | CNID_FLAG_LAZY_INIT; + cdb->cnid_add = cnid_sqlite_add; + cdb->cnid_delete = cnid_sqlite_delete; + cdb->cnid_get = cnid_sqlite_get; + cdb->cnid_lookup = cnid_sqlite_lookup; + cdb->cnid_find = cnid_sqlite_find; + cdb->cnid_nextid = NULL; + cdb->cnid_resolve = cnid_sqlite_resolve; + cdb->cnid_getstamp = cnid_sqlite_getstamp; + cdb->cnid_update = cnid_sqlite_update; + cdb->cnid_rebuild_add = cnid_sqlite_rebuild_add; + cdb->cnid_close = cnid_sqlite_close; +#if 0 + cdb->cnid_wipe = cnid_sqlite_wipe; +#endif + return cdb; +} + +/* ---------------------- */ +struct _cnid_db *cnid_sqlite_open(struct cnid_open_args *args) +{ + EC_INIT; + CNID_sqlite_private *db = NULL; + struct _cnid_db *cdb = NULL; + char *sql = NULL; + struct vol *vol = args->cnid_args_vol; + sqlite3_stmt *transient_stmt = NULL; + + EC_NULL(cdb = cnid_sqlite_new(vol)); + EC_NULL(db = + (CNID_sqlite_private *) calloc(1, + sizeof + (CNID_sqlite_private))); + cdb->cnid_db_private = db; + + /* Initialize and connect to sqlite3 database */ + sqlite3_initialize(); + EC_ZERO( sqlite3_open_v2("file:/var/db/netatalk/cnid.sqlite", + &db->cnid_sqlite_con, + SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, NULL)); + + + /* Add volume to volume table */ + if (cnid_sqlite_execute(db->cnid_sqlite_con, + "CREATE TABLE IF NOT EXISTS volumes " + "( VolUUID CHAR(32) PRIMARY KEY," + "VolPath TEXT(4096)," + "Stamp BINARY(8)," + "Depleted INT," + "INDEX(VolPath(64))" + ")")) + { + LOG(log_error, logtype_cnid, "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + EC_FAIL; + } + + /* Create a blob. The MySQL code used an escape string function, + but we're going to use a prepared statement */ + time_t now = time(NULL); + char stamp[8]; + memset(stamp, 0, 8); + memcpy(stamp, &now, sizeof(time_t)); + + EC_NEG1(asprintf + (&sql, + "INSERT INTO volumes " + "(VolUUID, Volpath, Stamp, Depleted) " + "VALUES(?, ?, ?, 0)")); + EC_ZERO_LOG(sqlite3_prepare_v2 + (db->cnid_sqlite_con, sql, strlen(sql), &transient_stmt, NULL)); + EC_ZERO_LOG(sqlite3_bind_text(transient_stmt, + 1, db->cnid_sqlite_voluuid_str, + strlen(db->cnid_sqlite_voluuid_str), SQLITE_STATIC) ); + EC_ZERO_LOG(sqlite3_bind_text(transient_stmt, + 2, vol->v_path, + strlen(vol->v_path), SQLITE_STATIC) ); + EC_ZERO_LOG(sqlite3_bind_text(transient_stmt, + 3, stamp, + strlen(stamp), SQLITE_STATIC) ); + + sqlite3_step(transient_stmt); +#if 0 // FIXME: must figure out how to check for failed insert + if (sqlite3_step(transient_stmt)) { + if (mysql_errno(db->cnid_sqlite_con) != ER_DUP_ENTRY) { + LOG(log_error, logtype_cnid, + "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + EC_FAIL; + } + } +#endif + sqlite3_finalize(transient_stmt); + +#if 0 // FIXME + /* + * Check whether CNID set overflowed before. + * If that's the case, in cnid_sqlite_add() we'll ignore the CNID + * "hint" taken from the AppleDouble file. + */ + if (cnid_sqlite_execute(db->cnid_sqlite_con, + "SELECT Depleted FROM volumes WHERE VolUUID='%s'", + db->cnid_sqlite_voluuid_str)) { + LOG(log_error, logtype_cnid, "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + EC_FAIL; + } + if ((result = mysql_store_result(db->cnid_sqlite_con)) == NULL) { + LOG(log_error, logtype_cnid, "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + errno = CNID_ERR_DB; + EC_FAIL; + } + if (mysql_num_rows(result)) { + row = mysql_fetch_row(result); + int depleted = atoi(row[0]); + if (depleted) + db->cnid_sqlite_flags |= CNID_SQLITE_FLAG_DEPLETED; + } + mysql_free_result(result); + result = NULL; +#endif + + /* Create volume table */ + if (cnid_sqlite_execute(db->cnid_sqlite_con, + "CREATE TABLE IF NOT EXISTS `%s`" + "(Id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT," + "Name VARCHAR(255) NOT NULL," + "Did INT UNSIGNED NOT NULL," + "DevNo BIGINT UNSIGNED NOT NULL," + "InodeNo BIGINT UNSIGNED NOT NULL," + "UNIQUE DidName(Did, Name), UNIQUE DevIno(DevNo, InodeNo)) " + "AUTO_INCREMENT=17", + db->cnid_sqlite_voluuid_str)) { + LOG(log_error, logtype_cnid, "sqlite query error: %s", + sqlite3_errmsg(db->cnid_sqlite_con)); + EC_FAIL; + } + + EC_ZERO(init_prepared_stmt(db)); + + LOG(log_debug, logtype_cnid, + "Finished initializing sqlite CNID module for volume '%s'", + vol); + + EC_CLEANUP: + if (transient_stmt) + sqlite3_finalize(transient_stmt); + + if (ret != 0) { + if (cdb) + free(cdb); + cdb = NULL; + if (db) + free(db); + } + return cdb; +} + +struct _cnid_module cnid_sqlite_module = { + "sqlite", + { NULL, NULL }, + cnid_sqlite_open, + 0 +}; diff --git a/libatalk/cnid/sqlite/meson.build b/libatalk/cnid/sqlite/meson.build new file mode 100644 index 0000000000..cb99e82b65 --- /dev/null +++ b/libatalk/cnid/sqlite/meson.build @@ -0,0 +1,9 @@ +libcnid_sqlite_sources = ['cnid_sqlite.c'] + +libcnid_sqlite = static_library( + 'cnid_sqlite', + libcnid_sqlite_sources, + include_directories: root_includes, + dependencies: sqlite_deps, + install: false, +) diff --git a/meson.build b/meson.build index 6faa8f9534..5a8e6e2cea 100644 --- a/meson.build +++ b/meson.build @@ -1524,6 +1524,18 @@ if get_option('with-cnid-mysql-backend') endif endif +# Check for sqlite CNID backend + +sqlite_deps = dependency('sqlite3', required: false) + +use_sqlite_backend = false + +if get_option('with-cnid-sqlite-backend') and sqlite_deps.found() + use_sqlite_backend = true + cnid_backends += ' sqlite' + cdata.set('CNID_BACKEND_SQLITE', 1) +endif + compiled_backends = '"' + cnid_backends + '"' cdata.set('compiled_backends', compiled_backends) diff --git a/meson_config.h b/meson_config.h index c712aa368e..14fe56d6cd 100644 --- a/meson_config.h +++ b/meson_config.h @@ -19,6 +19,9 @@ /* whether the MySQL CNID module is available */ #mesondefine CNID_BACKEND_MYSQL +/* whether the SQLite CNID module is available */ +#mesondefine CNID_BACKEND_SQLITE + /* Path to dbus-daemon */ #mesondefine DBUS_DAEMON_PATH diff --git a/meson_options.txt b/meson_options.txt index 8f3a09760d..3113caacc1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -33,6 +33,12 @@ option( value: true, description: 'Compile MySQL CNID scheme', ) +option( + 'with-cnid-sqlite-backend', + type: 'boolean', + value: true, + description: 'Compile SQLite CNID scheme', +) option( 'with-cracklib', type: 'boolean',