netatalk.io

Dev Docs CNID Database System

CNID Database System

Overview

The CNID (Catalog Node ID) system provides persistent file and directory identification for Netatalk. AFP clients expect files to maintain stable identity across filesystem operations like renames, moves, and remounts — essential for Classic Mac OS aliases and modern Finder references. CNIDs map each file and directory to a unique 32-bit integer that persists independently of the underlying inode or pathname.

Key Constants

Defined in include/atalk/cnid.h:

#define CNID_INVALID   0        /* Not a valid CNID */
#define CNID_START     17       /* First assignable ID (1–16 reserved by AFP spec) */

#define CNID_ERR_PARAM 0x80000001   /* Bad parameter */
#define CNID_ERR_PATH  0x80000002   /* Path too long or inaccessible */
#define CNID_ERR_DB    0x80000003   /* Database error */
#define CNID_ERR_CLOSE 0x80000004   /* Database was not open */
#define CNID_ERR_MAX   0x80000005   /* CNID space exhausted */

Architecture Summary

The CNID system is built on a pluggable dispatch architecture. Three backends are available:

Backend Module Name Storage Daemon Required Notes
SQLite sqlite Embedded .sqlite file None (in-process) Default
MySQL mysql MySQL server None (in-process) Highest performance, Setup required
BDB (Berkeley DB) dbd On-disk BDB files cnid_metadcnid_dbd Most mature, slowest
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
    subgraph "AFP Operations"
        A["cnid_add()"]:::blue
        B["cnid_get()"]:::blue
        C["cnid_lookup()"]:::blue
        D["cnid_resolve()"]:::blue
        E["cnid_update()"]:::blue
        F["cnid_delete()"]:::blue
        G["cnid_find()"]:::blue
    end

    subgraph "Dispatch Layer"
        H["cnid_db function pointers"]:::purple
        I["cnid_init() / cnid_register()"]:::purple
    end

    subgraph "Backends"
        K["sqlite — Embedded SQLite (default)"]:::yellow
        L["mysql — Remote MySQL"]:::salmon
        J["dbd — BDB via cnid_dbd daemon"]:::green
    end

    A & B & C & D & E & F & G --> H
    I --> K & L & J
    H --> K & L & J

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
    classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10

CNID Dispatch Layer

Module Registration

Backend registration uses a linked list of _cnid_module entries. At startup, cnid_init() in libatalk/cnid/cnid_init.c registers all compile-time-enabled backends via cnid_register():

struct _cnid_module {
    char *name;                                              /* "dbd", "sqlite", "mysql" */
    struct list_head db_list;                                 /* Bidirectional linked list */
    struct _cnid_db *(*cnid_open)(struct cnid_open_args *args); /* Backend open function */
    uint32_t flags;                                          /* Capability flags */
};

Each backend defines a static module instance: - SQLite: cnid_sqlite_module in libatalk/cnid/sqlite/cnid_sqlite.c{ "sqlite", {NULL,NULL}, cnid_sqlite_open, 0 } - MySQL: cnid_mysql_module in libatalk/cnid/mysql/cnid_mysql.c{ "mysql", {NULL,NULL}, cnid_mysql_open, 0 } - BDB: cnid_dbd_module in libatalk/cnid/dbd/cnid_dbd.c{ "dbd", {NULL,NULL}, cnid_dbd_open, 0 }

Conditional compilation guards in cnid_init():

void cnid_init(void)
{
#ifdef CNID_BACKEND_DBD
    cnid_register(&cnid_dbd_module);
#endif
#ifdef CNID_BACKEND_MYSQL
    cnid_register(&cnid_mysql_module);
#endif
#ifdef CNID_BACKEND_SQLITE
    cnid_register(&cnid_sqlite_module);
#endif
}

The cnid_db Abstraction

Every open volume gets a cnid_db instance (defined in include/atalk/cnid.h) populated by the backend’s cnid_open function. Operations dispatch through function pointers:

typedef struct _cnid_db {
    uint32_t      cnid_db_flags;       /* CNID_FLAG_PERSISTENT | CNID_FLAG_LAZY_INIT etc. */
    struct vol   *cnid_db_vol;         /* Associated volume */
    void         *cnid_db_private;     /* Backend-specific state */

    /* Operation function pointers */
    cnid_t (*cnid_add)(struct _cnid_db *cdb, const struct stat *st,
                       cnid_t did, const char *name, size_t, cnid_t hint);
    int    (*cnid_delete)(struct _cnid_db *cdb, cnid_t id);
    cnid_t (*cnid_get)(struct _cnid_db *cdb, cnid_t did, const char *name, size_t);
    cnid_t (*cnid_lookup)(struct _cnid_db *cdb, const struct stat *st,
                          cnid_t did, const char *name, size_t);
    cnid_t (*cnid_nextid)(struct _cnid_db *cdb);
    char  *(*cnid_resolve)(struct _cnid_db *cdb, cnid_t *id, void *buffer, size_t len);
    int    (*cnid_update)(struct _cnid_db *cdb, cnid_t id, const struct stat *st,
                          cnid_t did, const char *name, size_t len);
    void   (*cnid_close)(struct _cnid_db *cdb);
    int    (*cnid_getstamp)(struct _cnid_db *cdb, void *buffer, const size_t len);
    cnid_t (*cnid_rebuild_add)(struct _cnid_db *, const struct stat *, cnid_t,
                               const char *, size_t, cnid_t);
    int    (*cnid_find)(struct _cnid_db *cdb, const char *name, size_t namelen,
                        void *buffer, size_t buflen);
    int    (*cnid_wipe)(struct _cnid_db *cdb);
} cnid_db;

Backend Capability Flags

Defined in include/atalk/cnid.h:

Flag Value Meaning
CNID_FLAG_PERSISTENT 0x01 Backend implements DID persistence
CNID_FLAG_MANGLING 0x02 Has name mangling feature
CNID_FLAG_SETUID 0x04 Set db owner to parent folder owner
CNID_FLAG_BLOCK 0x08 Block signals in update
CNID_FLAG_NODEV 0x10 Don’t use device number, only inode
CNID_FLAG_LAZY_INIT 0x20 Supports lazy initialization
CNID_FLAG_INODE 0x80 In cnid_add the inode is authoritative

All three backends set CNID_FLAG_PERSISTENT | CNID_FLAG_LAZY_INIT at open time.

Open Args

Passed from cnid_open() to each backend’s open function via cnid_open_args:

struct cnid_open_args {
    uint32_t cnid_args_flags;
    struct vol *cnid_args_vol;
};

SQLite Backend (Default)

The SQLite backend (libatalk/cnid/sqlite/cnid_sqlite.c) runs in-process within each afpd child, eliminating the daemon overhead. It is the default CNID backend. The private state is CNID_sqlite_private (defined in include/atalk/cnid_sqlite_private.h):

typedef struct CNID_sqlite_private {
    struct vol *vol;
    uint32_t cnid_sqlite_flags;          /* CNID_SQLITE_FLAG_DEPLETED */
    sqlite3 *cnid_sqlite_con;            /* SQLite connection */
    char *cnid_sqlite_voluuid_str;       /* Volume UUID (dashes stripped) */
    cnid_t cnid_sqlite_hint;             /* CNID hint from AppleDouble */
    sqlite3_stmt *cnid_lookup_stmt;      /* Prepared: lookup by did+name OR dev+ino */
    sqlite3_stmt *cnid_add_stmt;         /* Prepared: INSERT without explicit Id */
    sqlite3_stmt *cnid_put_stmt;         /* Prepared: INSERT with explicit Id (hint) */
    sqlite3_stmt *cnid_get_stmt;         /* Prepared: SELECT by did+name */
    sqlite3_stmt *cnid_resolve_stmt;     /* Prepared: SELECT by Id */
    sqlite3_stmt *cnid_delete_stmt;      /* Prepared: DELETE by Id */
    sqlite3_stmt *cnid_getstamp_stmt;    /* Prepared: get stamp from volumes table */
    sqlite3_stmt *cnid_find_stmt;        /* Prepared: search by LIKE pattern */
} CNID_sqlite_private;

Schema

Database location: <v_dbpath>/<vol_localname>.sqlite (or <statedir>CNID/<vol_localname>/<vol_localname>.sqlite). Created with WAL journal mode and PRAGMA synchronous=NORMAL.

volumes table (shared across volumes in the same db file):

CREATE TABLE IF NOT EXISTS volumes (
    VolUUID   CHAR(32) PRIMARY KEY,
    VolPath   TEXT(4096),
    Stamp     BINARY(8),
    Depleted  INT
);
CREATE INDEX IF NOT EXISTS idx_volpath ON volumes(VolPath);

Per-volume CNID table (named by stripped volume UUID):

CREATE TABLE IF NOT EXISTS "<voluuid>" (
    Id       INTEGER PRIMARY KEY AUTOINCREMENT,
    Name     VARCHAR(255) NOT NULL,
    Did      INTEGER NOT NULL,
    DevNo    INTEGER NOT NULL,
    InodeNo  INTEGER NOT NULL,
    UNIQUE (Did, Name),
    UNIQUE (DevNo, InodeNo)
);

The AUTOINCREMENT sequence starts at 16 (reserved CNIDs 1–16 per AFP spec). When the CNID space overflows UINT32_MAX, the backend sets CNID_SQLITE_FLAG_DEPLETED, truncates the table, resets the sequence to 16, and ignores future CNID hints from AppleDouble files.

Advantages and Limitations

Advantages: No daemon processes, simple deployment, WAL mode provides concurrent read access, automatic recovery.

Limitations: File-level locking limits write concurrency to one afpd process at a time (mitigated by sqlite3_busy_timeout of 2000ms). cnid_sqlite_rebuild_add() is not supported — returns CNID_INVALID.


MySQL Backend

The MySQL backend (libatalk/cnid/mysql/cnid_mysql.c) also runs in-process, connecting to a remote MySQL server. It is the preferred backend for multi-server deployments requiring true concurrent access. The private state is CNID_mysql_private (defined in include/atalk/cnid_mysql_private.h):

typedef struct CNID_mysql_private {
    struct vol *vol;
    uint32_t      cnid_mysql_flags;       /* CNID_MYSQL_FLAG_DEPLETED */
    MYSQL        *cnid_mysql_con;         /* MySQL connection */
    char         *cnid_mysql_voluuid_str; /* Volume UUID (dashes stripped) */
    cnid_t        cnid_mysql_hint;        /* CNID hint from AppleDouble */
    MYSQL_STMT   *cnid_lookup_stmt;       /* Prepared: lookup */
    MYSQL_STMT   *cnid_add_stmt;          /* Prepared: INSERT without Id */
    MYSQL_STMT   *cnid_put_stmt;          /* Prepared: INSERT with Id (hint) */
} CNID_mysql_private;

Schema

volumes table:

CREATE TABLE IF NOT EXISTS volumes (
    VolUUID  CHAR(32) PRIMARY KEY,
    VolPath  TEXT(4096),
    Stamp    BINARY(8),
    Depleted INT,
    INDEX(VolPath(64))
);

Per-volume CNID table (named by stripped volume UUID):

CREATE TABLE IF NOT EXISTS `<voluuid>` (
    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;

Connection Management

cnid_mysql_open() configures: - MYSQL_OPT_RECONNECT enabled - 30-second connect/read/write timeouts - utf8mb4 charset for UTF-8 volumes - TCP_NODELAY for TCP connections (reduced latency) - CLIENT_MULTI_STATEMENTS for transactional wipe operations

On CR_SERVER_LOST, prepared statements are automatically re-initialized.

Advantages and Limitations

Advantages: Centralized database for multi-server deployments, true concurrent access, proven durability, network-accessible.

Limitations: External MySQL server dependency, network latency, cnid_mysql_rebuild_add() not supported. Uses raw mysql_query() for get/resolve/delete/find (not prepared statements for those operations).


BDB Backend (Legacy)

The BDB backend uses a two-tier daemon architecture where database access is managed by dedicated per-volume daemon processes, orchestrated by a coordinator daemon. It is the most mature backend but also the slowest due to the inter-process communication overhead.

Key Source Files

Component File
Metadata coordinator etc/cnid_dbd/cnid_metad.c
Per-volume daemon etc/cnid_dbd/main.c
BDB interface layer etc/cnid_dbd/dbif.c, etc/cnid_dbd/dbif.h
IPC communication etc/cnid_dbd/comm.c, etc/cnid_dbd/comm.h
Wire format packing etc/cnid_dbd/pack.c, etc/cnid_dbd/pack.h
Unix socket handling etc/cnid_dbd/usockfd.c
Database parameters etc/cnid_dbd/db_param.c, etc/cnid_dbd/db_param.h
Client-side library libatalk/cnid/dbd/cnid_dbd.c, libatalk/cnid/dbd/cnid_dbd.h

Connection Flow

%%{ init: { 'themeVariables': { 'fontSize': '14px' } } }%%
sequenceDiagram
    participant A as afpd child
    participant M as cnid_metad
    participant D as cnid_dbd
    participant B as Berkeley DB

    Note over A,M: 1. TCP connection to cnid_metad
    A->>M: Connect (vol_name, vol_path, username)
    Note over M,D: 2. Fork cnid_dbd if needed
    M->>D: fork+exec cnid_dbd -p path -t ctrlfd -l clntfd
    Note over M,D: 3. Pass client fd via socketpair
    M->>D: send_fd(control_fd, rqstfd)
    Note over A,D: 4. Direct TCP communication
    A->>D: CNID request (struct cnid_dbd_rqst)
    D->>B: dbif_get / dbif_put / dbif_del
    B-->>D: Result
    D-->>A: CNID reply (struct cnid_dbd_rply)

cnid_metad — Metadata Coordinator

etc/cnid_dbd/cnid_metad.c is the entry point for the metadata coordinator daemon. Key aspects:

Listening: Binds a TCP socket on the address configured by cnid listen in afp.conf (default localhost:4700). Uses tsockfd_create() from etc/cnid_dbd/usockfd.c for socket setup.

Volume tracking: Maintains a static array of struct server entries (up to MAXVOLS = 4096):

struct server {
    char *v_path;       /* Volume path */
    pid_t pid;          /* cnid_dbd PID (0 if not running) */
    time_t tm;          /* Last respawn timestamp */
    unsigned int count; /* Respawn count within TESTTIME window */
    int control_fd;     /* Socketpair fd to child cnid_dbd */
};

Spawn logic: maybe_start_dbd() either passes the new client fd to an existing cnid_dbd via send_fd(), or forks a new cnid_dbd process. Respawn rate limiting prevents crash loops (MAXSPAWN = 3 within TESTTIME = 10 seconds).

Child reaping: SIGCHLD handler sets a flag; the main loop calls waitpid(-1, &status, WNOHANG) and clears the corresponding srv[] slot.

cnid_dbd — Per-Volume Database Daemon

etc/cnid_dbd/main.c implements the per-volume daemon:

  1. Startup: Parses -F (config), -p (volume path), -t (control fd), -l (client fd), -u (username). Loads volume config, opens BDB environment with recovery.

  2. Database open: open_db() acquires an exclusive lock on <dbpath>/.AppleDB/lock, then calls dbif_init() and dbif_env_open() (see etc/cnid_dbd/dbif.h) with DB_CREATE | DB_INIT_LOG | DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_TXN | DB_RECOVER.

  3. Request loop: loop() uses comm_rcv() (in etc/cnid_dbd/comm.c) with pselect() to wait for requests, dispatching to the appropriate dbd_* handler:

switch (rqst.op) {
    case CNID_DBD_OP_ADD:        ret = dbd_add(dbd, &rqst, &rply);        break;
    case CNID_DBD_OP_GET:        ret = dbd_get(dbd, &rqst, &rply);        break;
    case CNID_DBD_OP_RESOLVE:    ret = dbd_resolve(dbd, &rqst, &rply);    break;
    case CNID_DBD_OP_LOOKUP:     ret = dbd_lookup(dbd, &rqst, &rply);     break;
    case CNID_DBD_OP_UPDATE:     ret = dbd_update(dbd, &rqst, &rply);     break;
    case CNID_DBD_OP_DELETE:     ret = dbd_delete(dbd, &rqst, &rply, DBIF_CNID); break;
    case CNID_DBD_OP_GETSTAMP:   ret = dbd_getstamp(dbd, &rqst, &rply);   break;
    case CNID_DBD_OP_REBUILD_ADD:ret = dbd_rebuild_add(dbd, &rqst, &rply); break;
    case CNID_DBD_OP_SEARCH:     ret = dbd_search(dbd, &rqst, &rply);     break;
    case CNID_DBD_OP_WIPE:       ret = reinit_db();                        break;
}
  1. Transaction management: Uses AUTO_COMMIT for reads. Writes trigger a transaction via dbif_txn_begin(). After each request, either dbif_txn_commit() or dbif_txn_abort() is called based on the handler’s return code.

  2. Checkpointing: dbif_txn_checkpoint() is called both on a timer (flush_interval, default 1800s) and after a write count threshold (flush_frequency, default 1000).

  3. Idle timeout: Exits cleanly after idle_timeout seconds (default 600) with no active connections.

Client-Side Library

libatalk/cnid/dbd/cnid_dbd.c implements the afpd-side client that talks to cnid_dbd. The private state is CNID_bdb_private (defined in include/atalk/cnid_bdb_private.h):

typedef struct CNID_bdb_private {
    struct vol *vol;
    int       fd;               /* TCP socket to cnid_dbd */
    char      stamp[ADEDLEN_PRIVSYN]; /* DB timestamp */
    char      *client_stamp;
    size_t    stamp_size;
    int       notfirst;         /* Reconnect indicator */
    int       changed;          /* Stamp changed */
} CNID_bdb_private;

Connection flow: 1. init_tsock() connects to cnid_metad via TCP (vol->v_cnidserver:vol->v_cnidport), sends volume name, path, and username as opening handshake. 2. transmit() handles send/receive with automatic reconnection on failure (up to MAX_DELAY = 20 seconds). 3. dbd_rpc() sends a cnid_dbd_rqst header optionally followed by name data, then reads back a cnid_dbd_rply.

BDB Wire Protocol

Request/Response Format

The wire protocol uses raw struct transmission with optional trailing name data.

Request (struct cnid_dbd_rqst in include/atalk/cnid_bdb_private.h):

Field Type Description
op int Operation code (CNID_DBD_OP_*)
cnid cnid_t CNID (network byte order)
dev dev_t Device number
ino ino_t Inode number
type uint32_t 0=file, 1=directory
did cnid_t Parent directory ID (network byte order)
name const char * Not sent in struct (pointer)
namelen size_t Length of trailing name data

The struct is sent first, then namelen bytes of name data follow.

Response (struct cnid_dbd_rply in include/atalk/cnid_bdb_private.h):

Field Type Description
result int Result code (CNID_DBD_RES_*)
cnid cnid_t Returned CNID
did cnid_t Parent directory ID
name char * Not sent in struct (pointer)
namelen size_t Length of trailing name data

Operation Codes

Defined in include/atalk/cnid_bdb_private.h:

Code Value Description
CNID_DBD_OP_OPEN 0x01 Open (no-op)
CNID_DBD_OP_CLOSE 0x02 Close (no-op)
CNID_DBD_OP_ADD 0x03 Add or lookup CNID
CNID_DBD_OP_GET 0x04 Get CNID by did+name
CNID_DBD_OP_RESOLVE 0x05 Resolve CNID to did+name
CNID_DBD_OP_LOOKUP 0x06 Lookup by dev/ino AND did/name
CNID_DBD_OP_UPDATE 0x07 Update metadata for existing CNID
CNID_DBD_OP_DELETE 0x08 Delete by CNID
CNID_DBD_OP_GETSTAMP 0x0b Get database stamp
CNID_DBD_OP_REBUILD_ADD 0x0c Force-insert with specific CNID
CNID_DBD_OP_SEARCH 0x0d Search by name pattern
CNID_DBD_OP_WIPE 0x0e Delete and recreate database

Result Codes

Code Value Description
CNID_DBD_RES_OK 0x00 Success
CNID_DBD_RES_NOTFOUND 0x01 Entry not found
CNID_DBD_RES_ERR_DB 0x02 Database error
CNID_DBD_RES_ERR_MAX 0x03 CNID space exhausted
CNID_DBD_RES_ERR_DUPLCNID 0x04 Duplicate CNID collision

Data Serialization

pack_cnid_data() in etc/cnid_dbd/pack.c serializes request data into the BDB record format. The packed layout matches include/atalk/cnid_private.h:

Offset Length Field
0 4 CNID
4 8 Device number (big-endian)
12 8 Inode number (big-endian)
20 4 Type (file=0, dir=1, network byte order)
24 4 Parent DID (network byte order)
28 variable Name (NUL-terminated)

Total header: CNID_HEADER_LEN = 28 bytes.

BDB Database Schema

The Berkeley DB environment lives in <volume_dbpath>/.AppleDB/. The DBD handle (defined in etc/cnid_dbd/dbif.h) manages 4 BDB databases defined by DBIF_DB_CNT = 4:

Index Constant File Type Key → Data
0 DBIF_CNID cnid2.db Primary (B-tree) CNID → packed record
1 DBIF_IDX_DEVINO secondary Secondary index dev+ino → CNID
2 DBIF_IDX_DIDNAME secondary Secondary index did+name → CNID
3 DBIF_IDX_NAME secondary Secondary index lowercased name → CNID

Secondary indexes are generated by BDB callback functions in etc/cnid_dbd/pack.c: - devino() — extracts dev+ino bytes from record - didname() — extracts did+name from record - idxname() — lowercases name via charset conversion

Rootinfo Record

A special record with key ROOTINFO_KEY ("\0\0\0\0", defined in include/atalk/cnid_private.h) stores volume metadata in cnid2.db:

Offset Length Content
0 4 Zero (CNID=0)
4 8 DB stamp (st_ctime of database file)
12 8 Unused
20 4 Last used CNID (network byte order)
24 4 Version (network byte order)
28 9 "RootInfo"

CNID version history (from include/atalk/cnid_private.h): - Version 0: up to Netatalk 2.1.x - Version 1: Netatalk 2.2+, added name index for cnid_find

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph LR
    subgraph "cnid2.db (Primary)"
        A["CNID → {cnid, dev, ino, type, did, name}"]:::blue
        R["0x00000000 → Rootinfo<br/>(stamp, last CNID, version)"]:::purple
    end

    subgraph "Secondary Indexes"
        B["DBIF_IDX_DEVINO<br/>dev+ino → CNID"]:::green
        C["DBIF_IDX_DIDNAME<br/>did+name → CNID"]:::green
        D["DBIF_IDX_NAME<br/>lowername → CNID"]:::green
    end

    subgraph "Lookup Patterns"
        E["cnid_resolve:<br/>CNID → did+name"]:::yellow
        F["cnid_get:<br/>did+name → CNID"]:::yellow
        G["cnid_lookup:<br/>dev/ino + did/name"]:::yellow
        H["cnid_find:<br/>name pattern search"]:::yellow
    end

    E --> A
    F --> C
    G --> B & C
    H --> D

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10

BDB Operation Details

dbd_add() (etc/cnid_dbd/dbd_add.c): First calls dbd_lookup() to check for existing entry. If not found, allocates a new CNID via get_cnid() (reads last CNID from rootinfo, increments, checks for collision) and inserts via add_cnid() with DB_NOOVERWRITE.

dbd_lookup() (etc/cnid_dbd/dbd_lookup.c): The most complex operation. Searches both DBIF_IDX_DEVINO and DBIF_IDX_DIDNAME indexes. Handles mismatches (renamed files, reused inodes, emacs-style saves) by deleting stale entries and returning CNID_DBD_RES_NOTFOUND. If the CNID hint from the AppleDouble file matches id_devino, performs an in-place update.

dbd_update() (etc/cnid_dbd/dbd_update.c): Deletes the existing record by CNID, dev/ino, and did/name (via three dbd_delete() calls), then re-inserts with updated metadata.

dbd_resolve() (etc/cnid_dbd/dbd_resolve.c): Simple primary key lookup on DBIF_CNID. Returns the did and full packed record (name at CNID_NAME_OFS).

dbd_check_indexes() (etc/cnid_dbd/dbd_dbcheck.c): Quick consistency check — counts entries in DBIF_CNID, DBIF_IDX_DEVINO, and DBIF_IDX_DIDNAME and verifies they match.


dbd Maintenance Utility

bin/dbd/cmd_dbd.c provides offline database maintenance. It works with all three CNID backends.

Usage

dbd [-cfFstuvV] <path to netatalk volume>
Flag Description
-s Scan mode: read-only, no filesystem modifications
-c Convert from adouble:v2 to adouble:ea
-f Force: delete and recreate CNID database (calls cnid_wipe())
-F Alternate config file path
-t Show statistics while running
-u Username for volumes using $u variable
-v Verbose output
-V Print version

Operation Flags

Defined in bin/dbd/cmd_dbd.h:

#define DBD_FLAGS_SCAN     (1 << 0)   /* Read-only scan */
#define DBD_FLAGS_FORCE    (1 << 1)   /* Wipe and recreate database */
#define DBD_FLAGS_STATS    (1 << 2)   /* Show statistics */
#define DBD_FLAGS_V2TOEA   (1 << 3)   /* Convert adouble:v2 to adouble:ea */
#define DBD_FLAGS_VERBOSE  (1 << 4)   /* Verbose output */

Scan Process

cmd_dbd_scanvol() (in bin/dbd/cmd_dbd_scanvol.c, declared in bin/dbd/cmd_dbd.h) recursively walks the volume filesystem:

  1. Opens the volume’s CNID database via cnid_open()
  2. If -f flag is set, calls cnid_wipe() to delete all entries
  3. Walks every file and directory, calling cnid_lookup() and cnid_add() to ensure CNID records exist and are consistent
  4. Optionally converts adouble metadata format (-c)

The utility must be run as root and validates that the provided path is the actual volume root (not a subdirectory).


Cache Hint Integration

The CNID provides persistent storage of object location information for fast file system path navigation (all users access the same CNID providing centralised coherence).

However, while the CNID is faster than disk, it is slower than memory — so we also maintain in-memory directory caches (per-user). As these are per-user, and the cache is used before CNID, we also need an asynchronous IPC hint notification system to inform other user processes to flush their caches — allowing them to fall back to the CNID or disk.

CNID-modifying operations in afpd (file/directory create, delete, rename, move, permission changes, etc.) also trigger cross-process IPC cache hints to invalidate sibling processes’ directory caches. This mechanism is documented in the dedicated Caching Architecture page.


Configuration

CNID Scheme Selection

In afp.conf, per-volume or global:

[Global]
cnid scheme = sqlite       ; default; or "mysql" or "dbd"
cnid listen = localhost:4700  ; cnid_metad listen address (BDB only)

[MyVolume]
cnid scheme = mysql        ; per-volume override

BDB Database Parameters

Read by db_param_read() from the .AppleDB directory. Defaults defined in etc/cnid_dbd/db_param.h:

Parameter Default Description
logfile_autoremove 1 Auto-remove BDB log files
cachesize 8192 KB (8 MB) BDB memory cache
maxlocks 20000 Maximum lock count
maxlockobjs 20000 Maximum lock object count
flush_frequency 1000 Checkpoint after N writes
flush_interval 1800 Checkpoint interval (seconds)
fd_table_size 512 Max concurrent client connections
idle_timeout 600 Exit after N seconds idle

MySQL Connection Options

Configured in afp.conf:

[Global]
cnid scheme = mysql
cnid mysql host = localhost
cnid mysql user = netatalk
cnid mysql pw = password
cnid mysql db = netatalk_cnid

Footnotes

This is a mirror of the Netatalk GitHub Wiki

Last updated 2026-04-06