2 * This file is part of DisOrder
3 * Copyright (C) 2005-2008 Richard Kettlewell
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
20 /** @file lib/trackdb.c
21 * @brief Track database
23 * This file is getting in desparate need of splitting up...
32 #include <sys/socket.h>
39 #include <sys/resource.h>
41 #include <arpa/inet.h>
54 #include "configuration.h"
59 #include "trackname.h"
60 #include "trackdb-int.h"
69 #define RESCAN "disorder-rescan"
70 #define DEADLOCK "disorder-deadlock"
72 static const char *getpart(const char *track
,
77 static char **trackdb_new_tid(int *ntracksp
,
80 static int trackdb_expire_noticed_tid(time_t earliest
, DB_TXN
*tid
);
81 static char *normalize_tag(const char *s
, size_t ns
);
83 const struct cache_type cache_files_type
= { 86400 };
84 unsigned long cache_files_hits
, cache_files_misses
;
86 /** @brief Set by trackdb_open() */
87 int trackdb_existing_database
;
89 /* setup and teardown ********************************************************/
91 static const char *home
; /* home had better not change */
92 DB_ENV
*trackdb_env
; /* db environment */
94 /** @brief The tracks database
95 * - Keys are UTF-8(NFC(unicode(path name)))
96 * - Values are encoded key-value pairs
97 * - Data is reconstructable data about tracks that currently exist
101 /** @brief The preferences database
103 * - Keys are UTF-8(NFC(unicode(path name)))
104 * - Values are encoded key-value pairs
105 * - Data is user data about tracks (that might not exist any more)
106 * and cannot be reconstructed
110 /** @brief The search database
112 * - Keys are UTF-8(NFKC(casefold(search term)))
113 * - Values are UTF-8(NFC(unicode(path name)))
114 * - There can be more than one value per key
115 * - Presence of key,value means that path matches the search terms
116 * - Only tracks fond in @ref trackdb_tracksdb are represented here
117 * - This database can be reconstructed, it contains no user data
119 DB
*trackdb_searchdb
;
121 /** @brief The tags database
123 * - Keys are UTF-8(NFKC(casefold(tag)))
124 * - Values are UTF-8(NFC(unicode(path name)))
125 * - There can be more than one value per key
126 * - Presence of key,value means that path matches the tag
127 * - This is always in sync with the tags preference
128 * - This database can be reconstructed, it contains no user data
130 DB
*trackdb_tagsdb
; /* the tags database */
132 /** @brief The global preferences database
133 * - Keys are UTF-8(NFC(preference))
134 * - Values are global preference values
135 * - Data is user data and cannot be reconstructed
137 DB
*trackdb_globaldb
; /* global preferences */
139 /** @brief The noticed database
140 * - Keys are 64-bit big-endian timestamps
141 * - Values are UTF-8(NFC(unicode(path name)))
142 * - There can be more than one value per key
143 * - Presence of key,value means that path was added at the given time
144 * - Data cannot be reconstructed (but isn't THAT important)
146 DB
*trackdb_noticeddb
; /* when track noticed */
148 /** @brief The user database
149 * - Keys are usernames
150 * - Values are encoded key-value pairs
151 * - Data is user data and cannot be reconstructed
155 static pid_t db_deadlock_pid
= -1; /* deadlock manager PID */
156 static pid_t rescan_pid
= -1; /* rescanner PID */
157 static int initialized
, opened
; /* state */
159 /* tracks matched by required_tags */
160 static char **reqtracks
;
161 static size_t nreqtracks
;
163 /* comparison function for keys */
164 static int compare(DB
attribute((unused
)) *db_
,
165 const DBT
*a
, const DBT
*b
) {
166 return compare_path_raw(a
->data
, a
->size
, b
->data
, b
->size
);
169 /** @brief Open database environment
170 * @param flags Flags word
172 * Flags should be one of:
173 * - @ref TRACKDB_NO_RECOVER
174 * - @ref TRACKDB_NORMAL_RECOVER
175 * - @ref TRACKDB_FATAL_RECOVER
176 * - @ref TRACKDB_MAY_CREATE
178 void trackdb_init(int flags
) {
180 const int recover
= flags
& TRACKDB_RECOVER_MASK
;
181 static int recover_type
[] = { 0, DB_RECOVER
, DB_RECOVER_FATAL
};
184 assert(initialized
== 0);
187 if(strcmp(home
, config
->home
))
188 fatal(0, "cannot change db home without server restart");
192 if(flags
& TRACKDB_MAY_CREATE
) {
198 /* Remove world/group permissions on any regular files already in the
199 * database directory. Actually we don't care about all of them but it's
200 * easier to just do the lot. This can be revisited if it's a serious
201 * practical inconvenience for anyone.
203 * The socket, not being a regular file, is excepted.
205 if(!(dp
= opendir(config
->home
)))
206 fatal(errno
, "error reading %s", config
->home
);
207 while((de
= readdir(dp
))) {
208 byte_xasprintf(&p
, "%s/%s", config
->home
, de
->d_name
);
209 if(lstat(p
, &st
) == 0
210 && S_ISREG(st
.st_mode
)
211 && (st
.st_mode
& 077)) {
212 if(chmod(p
, st
.st_mode
& 07700) < 0)
213 fatal(errno
, "cannot chmod %s", p
);
220 /* create environment */
221 if((err
= db_env_create(&trackdb_env
, 0))) fatal(0, "db_env_create: %s",
223 if((err
= trackdb_env
->set_alloc(trackdb_env
,
224 xmalloc_noptr
, xrealloc_noptr
, xfree
)))
225 fatal(0, "trackdb_env->set_alloc: %s", db_strerror(err
));
226 if((err
= trackdb_env
->set_lk_max_locks(trackdb_env
, 10000)))
227 fatal(0, "trackdb_env->set_lk_max_locks: %s", db_strerror(err
));
228 if((err
= trackdb_env
->set_lk_max_objects(trackdb_env
, 10000)))
229 fatal(0, "trackdb_env->set_lk_max_objects: %s", db_strerror(err
));
230 if((err
= trackdb_env
->open(trackdb_env
, config
->home
,
236 |recover_type
[recover
],
238 fatal(0, "trackdb_env->open %s: %s", config
->home
, db_strerror(err
));
239 trackdb_env
->set_errpfx(trackdb_env
, "DB");
240 trackdb_env
->set_errfile(trackdb_env
, stderr
);
241 trackdb_env
->set_verbose(trackdb_env
, DB_VERB_DEADLOCK
, 1);
242 trackdb_env
->set_verbose(trackdb_env
, DB_VERB_RECOVERY
, 1);
243 trackdb_env
->set_verbose(trackdb_env
, DB_VERB_REPLICATION
, 1);
244 D(("initialized database environment"));
247 /* called when deadlock manager terminates */
248 static int reap_db_deadlock(ev_source
attribute((unused
)) *ev
,
249 pid_t
attribute((unused
)) pid
,
251 const struct rusage
attribute((unused
)) *rusage
,
252 void attribute((unused
)) *u
) {
253 db_deadlock_pid
= -1;
255 fatal(0, "deadlock manager unexpectedly terminated: %s",
258 D(("deadlock manager terminated: %s", wstat(status
)));
262 static pid_t
subprogram(ev_source
*ev
, int outputfd
, const char *prog
,
266 const char *args
[1024], **argp
, *a
;
270 *argp
++ = "--config";
271 *argp
++ = configfile
;
272 *argp
++ = debugging ?
"--debug" : "--no-debug";
273 *argp
++ = log_default
== &log_syslog ?
"--syslog" : "--no-syslog";
275 while((a
= va_arg(ap
, const char *)))
279 /* If we're in the background then trap subprocess stdout/stderr */
280 if(!(pid
= xfork())) {
283 ev_signal_atfork(ev
);
284 signal(SIGPIPE
, SIG_DFL
);
289 /* ensure we don't leak privilege anywhere */
290 if(setuid(geteuid()) < 0)
291 fatal(errno
, "error calling setuid");
292 /* If we were negatively niced, undo it. We don't bother checking for
293 * error, it's not that important. */
294 setpriority(PRIO_PROCESS
, 0, 0);
295 execvp(prog
, (char **)args
);
296 fatal(errno
, "error invoking %s", prog
);
301 /* start deadlock manager */
302 void trackdb_master(ev_source
*ev
) {
303 assert(db_deadlock_pid
== -1);
304 db_deadlock_pid
= subprogram(ev
, -1, DEADLOCK
, (char *)0);
305 ev_child(ev
, db_deadlock_pid
, 0, reap_db_deadlock
, 0);
306 D(("started deadlock manager"));
309 /* close environment */
310 void trackdb_deinit(void) {
314 assert(initialized
== 1);
317 /* close the environment */
318 if((err
= trackdb_env
->close(trackdb_env
, 0)))
319 fatal(0, "trackdb_env->close: %s", db_strerror(err
));
321 if(rescan_pid
!= -1) {
322 /* shut down the rescanner */
323 if(kill(rescan_pid
, SIGTERM
) < 0)
324 fatal(errno
, "error killing rescanner");
325 /* wait for the rescanner to finish */
326 while(waitpid(rescan_pid
, &err
, 0) == -1 && errno
== EINTR
)
330 /* TODO kill any stats subprocesses */
332 /* finally terminate the deadlock manager */
333 if(db_deadlock_pid
!= -1 && kill(db_deadlock_pid
, SIGTERM
) < 0)
334 fatal(errno
, "error killing deadlock manager");
335 db_deadlock_pid
= -1;
337 D(("deinitialized database environment"));
340 /* open a specific database */
341 static DB
*open_db(const char *path
,
349 D(("open %s", path
));
350 path
= config_get_file(path
);
351 if((err
= db_create(&db
, trackdb_env
, 0)))
352 fatal(0, "db_create %s: %s", path
, db_strerror(err
));
354 if((err
= db
->set_flags(db
, dbflags
)))
355 fatal(0, "db->set_flags %s: %s", path
, db_strerror(err
));
356 if(dbtype
== DB_BTREE
)
357 if((err
= db
->set_bt_compare(db
, compare
)))
358 fatal(0, "db->set_bt_compare %s: %s", path
, db_strerror(err
));
359 if((err
= db
->open(db
, 0, path
, 0, dbtype
,
360 openflags
| DB_AUTO_COMMIT
, mode
))) {
361 if((openflags
& DB_CREATE
) || errno
!= ENOENT
)
362 fatal(0, "db->open %s: %s", path
, db_strerror(err
));
369 /** @brief Open track databases
370 * @param flags Flags flags word
372 * @p flags should have one of:
373 * - @p TRACKDB_NO_UPGRADE, if no upgrade should be attempted
374 * - @p TRACKDB_CAN_UPGRADE, if an upgrade may be attempted
375 * - @p TRACKDB_OPEN_FOR_UPGRADE, if this is disorder-dbupgrade
377 * - @p TRACKDB_READ_ONLY, read only access
379 void trackdb_open(int flags
) {
382 uint32_t dbflags
= flags
& TRACKDB_READ_ONLY ? DB_RDONLY
: DB_CREATE
;
387 /* check the database version first */
388 trackdb_globaldb
= open_db("global.db", 0, DB_HASH
, DB_RDONLY
, 0666);
389 if(trackdb_globaldb
) {
390 /* This is an existing database */
394 s
= trackdb_get_global("_dbversion");
395 /* Close the database again, we'll open it property below */
396 if((err
= trackdb_globaldb
->close(trackdb_globaldb
, 0)))
397 fatal(0, "error closing global.db: %s", db_strerror(err
));
398 trackdb_globaldb
= 0;
399 /* Convert version string to an integer */
400 oldversion
= s ?
atol(s
) : 1;
401 if(oldversion
> config
->dbversion
) {
402 /* Database is from the future; we never allow this. */
403 fatal(0, "this version of DisOrder is too old for database version %ld",
406 if(oldversion
< config
->dbversion
) {
407 /* Database version is out of date */
408 switch(flags
& TRACKDB_UPGRADE_MASK
) {
409 case TRACKDB_NO_UPGRADE
:
410 /* This database needs upgrading but this is not permitted */
411 fatal(0, "database needs upgrading from %ld to %ld",
412 oldversion
, config
->dbversion
);
413 case TRACKDB_CAN_UPGRADE
:
414 /* This database needs upgrading */
415 info("invoking disorder-dbupgrade to upgrade from %ld to %ld",
416 oldversion
, config
->dbversion
);
417 pid
= subprogram(0, -1, "disorder-dbupgrade", (char *)0);
418 while(waitpid(pid
, &err
, 0) == -1 && errno
== EINTR
)
421 fatal(0, "disorder-dbupgrade %s", wstat(err
));
422 info("disorder-dbupgrade succeeded");
424 case TRACKDB_OPEN_FOR_UPGRADE
:
430 if(oldversion
== config
->dbversion
&& (flags
& TRACKDB_OPEN_FOR_UPGRADE
)) {
431 /* This doesn't make any sense */
432 fatal(0, "database is already at current version");
434 trackdb_existing_database
= 1;
436 if(flags
& TRACKDB_OPEN_FOR_UPGRADE
) {
437 /* Cannot upgrade a new database */
438 fatal(0, "cannot upgrade a database that does not exist");
440 /* This is a brand new database */
441 trackdb_existing_database
= 0;
443 /* open the databases */
444 trackdb_tracksdb
= open_db("tracks.db",
445 DB_RECNUM
, DB_BTREE
, dbflags
, 0666);
446 trackdb_searchdb
= open_db("search.db",
447 DB_DUP
|DB_DUPSORT
, DB_HASH
, dbflags
, 0666);
448 trackdb_tagsdb
= open_db("tags.db",
449 DB_DUP
|DB_DUPSORT
, DB_HASH
, dbflags
, 0666);
450 trackdb_prefsdb
= open_db("prefs.db", 0, DB_HASH
, dbflags
, 0666);
451 trackdb_globaldb
= open_db("global.db", 0, DB_HASH
, dbflags
, 0666);
452 trackdb_noticeddb
= open_db("noticed.db",
453 DB_DUPSORT
, DB_BTREE
, dbflags
, 0666);
454 trackdb_usersdb
= open_db("users.db",
455 0, DB_HASH
, dbflags
, 0600);
456 if(!trackdb_existing_database
) {
457 /* Stash the database version */
460 assert(!(flags
& TRACKDB_OPEN_FOR_UPGRADE
));
461 snprintf(buf
, sizeof buf
, "%ld", config
->dbversion
);
462 trackdb_set_global("_dbversion", buf
, 0);
464 D(("opened databases"));
467 /* close track databases */
468 void trackdb_close(void) {
474 if((err
= trackdb_tracksdb
->close(trackdb_tracksdb
, 0)))
475 fatal(0, "error closing tracks.db: %s", db_strerror(err
));
476 if((err
= trackdb_searchdb
->close(trackdb_searchdb
, 0)))
477 fatal(0, "error closing search.db: %s", db_strerror(err
));
478 if((err
= trackdb_tagsdb
->close(trackdb_tagsdb
, 0)))
479 fatal(0, "error closing tags.db: %s", db_strerror(err
));
480 if((err
= trackdb_prefsdb
->close(trackdb_prefsdb
, 0)))
481 fatal(0, "error closing prefs.db: %s", db_strerror(err
));
482 if((err
= trackdb_globaldb
->close(trackdb_globaldb
, 0)))
483 fatal(0, "error closing global.db: %s", db_strerror(err
));
484 if((err
= trackdb_noticeddb
->close(trackdb_noticeddb
, 0)))
485 fatal(0, "error closing noticed.db: %s", db_strerror(err
));
486 if((err
= trackdb_usersdb
->close(trackdb_usersdb
, 0)))
487 fatal(0, "error closing users.db: %s", db_strerror(err
));
488 trackdb_tracksdb
= trackdb_searchdb
= trackdb_prefsdb
= 0;
489 trackdb_tagsdb
= trackdb_globaldb
= 0;
490 D(("closed databases"));
493 /* generic db routines *******************************************************/
495 /* fetch and decode a database entry. Returns 0, DB_NOTFOUND or
496 * DB_LOCK_DEADLOCK. */
497 int trackdb_getdata(DB
*db
,
504 switch(err
= db
->get(db
, tid
, make_key(&key
, track
),
505 prepare_data(&data
), 0)) {
507 *kp
= kvp_urldecode(data
.data
, data
.size
);
512 case DB_LOCK_DEADLOCK
:
513 error(0, "error querying database: %s", db_strerror(err
));
516 fatal(0, "error querying database: %s", db_strerror(err
));
520 /* encode and store a database entry. Returns 0, DB_KEYEXIST or
521 * DB_LOCK_DEADLOCK. */
522 int trackdb_putdata(DB
*db
,
530 switch(err
= db
->put(db
, tid
, make_key(&key
, track
),
531 encode_data(&data
, k
), flags
)) {
535 case DB_LOCK_DEADLOCK
:
536 error(0, "error updating database: %s", db_strerror(err
));
539 fatal(0, "error updating database: %s", db_strerror(err
));
543 /** @brief Delete a database entry
545 * @param track Key to delete
546 * @param tid Transaction ID
547 * @return 0, DB_NOTFOUND or DB_LOCK_DEADLOCK
549 int trackdb_delkey(DB
*db
,
555 switch(err
= db
->del(db
, tid
, make_key(&key
, track
), 0)) {
559 case DB_LOCK_DEADLOCK
:
560 error(0, "error updating database: %s", db_strerror(err
));
563 fatal(0, "error updating database: %s", db_strerror(err
));
567 /* open a database cursor */
568 DBC
*trackdb_opencursor(DB
*db
, DB_TXN
*tid
) {
572 switch(err
= db
->cursor(db
, tid
, &c
, 0)) {
574 default: fatal(0, "error creating cursor: %s", db_strerror(err
));
579 /* close a database cursor; returns 0 or DB_LOCK_DEADLOCK */
580 int trackdb_closecursor(DBC
*c
) {
584 switch(err
= c
->c_close(c
)) {
587 case DB_LOCK_DEADLOCK
:
588 error(0, "error closing cursor: %s", db_strerror(err
));
591 fatal(0, "error closing cursor: %s", db_strerror(err
));
595 /* delete a (key,data) pair. Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
596 int trackdb_delkeydata(DB
*db
,
604 c
= trackdb_opencursor(db
, tid
);
605 switch(err
= c
->c_get(c
, make_key(&key
, word
),
606 make_key(&data
, track
), DB_GET_BOTH
)) {
608 switch(err
= c
->c_del(c
, 0)) {
614 case DB_LOCK_DEADLOCK
:
615 error(0, "error updating database: %s", db_strerror(err
));
618 fatal(0, "c->c_del: %s", db_strerror(err
));
623 case DB_LOCK_DEADLOCK
:
624 error(0, "error updating database: %s", db_strerror(err
));
627 fatal(0, "c->c_get: %s", db_strerror(err
));
629 if(trackdb_closecursor(c
)) err
= DB_LOCK_DEADLOCK
;
633 /* start a transaction */
634 DB_TXN
*trackdb_begin_transaction(void) {
638 if((err
= trackdb_env
->txn_begin(trackdb_env
, 0, &tid
, 0)))
639 fatal(0, "trackdb_env->txn_begin: %s", db_strerror(err
));
643 /* abort transaction */
644 void trackdb_abort_transaction(DB_TXN
*tid
) {
648 if((err
= tid
->abort(tid
)))
649 fatal(0, "tid->abort: %s", db_strerror(err
));
652 /* commit transaction */
653 void trackdb_commit_transaction(DB_TXN
*tid
) {
656 if((err
= tid
->commit(tid
, 0)))
657 fatal(0, "tid->commit: %s", db_strerror(err
));
660 /* search/tags shared code ***************************************************/
662 /* comparison function used by dedupe() */
663 static int wordcmp(const void *a
, const void *b
) {
664 return strcmp(*(const char **)a
, *(const char **)b
);
667 /* sort and de-dupe VEC */
668 static char **dedupe(char **vec
, int nvec
) {
671 qsort(vec
, nvec
, sizeof (char *), wordcmp
);
675 for(n
= 1; n
< nvec
; ++n
)
676 if(strcmp(vec
[n
], vec
[m
- 1]))
683 /* update a key/track database. Returns 0 or DB_DEADLOCK. */
684 static int register_word(DB
*db
, const char *what
,
685 const char *track
, const char *word
,
690 switch(err
= db
->put(db
, tid
, make_key(&key
, word
),
691 make_key(&data
, track
), DB_NODUPDATA
)) {
695 case DB_LOCK_DEADLOCK
:
696 error(0, "error updating %s.db: %s", what
, db_strerror(err
));
699 fatal(0, "error updating %s.db: %s", what
, db_strerror(err
));
703 /* search primitives *********************************************************/
705 /* return true iff NAME is a trackname_display_ pref */
706 static int is_display_pref(const char *name
) {
707 static const char prefix
[] = "trackname_display_";
708 return !strncmp(name
, prefix
, (sizeof prefix
) - 1);
711 /** @brief Word_Break property tailor that treats underscores as spaces */
712 static int tailor_underscore_Word_Break_Other(uint32_t c
) {
716 case 0x005F: /* LOW LINE (SPACING UNDERSCORE) */
717 return unicode_Word_Break_Other
;
721 /** @brief Remove all combining characters in-place
722 * @param s Pointer to start of string
723 * @param ns Length of string
724 * @return New, possiblby reduced, length
726 static size_t remove_combining_chars(uint32_t *s
, size_t ns
) {
727 uint32_t *start
= s
, *t
= s
, *end
= s
+ ns
;
730 const uint32_t c
= *s
++;
731 if(!utf32_combining_class(c
))
737 /** @brief Normalize and split a string using a given tailoring */
738 static void word_split(struct vector
*v
,
740 unicode_property_tailor
*pt
) {
742 uint32_t *t32
, **w32
;
744 /* Convert to UTF-32 */
745 if(!(t32
= utf8_to_utf32(s
, strlen(s
), &nt32
)))
747 /* Erase case distinctions */
748 if(!(t32
= utf32_casefold_compat(t32
, nt32
, &nt32
)))
750 /* Drop combining characters */
751 nt32
= remove_combining_chars(t32
, nt32
);
752 /* Split into words, treating _ as a space */
753 w32
= utf32_word_split(t32
, nt32
, &nw
, pt
);
754 /* Convert words back to UTF-8 and append to result */
755 for(i
= 0; i
< nw
; ++i
)
756 vector_append(v
, utf32_to_utf8(w32
[i
], utf32_len(w32
[i
]), 0));
759 /** @brief Normalize a tag
761 * @param ns Length of tag
762 * @return Normalized string or NULL on error
764 * The return value will be:
766 * - have no leading or trailing space
767 * - have no combining characters
768 * - all spacing between words will be a single U+0020 SPACE
770 static char *normalize_tag(const char *s
, size_t ns
) {
771 uint32_t *s32
, **w32
;
772 size_t ns32
, nw32
, i
;
775 if(!(s32
= utf8_to_utf32(s
, ns
, &ns32
)))
777 if(!(s32
= utf32_casefold_compat(s32
, ns32
, &ns32
))) /* ->NFKD */
779 ns32
= remove_combining_chars(s32
, ns32
);
780 /* Split into words, no Word_Break tailoring */
781 w32
= utf32_word_split(s32
, ns32
, &nw32
, 0);
782 /* Compose back into a string */
784 for(i
= 0; i
< nw32
; ++i
) {
786 dynstr_append(d
, ' ');
787 dynstr_append_string(d
, utf32_to_utf8(w32
[i
], utf32_len(w32
[i
]), 0));
793 /* compute the words of a track name */
794 static char **track_to_words(const char *track
,
795 const struct kvp
*p
) {
797 const char *rootless
= track_rootless(track
);
800 rootless
= track
; /* bodge */
802 rootless
= strip_extension(rootless
);
803 word_split(&v
, strip_extension(rootless
), tailor_underscore_Word_Break_Other
);
804 for(; p
; p
= p
->next
)
805 if(is_display_pref(p
->name
))
806 word_split(&v
, p
->value
, 0);
807 vector_terminate(&v
);
808 return dedupe(v
.vec
, v
.nvec
);
811 /* return nonzero iff WORD is a stopword */
812 static int stopword(const char *word
) {
815 for(n
= 0; n
< config
->stopword
.n
816 && strcmp(word
, config
->stopword
.s
[n
]); ++n
)
818 return n
< config
->stopword
.n
;
821 /* record that WORD appears in TRACK. Returns 0 or DB_LOCK_DEADLOCK. */
822 static int register_search_word(const char *track
, const char *word
,
824 if(stopword(word
)) return 0;
825 return register_word(trackdb_searchdb
, "search", track
, word
, tid
);
828 /* Tags **********************************************************************/
830 /* Return nonzero if C is a valid tag character */
831 static int tagchar(int c
) {
840 /* Parse and de-dupe a tag list. If S=0 then assumes "". */
841 static char **parsetags(const char *s
) {
847 /* skip initial separators */
848 while(*s
&& (!tagchar(*s
) || *s
== ' '))
851 /* find the extent of the tag */
853 while(*s
&& tagchar(*s
))
855 /* strip trailing spaces */
856 while(s
> t
&& s
[-1] == ' ')
858 /* add tag to list */
859 vector_append(&v
, normalize_tag(t
, (size_t)(s
- t
)));
860 /* skip intermediate and trailing separators */
861 while(*s
&& (!tagchar(*s
) || *s
== ' '))
865 vector_terminate(&v
);
866 return dedupe(v
.vec
, v
.nvec
);
869 /* Record that TRACK has TAG. Returns 0 or DB_LOCK_DEADLOCK. */
870 static int register_tag(const char *track
, const char *tag
, DB_TXN
*tid
) {
871 return register_word(trackdb_tagsdb
, "tags", track
, tag
, tid
);
874 /* aliases *******************************************************************/
876 /* compute the alias and store at aliasp. Returns 0 or DB_LOCK_DEADLOCK. If
877 * there is no alias sets *aliasp to 0. */
878 static int compute_alias(char **aliasp
,
883 const char *s
= config
->alias
, *t
, *expansion
, *part
;
884 int c
, used_db
= 0, slash_prefix
, err
;
886 const char *const root
= find_track_root(track
);
889 /* Bodge for tracks with no root */
894 dynstr_append_string(&d
, root
);
895 while((c
= (unsigned char)*s
++)) {
897 dynstr_append(&d
, c
);
900 if((slash_prefix
= (*s
== '/')))
903 assert(t
!= 0); /* validated at startup */
904 part
= xstrndup(s
, t
- s
);
905 expansion
= getpart(track
, "display", part
, p
, &used_db
);
907 if(slash_prefix
) dynstr_append(&d
, '/');
908 dynstr_append_string(&d
, expansion
);
910 s
= t
+ 1; /* skip {part} */
912 /* only admit to the alias if we used the db... */
917 dynstr_terminate(&d
);
918 /* ...and the answer differs from the original... */
919 if(!strcmp(track
, d
.vec
)) {
923 /* ...and there isn't already a different track with that name (including as
925 switch(err
= trackdb_getdata(trackdb_tracksdb
, d
.vec
, &at
, tid
)) {
927 if((s
= kvp_get(at
, "_alias_for"))
928 && !strcmp(s
, track
)) {
940 /* get track and prefs data (if tp/pp not null pointers). Returns 0 on
941 * success, DB_NOTFOUND if the track does not exist or DB_LOCK_DEADLOCK.
942 * Always sets the return values, even if only to null pointers. */
943 static int gettrackdata(const char *track
,
946 const char **actualp
,
948 #define GTD_NOALIAS 0x0001
951 const char *actual
= track
;
952 struct kvp
*t
= 0, *p
= 0;
954 if((err
= trackdb_getdata(trackdb_tracksdb
, track
, &t
, tid
))) goto done
;
955 if((actual
= kvp_get(t
, "_alias_for"))) {
956 if(flags
& GTD_NOALIAS
) {
957 error(0, "alias passed to gettrackdata where real path required");
960 if((err
= trackdb_getdata(trackdb_tracksdb
, actual
, &t
, tid
))) goto done
;
965 if((err
= trackdb_getdata(trackdb_prefsdb
, actual
, &p
, tid
)) == DB_LOCK_DEADLOCK
)
970 if(actualp
) *actualp
= actual
;
976 /* trackdb_notice() **********************************************************/
978 /** @brief notice a possibly new track
979 * @return @c DB_NOTFOUND if new, 0 if already known
981 int trackdb_notice(const char *track
,
987 tid
= trackdb_begin_transaction();
988 err
= trackdb_notice_tid(track
, path
, tid
);
989 if((err
== DB_LOCK_DEADLOCK
)) goto fail
;
992 trackdb_abort_transaction(tid
);
994 trackdb_commit_transaction(tid
);
998 /** @brief notice a possibly new track
999 * @param track NFC UTF-8 track name
1000 * @param path Raw path name
1001 * @param tid Transaction ID
1002 * @return @c DB_NOTFOUND if new, 0 if already known, @c DB_LOCK_DEADLOCK also
1004 int trackdb_notice_tid(const char *track
,
1008 struct kvp
*t
, *a
, *p
;
1012 /* notice whether the tracks.db entry changes */
1014 /* get any existing tracks entry */
1015 if((err
= gettrackdata(track
, &t
, &p
, 0, 0, tid
)) == DB_LOCK_DEADLOCK
)
1017 ret
= err
; /* 0 or DB_NOTFOUND */
1018 /* this is a real track */
1019 t_changed
+= kvp_set(&t
, "_alias_for", 0);
1020 t_changed
+= kvp_set(&t
, "_path", path
);
1021 /* if we have an alias record it in the database */
1022 if((err
= compute_alias(&alias
, track
, p
, tid
))) return err
;
1024 /* won't overwrite someone else's alias as compute_alias() checks */
1025 D(("%s: alias %s", track
, alias
));
1027 kvp_set(&a
, "_alias_for", track
);
1028 if((err
= trackdb_putdata(trackdb_tracksdb
, alias
, a
, tid
, 0))) return err
;
1030 /* update search.db */
1031 w
= track_to_words(track
, p
);
1032 for(n
= 0; w
[n
]; ++n
)
1033 if((err
= register_search_word(track
, w
[n
], tid
)))
1035 /* update tags.db */
1036 w
= parsetags(kvp_get(p
, "tags"));
1037 for(n
= 0; w
[n
]; ++n
)
1038 if((err
= register_tag(track
, w
[n
], tid
)))
1041 /* only store the tracks.db entry if it has changed */
1042 if(t_changed
&& (err
= trackdb_putdata(trackdb_tracksdb
, track
, t
, tid
, 0)))
1044 if(ret
== DB_NOTFOUND
) {
1045 uint32_t timestamp
[2];
1050 timestamp
[0] = htonl((uint64_t)now
>> 32);
1051 timestamp
[1] = htonl((uint32_t)now
);
1052 memset(&key
, 0, sizeof key
);
1053 key
.data
= timestamp
;
1054 key
.size
= sizeof timestamp
;
1055 switch(err
= trackdb_noticeddb
->put(trackdb_noticeddb
, tid
, &key
,
1056 make_key(&data
, track
), 0)) {
1058 case DB_LOCK_DEADLOCK
: return err
;
1059 default: fatal(0, "error updating noticed.db: %s", db_strerror(err
));
1065 /* trackdb_obsolete() ********************************************************/
1067 /* obsolete a track */
1068 int trackdb_obsolete(const char *track
, DB_TXN
*tid
) {
1073 if((err
= gettrackdata(track
, 0, &p
, 0,
1074 GTD_NOALIAS
, tid
)) == DB_LOCK_DEADLOCK
)
1076 else if(err
== DB_NOTFOUND
) return 0;
1077 /* compute the alias, if any, and delete it */
1078 if((err
= compute_alias(&alias
, track
, p
, tid
))) return err
;
1080 /* if the alias points to some other track then compute_alias won't
1082 if((err
= trackdb_delkey(trackdb_tracksdb
, alias
, tid
))
1083 && err
!= DB_NOTFOUND
)
1086 /* update search.db */
1087 w
= track_to_words(track
, p
);
1088 for(n
= 0; w
[n
]; ++n
)
1089 if(trackdb_delkeydata(trackdb_searchdb
,
1090 w
[n
], track
, tid
) == DB_LOCK_DEADLOCK
)
1092 /* update tags.db */
1093 w
= parsetags(kvp_get(p
, "tags"));
1094 for(n
= 0; w
[n
]; ++n
)
1095 if(trackdb_delkeydata(trackdb_tagsdb
,
1096 w
[n
], track
, tid
) == DB_LOCK_DEADLOCK
)
1099 /* update tracks.db */
1100 if(trackdb_delkey(trackdb_tracksdb
, track
, tid
) == DB_LOCK_DEADLOCK
)
1102 /* We don't delete the prefs, so they survive temporary outages of the
1103 * (possibly virtual) track filesystem */
1107 /* trackdb_stats() ***********************************************************/
1109 #define H(name) { #name, offsetof(DB_HASH_STAT, name) }
1110 #define B(name) { #name, offsetof(DB_BTREE_STAT, name) }
1112 static const struct statinfo
{
1115 } statinfo_hash
[] = {
1131 }, statinfo_btree
[] = {
1152 /* look up stats for DB */
1153 static int get_stats(struct vector
*v
,
1155 const struct statinfo
*si
,
1164 switch(err
= database
->stat(database
, tid
, &sp
, 0)) {
1167 case DB_LOCK_DEADLOCK
:
1168 error(0, "error querying database: %s", db_strerror(err
));
1171 fatal(0, "error querying database: %s", db_strerror(err
));
1173 for(n
= 0; n
< nsi
; ++n
) {
1174 byte_xasprintf(&str
, "%s=%"PRIuMAX
, si
[n
].name
,
1175 (uintmax_t)*(u_int32_t
*)((char *)sp
+ si
[n
].offset
));
1176 vector_append(v
, str
);
1182 /** @brief One entry in the search league */
1183 struct search_entry
{
1188 /** @brief Add a word to the search league
1189 * @param se Pointer to search league
1190 * @param count Maximum size for search league
1191 * @param nse Current size of search league
1192 * @param word New word, or NULL
1193 * @param n How often @p word appears
1194 * @return New size of search league
1196 static int register_search_entry(struct search_entry
*se
,
1203 if(word
&& (nse
< count
|| n
> se
[nse
- 1].n
)) {
1204 /* Find the starting point */
1209 /* Find the insertion point */
1210 while(i
> 0 && n
> se
[i
- 1].n
)
1212 memmove(&se
[i
+ 1], &se
[i
], (nse
- i
- 1) * sizeof *se
);
1219 /* find the top COUNT words in the search database */
1220 static int search_league(struct vector
*v
, int count
, DB_TXN
*tid
) {
1221 struct search_entry
*se
;
1224 int err
, n
= 0, nse
= 0, i
;
1229 cursor
= trackdb_opencursor(trackdb_searchdb
, tid
);
1230 se
= xmalloc(count
* sizeof *se
);
1231 /* Walk across the whole database counting up the number of times each
1233 while(!(err
= cursor
->c_get(cursor
, prepare_data(&k
), prepare_data(&d
),
1235 if(word
&& wl
== k
.size
&& !strncmp(word
, k
.data
, wl
))
1236 ++n
; /* same word again */
1238 nse
= register_search_entry(se
, count
, nse
, word
, n
);
1239 word
= xstrndup(k
.data
, wl
= k
.size
);
1247 case DB_LOCK_DEADLOCK
:
1248 error(0, "error querying search database: %s", db_strerror(err
));
1251 fatal(0, "error querying search database: %s", db_strerror(err
));
1253 if(trackdb_closecursor(cursor
)) err
= DB_LOCK_DEADLOCK
;
1255 nse
= register_search_entry(se
, count
, nse
, word
, n
);
1256 byte_xasprintf(&str
, "Top %d search words:", nse
);
1257 vector_append(v
, str
);
1258 for(i
= 0; i
< nse
; ++i
) {
1259 byte_xasprintf(&str
, "%4d: %5d %s", i
+ 1, se
[i
].n
, se
[i
].word
);
1260 vector_append(v
, str
);
1265 #define SI(what) statinfo_##what, \
1266 sizeof statinfo_##what / sizeof (struct statinfo)
1268 /* return a list of database stats */
1269 char **trackdb_stats(int *nstatsp
) {
1275 tid
= trackdb_begin_transaction();
1277 vector_append(&v
, (char *)"Tracks database stats:");
1278 if(get_stats(&v
, trackdb_tracksdb
, SI(btree
), tid
)) goto fail
;
1279 vector_append(&v
, (char *)"");
1280 vector_append(&v
, (char *)"Search database stats:");
1281 if(get_stats(&v
, trackdb_searchdb
, SI(hash
), tid
)) goto fail
;
1282 vector_append(&v
, (char *)"");
1283 vector_append(&v
, (char *)"Prefs database stats:");
1284 if(get_stats(&v
, trackdb_prefsdb
, SI(hash
), tid
)) goto fail
;
1285 vector_append(&v
, (char *)"");
1286 if(search_league(&v
, 10, tid
)) goto fail
;
1287 vector_terminate(&v
);
1290 trackdb_abort_transaction(tid
);
1292 trackdb_commit_transaction(tid
);
1293 if(nstatsp
) *nstatsp
= v
.nvec
;
1297 struct stats_details
{
1298 void (*done
)(char *data
, void *u
);
1300 int exited
; /* subprocess exited */
1301 int closed
; /* pipe close */
1302 int wstat
; /* wait status from subprocess */
1303 struct dynstr data
[1]; /* data read from pipe */
1306 static void stats_complete(struct stats_details
*d
) {
1309 if(!(d
->exited
&& d
->closed
))
1311 byte_xasprintf(&s
, "\n"
1313 "track lookup cache hits: %lu\n"
1314 "track lookup cache misses: %lu\n",
1316 cache_files_misses
);
1317 dynstr_append_string(d
->data
, s
);
1318 dynstr_terminate(d
->data
);
1319 d
->done(d
->data
->vec
, d
->u
);
1322 static int stats_finished(ev_source
attribute((unused
)) *ev
,
1323 pid_t
attribute((unused
)) pid
,
1325 const struct rusage
attribute((unused
)) *rusage
,
1327 struct stats_details
*const d
= u
;
1331 error(0, "disorder-stats %s", wstat(status
));
1336 static int stats_read(ev_source
attribute((unused
)) *ev
,
1342 struct stats_details
*const d
= u
;
1344 dynstr_append_bytes(d
->data
, ptr
, bytes
);
1345 ev_reader_consume(reader
, bytes
);
1352 static int stats_error(ev_source
attribute((unused
)) *ev
,
1355 struct stats_details
*const d
= u
;
1357 error(errno_value
, "error reading from pipe to disorder-stats");
1363 void trackdb_stats_subprocess(ev_source
*ev
,
1364 void (*done
)(char *data
, void *u
),
1368 struct stats_details
*d
= xmalloc(sizeof *d
);
1370 dynstr_init(d
->data
);
1374 pid
= subprogram(ev
, p
[1], "disorder-stats", (char *)0);
1376 ev_child(ev
, pid
, 0, stats_finished
, d
);
1377 ev_reader_new(ev
, p
[0], stats_read
, stats_error
, d
, "disorder-stats reader");
1380 /* set a pref (remove if value=0) */
1381 int trackdb_set(const char *track
,
1383 const char *value
) {
1384 struct kvp
*t
, *p
, *a
;
1387 char *oldalias
, *newalias
, **oldtags
= 0, **newtags
;
1390 /* TODO: if value matches default then set value=0 */
1394 tid
= trackdb_begin_transaction();
1395 if((err
= gettrackdata(track
, &t
, &p
, 0,
1396 0, tid
)) == DB_LOCK_DEADLOCK
)
1398 if(err
== DB_NOTFOUND
) break;
1399 if(name
[0] == '_') {
1400 if(kvp_set(&t
, name
, value
))
1401 if(trackdb_putdata(trackdb_tracksdb
, track
, t
, tid
, 0))
1404 /* get the old alias name */
1405 if(compute_alias(&oldalias
, track
, p
, tid
)) goto fail
;
1406 /* get the old tags */
1407 if(!strcmp(name
, "tags"))
1408 oldtags
= parsetags(kvp_get(p
, "tags"));
1410 if(kvp_set(&p
, name
, value
))
1411 if(trackdb_putdata(trackdb_prefsdb
, track
, p
, tid
, 0))
1413 /* compute the new alias name */
1414 if((err
= compute_alias(&newalias
, track
, p
, tid
))) goto fail
;
1415 /* check whether alias has changed */
1416 if(!(oldalias
== newalias
1417 || (oldalias
&& newalias
&& !strcmp(oldalias
, newalias
)))) {
1418 /* adjust alias records to fit change */
1420 && trackdb_delkey(trackdb_tracksdb
, oldalias
, tid
) == DB_LOCK_DEADLOCK
)
1424 kvp_set(&a
, "_alias_for", track
);
1425 if(trackdb_putdata(trackdb_tracksdb
, newalias
, a
, tid
, 0)) goto fail
;
1428 /* check whether tags have changed */
1429 if(!strcmp(name
, "tags")) {
1430 newtags
= parsetags(value
);
1431 while(*oldtags
|| *newtags
) {
1432 if(*oldtags
&& *newtags
) {
1433 cmp
= strcmp(*oldtags
, *newtags
);
1435 /* keeping this tag */
1439 /* old tag fits into a gap in the new list, so delete old */
1442 /* new tag fits into a gap in the old list, so insert new */
1444 } else if(*oldtags
) {
1445 /* we've run out of new tags, so remaining old ones are to be
1448 if(trackdb_delkeydata(trackdb_tagsdb
,
1449 *oldtags
, track
, tid
) == DB_LOCK_DEADLOCK
)
1453 /* we've run out of old tags, so remainig new ones are to be
1456 if(register_tag(track
, *newtags
, tid
)) goto fail
;
1466 trackdb_abort_transaction(tid
);
1468 trackdb_commit_transaction(tid
);
1469 return err
== 0 ?
0 : -1;
1473 const char *trackdb_get(const char *track
,
1475 return kvp_get(trackdb_get_all(track
), name
);
1478 /* get all prefs as a 0-terminated array */
1479 struct kvp
*trackdb_get_all(const char *track
) {
1480 struct kvp
*t
, *p
, **pp
;
1484 tid
= trackdb_begin_transaction();
1485 if(gettrackdata(track
, &t
, &p
, 0, 0, tid
) == DB_LOCK_DEADLOCK
)
1489 trackdb_abort_transaction(tid
);
1491 trackdb_commit_transaction(tid
);
1492 for(pp
= &p
; *pp
; pp
= &(*pp
)->next
)
1499 const char *trackdb_resolve(const char *track
) {
1504 tid
= trackdb_begin_transaction();
1505 if(gettrackdata(track
, 0, 0, &actual
, 0, tid
) == DB_LOCK_DEADLOCK
)
1509 trackdb_abort_transaction(tid
);
1511 trackdb_commit_transaction(tid
);
1515 int trackdb_isalias(const char *track
) {
1516 const char *actual
= trackdb_resolve(track
);
1518 return strcmp(actual
, track
);
1521 /* test whether a track exists (perhaps an alias) */
1522 int trackdb_exists(const char *track
) {
1527 tid
= trackdb_begin_transaction();
1528 /* unusually, here we want the return value */
1529 if((err
= gettrackdata(track
, 0, 0, 0, 0, tid
)) == DB_LOCK_DEADLOCK
)
1533 trackdb_abort_transaction(tid
);
1535 trackdb_commit_transaction(tid
);
1539 /* return the list of tags */
1540 char **trackdb_alltags(void) {
1545 WITH_TRANSACTION(trackdb_listkeys(trackdb_tagsdb
, v
, tid
));
1549 /** @brief List all the keys in @p db
1550 * @param db Database
1551 * @param v Vector to store keys in
1552 * @param tid Transaction ID
1553 * @return 0 or DB_LOCK_DEADLOCK
1555 int trackdb_listkeys(DB
*db
, struct vector
*v
, DB_TXN
*tid
) {
1558 DBC
*const c
= trackdb_opencursor(db
, tid
);
1561 memset(&k
, 0, sizeof k
);
1562 while(!(e
= c
->c_get(c
, &k
, prepare_data(&d
), DB_NEXT_NODUP
)))
1563 vector_append(v
, xstrndup(k
.data
, k
.size
));
1567 case DB_LOCK_DEADLOCK
:
1570 fatal(0, "c->c_get: %s", db_strerror(e
));
1572 if((e
= trackdb_closecursor(c
)))
1574 vector_terminate(v
);
1578 /* return 1 iff sorted tag lists A and B have at least one member in common */
1579 static int tag_intersection(char **a
, char **b
) {
1582 /* Same sort of logic as trackdb_set() above */
1584 if(!(cmp
= strcmp(*a
, *b
))) return 1;
1585 else if(cmp
< 0) ++a
;
1591 /* Check whether a track is suitable for random play. Returns 0 if it is,
1592 * DB_NOTFOUND if it is not or DB_LOCK_DEADLOCK if the database gave us
1594 static int check_suitable(const char *track
,
1596 char **required_tags
,
1597 char **prohibited_tags
) {
1601 const char *pick_at_random
, *played_time
;
1603 /* don't pick tracks that aren't in any surviving collection (for instance
1604 * you've edited the config but the rescan hasn't done its job yet) */
1605 if(!find_track_root(track
)) {
1606 info("found track not in any collection: %s", track
);
1609 /* don't pick aliases - only pick the canonical form */
1610 if(gettrackdata(track
, &t
, &p
, 0, 0, tid
) == DB_LOCK_DEADLOCK
)
1611 return DB_LOCK_DEADLOCK
;
1612 if(kvp_get(t
, "_alias_for"))
1614 /* check that random play is not suppressed for this track */
1615 if((pick_at_random
= kvp_get(p
, "pick_at_random"))
1616 && !strcmp(pick_at_random
, "0"))
1618 /* don't pick a track that's been played in the last 8 hours */
1619 if((played_time
= kvp_get(p
, "played_time"))) {
1620 last
= atoll(played_time
);
1622 if(now
< last
+ 8 * 3600) /* TODO configurable */
1625 track_tags
= parsetags(kvp_get(p
, "tags"));
1626 /* check that no prohibited tag is present for this track */
1627 if(prohibited_tags
&& tag_intersection(track_tags
, prohibited_tags
))
1629 /* check that at least one required tags is present for this track */
1630 if(*required_tags
&& !tag_intersection(track_tags
, required_tags
))
1635 /* attempt to pick a random non-alias track */
1636 const char *trackdb_random(int tries
) {
1641 const char *track
, *candidate
;
1644 char **required_tags
, **prohibited_tags
, **tp
;
1649 tid
= trackdb_begin_transaction();
1650 if((err
= trackdb_get_global_tid("required-tags", tid
, &tags
)))
1652 required_tags
= parsetags(tags
);
1653 if((err
= trackdb_get_global_tid("prohibited-tags", tid
, &tags
)))
1655 prohibited_tags
= parsetags(tags
);
1657 if(*required_tags
) {
1658 /* Bung all the suitable tracks into a hash and convert to a list of keys
1659 * (to eliminate duplicates). We cache this list since it is possible
1660 * that it will be very large. */
1663 for(tp
= required_tags
; *tp
; ++tp
) {
1664 c
= trackdb_opencursor(trackdb_tagsdb
, tid
);
1665 memset(&key
, 0, sizeof key
);
1667 key
.size
= strlen(*tp
);
1669 err
= c
->c_get(c
, &key
, prepare_data(&data
), DB_SET
);
1671 hash_add(h
, xstrndup(data
.data
, data
.size
), 0,
1672 HASH_INSERT_OR_REPLACE
);
1674 err
= c
->c_get(c
, &key
, prepare_data(&data
), DB_NEXT_DUP
);
1680 case DB_LOCK_DEADLOCK
:
1683 fatal(0, "error querying tags.db: %s", db_strerror(err
));
1685 trackdb_closecursor(c
);
1688 error(0, "required tag %s does not match any tracks", *tp
);
1690 nreqtracks
= hash_count(h
);
1691 reqtracks
= hash_keys(h
);
1693 while(nreqtracks
&& !track
&& tries
-- > 0) {
1694 r
= (rand() * (double)nreqtracks
/ (RAND_MAX
+ 1.0));
1695 candidate
= reqtracks
[r
];
1696 switch(check_suitable(candidate
, tid
,
1697 required_tags
, prohibited_tags
)) {
1703 case DB_LOCK_DEADLOCK
:
1708 /* No required tags. We pick random record numbers in the database
1710 switch(err
= trackdb_tracksdb
->stat(trackdb_tracksdb
, tid
, &sp
, 0)) {
1713 case DB_LOCK_DEADLOCK
:
1714 error(0, "error querying tracks.db: %s", db_strerror(err
));
1717 fatal(0, "error querying tracks.db: %s", db_strerror(err
));
1720 error(0, "cannot pick tracks at random from an empty database");
1721 while(sp
->bt_nkeys
&& !track
&& tries
-- > 0) {
1722 /* record numbers count from 1 upwards */
1723 r
= 1 + (rand() * (double)sp
->bt_nkeys
/ (RAND_MAX
+ 1.0));
1724 memset(&key
, sizeof key
, 0);
1725 key
.flags
= DB_DBT_MALLOC
;
1726 key
.size
= sizeof r
;
1728 switch(err
= trackdb_tracksdb
->get(trackdb_tracksdb
, tid
, &key
, prepare_data(&data
),
1732 case DB_LOCK_DEADLOCK
:
1733 error(0, "error querying tracks.db: %s", db_strerror(err
));
1736 fatal(0, "error querying tracks.db: %s", db_strerror(err
));
1738 candidate
= xstrndup(key
.data
, key
.size
);
1739 switch(check_suitable(candidate
, tid
,
1740 required_tags
, prohibited_tags
)) {
1746 case DB_LOCK_DEADLOCK
:
1753 trackdb_closecursor(c
);
1755 trackdb_abort_transaction(tid
);
1757 trackdb_commit_transaction(tid
);
1759 error(0, "could not pick a random track");
1763 /* get a track name given the prefs. Set *used_db to 1 if we got the answer
1764 * from the prefs. */
1765 static const char *getpart(const char *track
,
1766 const char *context
,
1768 const struct kvp
*p
,
1773 byte_xasprintf(&pref
, "trackname_%s_%s", context
, part
);
1774 if((result
= kvp_get(p
, pref
)))
1777 result
= trackname_part(track
, context
, part
);
1778 assert(result
!= 0);
1782 /* get a track name part, like trackname_part(), but taking the database into
1784 const char *trackdb_getpart(const char *track
,
1785 const char *context
,
1793 /* construct the full pref */
1794 byte_xasprintf(&pref
, "trackname_%s_%s", context
, part
);
1796 tid
= trackdb_begin_transaction();
1797 if((err
= gettrackdata(track
, 0, &p
, &actual
, 0, tid
)) == DB_LOCK_DEADLOCK
)
1801 trackdb_abort_transaction(tid
);
1803 trackdb_commit_transaction(tid
);
1804 return getpart(actual
, context
, part
, p
, &used_db
);
1807 /* get the raw path name for @track@ (might be an alias) */
1808 const char *trackdb_rawpath(const char *track
) {
1814 tid
= trackdb_begin_transaction();
1815 if(gettrackdata(track
, &t
, 0, 0, 0, tid
) == DB_LOCK_DEADLOCK
)
1819 trackdb_abort_transaction(tid
);
1821 trackdb_commit_transaction(tid
);
1822 if(!(path
= kvp_get(t
, "_path"))) path
= track
;
1826 /* trackdb_list **************************************************************/
1828 /* this is incredibly ugly, sorry, perhaps it will be rewritten to be actually
1829 * readable at some point */
1831 /* return true if the basename of TRACK[0..TL-1], as defined by DL, matches RE.
1832 * If RE is a null pointer then it matches everything. */
1833 static int track_matches(size_t dl
, const char *track
, size_t tl
,
1841 switch(rc
= pcre_exec(re
, 0, track
, tl
, 0, 0, ovec
, 3)) {
1842 case PCRE_ERROR_NOMATCH
: return 0;
1845 error(0, "pcre_exec returned %d, subject '%s'", rc
, track
);
1852 static int do_list(struct vector
*v
, const char *dir
,
1853 enum trackdb_listable what
, const pcre
*re
, DB_TXN
*tid
) {
1859 size_t l
, last_dir_len
= 0;
1860 char *last_dir
= 0, *track
, *alias
;
1864 cursor
= trackdb_opencursor(trackdb_tracksdb
, tid
);
1867 /* find the first key >= dir */
1868 err
= cursor
->c_get(cursor
, &k
, &d
, DB_SET_RANGE
);
1869 /* keep going while we're dealing with <dir/anything> */
1872 && ((char *)k
.data
)[dl
] == '/'
1873 && !memcmp(k
.data
, dir
, dl
)) {
1874 ptr
= memchr((char *)k
.data
+ dl
+ 1, '/', k
.size
- (dl
+ 1));
1876 /* we have <dir/component/anything>, so <dir/component> is a directory */
1877 l
= ptr
- (char *)k
.data
;
1878 if(what
& trackdb_directories
)
1880 && l
== last_dir_len
1881 && !memcmp(last_dir
, k
.data
, l
))) {
1882 last_dir
= xstrndup(k
.data
, last_dir_len
= l
);
1883 if(track_matches(dl
, k
.data
, l
, re
))
1884 vector_append(v
, last_dir
);
1887 /* found a plain file */
1888 if((what
& trackdb_files
)) {
1889 track
= xstrndup(k
.data
, k
.size
);
1890 if((err
= trackdb_getdata(trackdb_prefsdb
,
1891 track
, &p
, tid
)) == DB_LOCK_DEADLOCK
)
1893 /* if this file has an alias in the same directory then we skip it */
1894 if((err
= compute_alias(&alias
, track
, p
, tid
)))
1896 if(!(alias
&& !strcmp(d_dirname(alias
), d_dirname(track
))))
1897 if(track_matches(dl
, k
.data
, k
.size
, re
))
1898 vector_append(v
, track
);
1901 err
= cursor
->c_get(cursor
, &k
, &d
, DB_NEXT
);
1909 case DB_LOCK_DEADLOCK
:
1910 error(0, "error querying database: %s", db_strerror(err
));
1913 fatal(0, "error querying database: %s", db_strerror(err
));
1916 if(trackdb_closecursor(cursor
)) err
= DB_LOCK_DEADLOCK
;
1920 /* return the directories or files below @dir@ */
1921 char **trackdb_list(const char *dir
, int *np
, enum trackdb_listable what
,
1929 tid
= trackdb_begin_transaction();
1932 if(do_list(&v
, dir
, what
, re
, tid
))
1935 for(n
= 0; n
< config
->collection
.n
; ++n
)
1936 if(do_list(&v
, config
->collection
.s
[n
].root
, what
, re
, tid
))
1941 trackdb_abort_transaction(tid
);
1943 trackdb_commit_transaction(tid
);
1944 vector_terminate(&v
);
1950 /* If S is tag:something, return something. Else return 0. */
1951 static const char *checktag(const char *s
) {
1952 if(!strncmp(s
, "tag:", 4))
1958 /* return a list of tracks containing all of the words given. If you
1959 * ask for only stopwords you get no tracks. */
1960 char **trackdb_search(char **wordlist
, int nwordlist
, int *ntracks
) {
1961 const char **w
, *best
= 0, *tag
;
1962 char **twords
, **tags
;
1964 int i
, j
, n
, err
, what
;
1974 *ntracks
= 0; /* for early returns */
1975 /* normalize all the words */
1976 w
= xmalloc(nwordlist
* sizeof (char *));
1977 istag
= xmalloc_noptr(nwordlist
);
1978 for(n
= 0; n
< nwordlist
; ++n
) {
1982 w
[n
] = utf8_casefold_compat(wordlist
[n
], strlen(wordlist
[n
]), 0);
1983 if(checktag(w
[n
])) {
1984 ++ntags
; /* count up tags */
1985 /* Normalize the tag */
1986 w
[n
] = normalize_tag(w
[n
] + 4, strlen(w
[n
] + 4));
1989 /* Normalize the search term by removing combining characters */
1990 if(!(w32
= utf8_to_utf32(w
[n
], strlen(w
[n
]), &nw32
)))
1992 nw32
= remove_combining_chars(w32
, nw32
);
1993 if(!(w
[n
] = utf32_to_utf8(w32
, nw32
, 0)))
1998 /* find the longest non-stopword */
1999 for(n
= 0; n
< nwordlist
; ++n
)
2000 if(!istag
[n
] && !stopword(w
[n
]))
2001 if(!best
|| strlen(w
[n
]) > strlen(best
))
2003 /* TODO: we should at least in principal be able to identify the word or tag
2004 * with the least matches in log time, and choose that as our primary search
2006 if(ntags
&& !best
) {
2007 /* Only tags are listed. We limit to the first and narrow down with the
2009 best
= istag
[0] ? w
[0] : 0;
2010 db
= trackdb_tagsdb
;
2013 /* We can limit to some word. */
2014 db
= trackdb_searchdb
;
2017 /* Only stopwords */
2023 tid
= trackdb_begin_transaction();
2024 /* find all the tracks that have that word */
2029 cursor
= trackdb_opencursor(db
, tid
);
2030 while(!(err
= cursor
->c_get(cursor
, &k
, &d
, what
))) {
2031 vector_append(&v
, xstrndup(d
.data
, d
.size
));
2038 case DB_LOCK_DEADLOCK
:
2039 error(0, "error querying %s database: %s", dbname
, db_strerror(err
));
2042 fatal(0, "error querying %s database: %s", dbname
, db_strerror(err
));
2044 if(trackdb_closecursor(cursor
)) err
= DB_LOCK_DEADLOCK
;
2046 /* do a naive search over that (hopefuly fairly small) list of tracks */
2048 for(n
= 0; n
< v
.nvec
; ++n
) {
2049 if((err
= gettrackdata(v
.vec
[n
], 0, &p
, 0, 0, tid
) == DB_LOCK_DEADLOCK
))
2052 error(0, "track %s unexpected error: %s", v
.vec
[n
], db_strerror(err
));
2055 twords
= track_to_words(v
.vec
[n
], p
);
2056 tags
= parsetags(kvp_get(p
, "tags"));
2057 for(i
= 0; i
< nwordlist
; ++i
) {
2060 /* Track must have this tag */
2061 for(j
= 0; tags
[j
]; ++j
)
2062 if(!strcmp(tag
, tags
[j
])) break; /* tag found */
2063 if(!tags
[j
]) break; /* tag not found */
2065 /* Track must contain this word */
2066 for(j
= 0; twords
[j
]; ++j
)
2067 if(!strcmp(w
[i
], twords
[j
])) break; /* word found */
2068 if(!twords
[j
]) break; /* word not found */
2071 if(i
>= nwordlist
) /* all words found */
2072 vector_append(&u
, v
.vec
[n
]);
2076 trackdb_closecursor(cursor
);
2078 trackdb_abort_transaction(tid
);
2079 info("retrying search");
2081 trackdb_commit_transaction(tid
);
2082 vector_terminate(&u
);
2088 /* trackdb_scan **************************************************************/
2090 int trackdb_scan(const char *root
,
2091 int (*callback
)(const char *track
,
2099 const size_t root_len
= root ?
strlen(root
) : 0;
2104 cursor
= trackdb_opencursor(trackdb_tracksdb
, tid
);
2106 err
= cursor
->c_get(cursor
, make_key(&k
, root
), prepare_data(&d
),
2109 memset(&k
, 0, sizeof k
);
2110 err
= cursor
->c_get(cursor
, &k
, prepare_data(&d
),
2115 || (k
.size
> root_len
2116 && !strncmp(k
.data
, root
, root_len
)
2117 && ((char *)k
.data
)[root_len
] == '/')) {
2118 data
= kvp_urldecode(d
.data
, d
.size
);
2119 if(kvp_get(data
, "_path")) {
2120 track
= xstrndup(k
.data
, k
.size
);
2121 /* Advance to the next track before the callback so that the callback
2122 * may safely delete the track */
2123 err
= cursor
->c_get(cursor
, &k
, &d
, DB_NEXT
);
2124 if((cberr
= callback(track
, data
, u
, tid
))) {
2129 err
= cursor
->c_get(cursor
, &k
, &d
, DB_NEXT
);
2133 trackdb_closecursor(cursor
);
2140 case DB_LOCK_DEADLOCK
:
2141 error(0, "c->c_get: %s", db_strerror(err
));
2144 fatal(0, "c->c_get: %s", db_strerror(err
));
2148 /* trackdb_rescan ************************************************************/
2150 /* called when the rescanner terminates */
2151 static int reap_rescan(ev_source
attribute((unused
)) *ev
,
2154 const struct rusage
attribute((unused
)) *rusage
,
2155 void attribute((unused
)) *u
) {
2156 if(pid
== rescan_pid
) rescan_pid
= -1;
2158 error(0, RESCAN
": %s", wstat(status
));
2160 D((RESCAN
" terminated: %s", wstat(status
)));
2161 /* Our cache of file lookups is out of date now */
2162 cache_clean(&cache_files_type
);
2163 eventlog("rescanned", (char *)0);
2167 /** @brief Initiate a rescan
2168 * @param ev Event loop or 0 to block
2169 * @param check 1 to recheck lengths, 0 to suppress check
2171 void trackdb_rescan(ev_source
*ev
, int check
) {
2174 if(rescan_pid
!= -1) {
2175 error(0, "rescan already underway");
2178 rescan_pid
= subprogram(ev
, -1, RESCAN
,
2179 check ?
"--check" : "--no-check",
2182 ev_child(ev
, rescan_pid
, 0, reap_rescan
, 0);
2183 D(("started rescanner"));
2185 /* This is the first rescan, we block until it is complete */
2186 while(waitpid(rescan_pid
, &w
, 0) < 0 && errno
== EINTR
)
2188 reap_rescan(0, rescan_pid
, w
, 0, 0);
2192 int trackdb_rescan_cancel(void) {
2193 if(rescan_pid
== -1) return 0;
2194 if(kill(rescan_pid
, SIGTERM
) < 0)
2195 fatal(errno
, "error killing rescanner");
2200 /* global prefs **************************************************************/
2202 void trackdb_set_global(const char *name
,
2210 tid
= trackdb_begin_transaction();
2211 if(!(err
= trackdb_set_global_tid(name
, value
, tid
)))
2213 trackdb_abort_transaction(tid
);
2215 trackdb_commit_transaction(tid
);
2216 /* log important state changes */
2217 if(!strcmp(name
, "playing")) {
2218 state
= !value
|| !strcmp(value
, "yes");
2219 info("playing %s by %s",
2220 state ?
"enabled" : "disabled",
2222 eventlog("state", state ?
"enable_play" : "disable_play", (char *)0);
2224 if(!strcmp(name
, "random-play")) {
2225 state
= !value
|| !strcmp(value
, "yes");
2226 info("random play %s by %s",
2227 state ?
"enabled" : "disabled",
2229 eventlog("state", state ?
"enable_random" : "disable_random", (char *)0);
2231 if(!strcmp(name
, "required-tags"))
2235 int trackdb_set_global_tid(const char *name
,
2241 memset(&k
, 0, sizeof k
);
2242 memset(&d
, 0, sizeof d
);
2243 k
.data
= (void *)name
;
2244 k
.size
= strlen(name
);
2246 d
.data
= (void *)value
;
2247 d
.size
= strlen(value
);
2250 err
= trackdb_globaldb
->put(trackdb_globaldb
, tid
, &k
, &d
, 0);
2252 err
= trackdb_globaldb
->del(trackdb_globaldb
, tid
, &k
, 0);
2253 if(err
== DB_LOCK_DEADLOCK
) return err
;
2255 fatal(0, "error updating database: %s", db_strerror(err
));
2259 const char *trackdb_get_global(const char *name
) {
2265 tid
= trackdb_begin_transaction();
2266 if(!(err
= trackdb_get_global_tid(name
, tid
, &r
)))
2268 trackdb_abort_transaction(tid
);
2270 trackdb_commit_transaction(tid
);
2274 int trackdb_get_global_tid(const char *name
,
2280 memset(&k
, 0, sizeof k
);
2281 k
.data
= (void *)name
;
2282 k
.size
= strlen(name
);
2283 switch(err
= trackdb_globaldb
->get(trackdb_globaldb
, tid
, &k
,
2284 prepare_data(&d
), 0)) {
2286 *rp
= xstrndup(d
.data
, d
.size
);
2291 case DB_LOCK_DEADLOCK
:
2294 fatal(0, "error reading database: %s", db_strerror(err
));
2298 /** @brief Retrieve the most recently added tracks
2299 * @param ntracksp Where to put count, or 0
2300 * @param maxtracks Maximum number of tracks to retrieve
2301 * @return null-terminated array of track names
2303 * The most recently added track is first in the array.
2305 char **trackdb_new(int *ntracksp
,
2311 tid
= trackdb_begin_transaction();
2312 tracks
= trackdb_new_tid(ntracksp
, maxtracks
, tid
);
2315 trackdb_abort_transaction(tid
);
2317 trackdb_commit_transaction(tid
);
2321 /** @brief Retrieve the most recently added tracks
2322 * @param ntracksp Where to put count, or 0
2323 * @param maxtracks Maximum number of tracks to retrieve, or 0 for all
2324 * @param tid Transaction ID
2325 * @return null-terminated array of track names, or NULL on deadlock
2327 * The most recently added track is first in the array.
2329 static char **trackdb_new_tid(int *ntracksp
,
2335 struct vector tracks
[1];
2337 vector_init(tracks
);
2338 c
= trackdb_opencursor(trackdb_noticeddb
, tid
);
2339 while((maxtracks
<= 0 || tracks
->nvec
< maxtracks
)
2340 && !(err
= c
->c_get(c
, prepare_data(&k
), prepare_data(&d
), DB_PREV
)))
2341 vector_append(tracks
, xstrndup(d
.data
, d
.size
));
2343 case 0: /* hit maxtracks */
2344 case DB_NOTFOUND
: /* ran out of tracks */
2346 case DB_LOCK_DEADLOCK
:
2347 trackdb_closecursor(c
);
2350 fatal(0, "error reading noticed.db: %s", db_strerror(err
));
2352 if((err
= trackdb_closecursor(c
)))
2353 return 0; /* deadlock */
2354 vector_terminate(tracks
);
2356 *ntracksp
= tracks
->nvec
;
2360 /** @brief Expire noticed.db
2361 * @param earliest Earliest timestamp to keep
2363 void trackdb_expire_noticed(time_t earliest
) {
2367 tid
= trackdb_begin_transaction();
2368 if(!trackdb_expire_noticed_tid(earliest
, tid
))
2370 trackdb_abort_transaction(tid
);
2372 trackdb_commit_transaction(tid
);
2375 /** @brief Expire noticed.db
2376 * @param earliest Earliest timestamp to keep
2377 * @param tid Transaction ID
2378 * @return 0 or DB_LOCK_DEADLOCK
2380 static int trackdb_expire_noticed_tid(time_t earliest
, DB_TXN
*tid
) {
2388 c
= trackdb_opencursor(trackdb_noticeddb
, tid
);
2389 while(!(err
= c
->c_get(c
, prepare_data(&k
), prepare_data(&d
), DB_NEXT
))) {
2391 when
= (time_t)(((uint64_t)ntohl(kk
[0]) << 32) + ntohl(kk
[1]));
2392 if(when
>= earliest
)
2394 if((err
= c
->c_del(c
, 0))) {
2395 if(err
!= DB_LOCK_DEADLOCK
)
2396 fatal(0, "error deleting expired noticed.db entry: %s",
2402 if(err
== DB_NOTFOUND
)
2404 if(err
&& err
!= DB_LOCK_DEADLOCK
)
2405 fatal(0, "error expiring noticed.db: %s", db_strerror(err
));
2407 if((err
= trackdb_closecursor(c
))) {
2408 if(err
!= DB_LOCK_DEADLOCK
)
2409 fatal(0, "error closing cursor: %s", db_strerror(err
));
2413 info("expired %d tracks from noticed.db", count
);
2417 /* tidying up ****************************************************************/
2419 void trackdb_gc(void) {
2423 if((err
= trackdb_env
->txn_checkpoint(trackdb_env
,
2424 config
->checkpoint_kbyte
,
2425 config
->checkpoint_min
,
2427 fatal(0, "trackdb_env->txn_checkpoint: %s", db_strerror(err
));
2428 if((err
= trackdb_env
->log_archive(trackdb_env
, &logfiles
, DB_ARCH_REMOVE
)))
2429 fatal(0, "trackdb_env->log_archive: %s", db_strerror(err
));
2430 /* This makes catastrophic recovery impossible. However, the user can still
2431 * preserve the important data by using disorder-dump to snapshot their
2432 * prefs, and later to restore it. This is likely to have much small
2433 * long-term storage requirements than record the db logfiles. */
2436 /* user database *************************************************************/
2438 /** @brief Return true if @p user is trusted */
2439 static int trusted(const char *user
) {
2442 for(n
= 0; (n
< config
->trust
.n
2443 && strcmp(config
->trust
.s
[n
], user
)); ++n
)
2445 return n
< config
->trust
.n
;
2448 /** @brief Return non-zero for a valid username
2450 * Currently we only allow the letters and digits in ASCII. We could be more
2451 * liberal than this but it is a nice simple test. It is critical that
2452 * semicolons are never allowed.
2454 static int valid_username(const char *user
) {
2458 const uint8_t c
= *user
++;
2459 /* For now we are very strict */
2460 if((c
>= 'a' && c
<= 'z')
2461 || (c
>= 'A' && c
<= 'Z')
2462 || (c
>= '0' && c
<= '9'))
2470 /** @brief Add a user */
2471 static int create_user(const char *user
,
2472 const char *password
,
2475 const char *confirmation
,
2481 /* sanity check user */
2482 if(!valid_username(user
)) {
2483 error(0, "invalid username '%s'", user
);
2486 if(parse_rights(rights
, 0, 1)) {
2487 error(0, "invalid rights string");
2490 /* data for this user */
2492 kvp_set(&k
, "password", password
);
2493 kvp_set(&k
, "rights", rights
);
2495 kvp_set(&k
, "email", email
);
2497 kvp_set(&k
, "confirmation", confirmation
);
2498 snprintf(s
, sizeof s
, "%jd", (intmax_t)time(0));
2499 kvp_set(&k
, "created", s
);
2500 return trackdb_putdata(trackdb_usersdb
, user
, k
, tid
, flags
);
2503 /** @brief Add one pre-existing user */
2504 static int one_old_user(const char *user
, const char *password
,
2508 /* www-data doesn't get added */
2509 if(!strcmp(user
, "www-data")) {
2510 info("not adding www-data to user database");
2514 if(!strcmp(user
, "root"))
2516 else if(trusted(user
)) {
2519 parse_rights(config
->default_rights
, &r
, 1);
2520 r
&= ~(rights_type
)(RIGHT_SCRATCH__MASK
|RIGHT_MOVE__MASK
|RIGHT_REMOVE__MASK
);
2521 r
|= (RIGHT_ADMIN
|RIGHT_RESCAN
2522 |RIGHT_SCRATCH_ANY
|RIGHT_MOVE_ANY
|RIGHT_REMOVE_ANY
);
2523 rights
= rights_string(r
);
2525 rights
= config
->default_rights
;
2526 return create_user(user
, password
, rights
, 0/*email*/, 0/*confirmation*/,
2527 tid
, DB_NOOVERWRITE
);
2530 static int trackdb_old_users_tid(DB_TXN
*tid
) {
2533 for(n
= 0; n
< config
->allow
.n
; ++n
) {
2534 switch(one_old_user(config
->allow
.s
[n
].s
[0], config
->allow
.s
[n
].s
[1],
2537 info("created user %s from 'allow' directive", config
->allow
.s
[n
].s
[0]);
2540 error(0, "user %s already exists, delete 'allow' directive",
2541 config
->allow
.s
[n
].s
[0]);
2542 /* This won't ever become fatal - eventually 'allow' will be
2545 case DB_LOCK_DEADLOCK
:
2546 return DB_LOCK_DEADLOCK
;
2552 /** @brief Read old 'allow' directives and copy them to the users database */
2553 void trackdb_old_users(void) {
2557 WITH_TRANSACTION(trackdb_old_users_tid(tid
));
2560 /** @brief Create a root user in the user database if there is none */
2561 void trackdb_create_root(void) {
2566 /* Choose a new root password */
2567 gcry_randomize(pwbin
, sizeof pwbin
, GCRY_STRONG_RANDOM
);
2568 pw
= mime_to_base64(pwbin
, sizeof pwbin
);
2569 /* Create the root user if it does not exist */
2570 WITH_TRANSACTION(create_user("root", pw
, "all",
2571 0/*email*/, 0/*confirmation*/,
2572 tid
, DB_NOOVERWRITE
));
2574 info("created root user");
2577 /** @brief Find a user's password from the database
2578 * @param user Username
2579 * @return Password or NULL
2581 * Only works if running as a user that can read the database!
2583 * If the user exists but has no password, "" is returned.
2585 const char *trackdb_get_password(const char *user
) {
2588 const char *password
;
2590 WITH_TRANSACTION(trackdb_getdata(trackdb_usersdb
, user
, &k
, tid
));
2593 password
= kvp_get(k
, "password");
2594 return password ? password
: "";
2597 /** @brief Add a new user
2598 * @param user Username
2599 * @param password Password or NULL
2600 * @param rights Initial rights
2601 * @param email Email address or NULL
2602 * @param confirmation Confirmation string or NULL
2603 * @return 0 on success, non-0 on error
2605 int trackdb_adduser(const char *user
,
2606 const char *password
,
2609 const char *confirmation
) {
2612 WITH_TRANSACTION(create_user(user
, password
, rights
, email
, confirmation
,
2613 tid
, DB_NOOVERWRITE
));
2615 error(0, "cannot create user '%s' because they already exist", user
);
2619 info("created user '%s' with rights '%s' and email address '%s'",
2620 user
, rights
, email
);
2622 info("created user '%s' with rights '%s'", user
, rights
);
2627 /** @brief Delete a user
2628 * @param user User to delete
2629 * @return 0 on success, non-0 if the user didn't exist anyway
2631 int trackdb_deluser(const char *user
) {
2634 WITH_TRANSACTION(trackdb_delkey(trackdb_usersdb
, user
, tid
));
2636 error(0, "cannot delete user '%s' because they do not exist", user
);
2639 info("deleted user '%s'", user
);
2643 /** @brief Get user information
2644 * @param user User to query
2645 * @return Linked list of user information or NULL if user does not exist
2647 * Every user has at least a @c rights entry so NULL can be used to mean no
2650 struct kvp
*trackdb_getuserinfo(const char *user
) {
2654 WITH_TRANSACTION(trackdb_getdata(trackdb_usersdb
, user
, &k
, tid
));
2661 /** @brief Edit user information
2662 * @param user User to edit
2663 * @param key Key to change
2664 * @param value Value to set, or NULL to remove
2665 * @param tid Transaction ID
2666 * @return 0, DB_LOCK_DEADLOCK or DB_NOTFOUND
2668 static int trackdb_edituserinfo_tid(const char *user
, const char *key
,
2669 const char *value
, DB_TXN
*tid
) {
2673 if((e
= trackdb_getdata(trackdb_usersdb
, user
, &k
, tid
)))
2675 if(!kvp_set(&k
, key
, value
))
2676 return 0; /* no change */
2677 return trackdb_putdata(trackdb_usersdb
, user
, k
, tid
, 0);
2680 /** @brief Edit user information
2681 * @param user User to edit
2682 * @param key Key to change
2683 * @param value Value to set, or NULL to remove
2684 * @return 0 on success, non-0 on error
2686 int trackdb_edituserinfo(const char *user
,
2687 const char *key
, const char *value
) {
2690 if(!strcmp(key
, "rights")) {
2692 error(0, "cannot remove 'rights' key from user '%s'", user
);
2695 if(parse_rights(value
, 0, 1)) {
2696 error(0, "invalid rights string");
2699 } else if(!strcmp(key
, "email")) {
2700 if(!strchr(value
, '@')) {
2701 error(0, "invalid email address '%s' for user '%s'", user
, value
);
2704 } else if(!strcmp(key
, "created")) {
2705 error(0, "cannot change creation date for user '%s'", user
);
2707 } else if(strcmp(key
, "password")
2708 && !strcmp(key
, "confirmation")) {
2709 error(0, "unknown user info key '%s' for user '%s'", key
, user
);
2712 WITH_TRANSACTION(trackdb_edituserinfo_tid(user
, key
, value
, tid
));
2714 error(0, "unknown user '%s'", user
);
2720 /** @brief List all users
2721 * @return NULL-terminated list of users
2723 char **trackdb_listusers(void) {
2728 WITH_TRANSACTION(trackdb_listkeys(trackdb_usersdb
, v
, tid
));
2732 /** @brief Confirm a user registration
2733 * @param user Username
2734 * @param confirmation Confirmation string
2735 * @param rightsp Where to put user rights
2736 * @param tid Transaction ID
2737 * @return 0 on success, non-0 on error
2739 static int trackdb_confirm_tid(const char *user
, const char *confirmation
,
2740 rights_type
*rightsp
,
2742 const char *stored_confirmation
;
2747 if((e
= trackdb_getdata(trackdb_usersdb
, user
, &k
, tid
)))
2749 if(!(stored_confirmation
= kvp_get(k
, "confirmation"))) {
2750 error(0, "already confirmed user '%s'", user
);
2751 /* DB claims -30,800 to -30,999 so -1 should be a safe bet */
2754 if(!(rights
= kvp_get(k
, "rights"))) {
2755 error(0, "no rights for unconfirmed user '%s'", user
);
2758 if(parse_rights(rights
, rightsp
, 1))
2760 if(strcmp(confirmation
, stored_confirmation
)) {
2761 error(0, "wrong confirmation string for user '%s'", user
);
2765 kvp_set(&k
, "confirmation", 0);
2766 return trackdb_putdata(trackdb_usersdb
, user
, k
, tid
, 0);
2769 /** @brief Confirm a user registration
2770 * @param user Username
2771 * @param confirmation Confirmation string
2772 * @param rightsp Where to put user rights
2773 * @return 0 on success, non-0 on error
2775 int trackdb_confirm(const char *user
, const char *confirmation
,
2776 rights_type
*rightsp
) {
2779 WITH_TRANSACTION(trackdb_confirm_tid(user
, confirmation
, rightsp
, tid
));
2782 info("registration confirmed for user '%s'", user
);
2785 error(0, "confirmation for nonexistent user '%s'", user
);
2787 default: /* already reported */
2797 indent-tabs-mode:nil