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_metad → cnid_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:
-
Startup: Parses
-F(config),-p(volume path),-t(control fd),-l(client fd),-u(username). Loads volume config, opens BDB environment with recovery. -
Database open:
open_db()acquires an exclusive lock on<dbpath>/.AppleDB/lock, then callsdbif_init()anddbif_env_open()(seeetc/cnid_dbd/dbif.h) withDB_CREATE | DB_INIT_LOG | DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_TXN | DB_RECOVER. -
Request loop:
loop()usescomm_rcv()(inetc/cnid_dbd/comm.c) withpselect()to wait for requests, dispatching to the appropriatedbd_*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;
}
-
Transaction management: Uses AUTO_COMMIT for reads. Writes trigger a transaction via
dbif_txn_begin(). After each request, eitherdbif_txn_commit()ordbif_txn_abort()is called based on the handler’s return code. -
Checkpointing:
dbif_txn_checkpoint()is called both on a timer (flush_interval, default 1800s) and after a write count threshold (flush_frequency, default 1000). -
Idle timeout: Exits cleanly after
idle_timeoutseconds (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:
- Opens the volume’s CNID database via
cnid_open() - If
-fflag is set, callscnid_wipe()to delete all entries - Walks every file and directory, calling
cnid_lookup()andcnid_add()to ensure CNID records exist and are consistent - 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