From 2550d6cb8c7f263f63a68122b69285f8f849dc18 Mon Sep 17 00:00:00 2001
From: Richard Nyberg <rnyberg@murmeldjur.se>
Date: Wed, 8 Feb 2006 22:56:35 +0000
Subject: [PATCH] Interaction with btpd is now much more like I want it.
 Previous work has moved toward btpd having a library of torrent to wich one
 may add or remove torrents, and where interaction on torrents are done by
 their assigned number. This commit is a step back from that and it makes life
 simpler and better for all :)

* Some options to btpd has changed:
  --no-daemon is the old -d.
  -d is now used to specify the btpd directory.
  --logfile option is reintroduced.
* The ipc code has been improved on both btpd and cli sides.
* All commands have been implemented.
* Various improvements in btpd.

With this commit we're very close to 0.8 :)
---
 btpd/btpd.c    | 118 +++--------------
 btpd/btpd.h    |  14 +-
 btpd/cli_if.c  | 251 ++++++++++++++----------------------
 btpd/content.c |  14 +-
 btpd/main.c    |  76 +++++------
 btpd/net.c     |   5 +-
 btpd/opts.c    |   1 -
 btpd/opts.h    |   1 -
 btpd/torrent.c | 153 +++++++++++++---------
 btpd/torrent.h |  16 ++-
 cli/btcli.c    | 341 +++++++++++++++++++++++++++----------------------
 cli/btpd_if.c  |  68 +++++-----
 cli/btpd_if.h  |  22 ++--
 13 files changed, 496 insertions(+), 584 deletions(-)

diff --git a/btpd/btpd.c b/btpd/btpd.c
index c73d5bd..ccc2e20 100644
--- a/btpd/btpd.c
+++ b/btpd/btpd.c
@@ -32,9 +32,6 @@
 static uint8_t m_peer_id[20];
 static struct event m_sigint;
 static struct event m_sigterm;
-static unsigned m_ntorrents;
-static struct torrent_tq m_torrents = BTPDQ_HEAD_INITIALIZER(m_torrents);
-static unsigned m_nactive;
 static int m_shutdown;
 
 void
@@ -44,92 +41,35 @@ btpd_exit(int code)
     exit(code);
 }
 
-void
-btpd_tp_activated(struct torrent *tp)
-{
-    m_nactive++;
-}
-
-void
-btpd_tp_deactivated(struct torrent *tp)
-{
-    m_nactive--;
-    if (m_nactive == 0 && m_shutdown)
-        btpd_exit(0);
-}
-
 static void
 grace_cb(int fd, short type, void *arg)
 {
     struct torrent *tp;
-    BTPDQ_FOREACH(tp, &m_torrents, entry)
-        torrent_deactivate(tp);
+    BTPDQ_FOREACH(tp, torrent_get_all(), entry)
+        torrent_stop(tp);
 }
 
 void
-btpd_shutdown(struct timeval *grace_tv)
+btpd_shutdown(int grace_seconds)
 {
-    if (m_nactive == 0)
+    if (torrent_count() == 0)
         btpd_exit(0);
     else {
         struct torrent *tp;
         m_shutdown = 1;
-        BTPDQ_FOREACH(tp, &m_torrents, entry)
-            torrent_deactivate(tp);
-        if (grace_tv != NULL)
-            event_once(-1, EV_TIMEOUT, grace_cb, NULL, grace_tv);
+        BTPDQ_FOREACH(tp, torrent_get_all(), entry)
+            if (tp->state != T_STOPPING)
+                torrent_stop(tp);
+        if (grace_seconds >= 0) {
+            event_once(-1, EV_TIMEOUT, grace_cb, NULL,
+                (& (struct timeval) { grace_seconds, 0 }));
+        }
     }
 }
 
-static void
-signal_cb(int signal, short type, void *arg)
-{
-    btpd_log(BTPD_L_BTPD, "Got signal %d.\n", signal);
-    btpd_shutdown((& (struct timeval) { 30, 0 }));
-}
-
-void
-btpd_add_torrent(struct torrent *tp)
-{
-    BTPDQ_INSERT_TAIL(&m_torrents, tp, entry);
-    m_ntorrents++;
-}
-
-void
-btpd_del_torrent(struct torrent *tp)
-{
-    BTPDQ_REMOVE(&m_torrents, tp, entry);
-    m_ntorrents--;
-}
-
-const struct torrent_tq *
-btpd_get_torrents(void)
-{
-    return &m_torrents;
-}
-
-unsigned
-btpd_get_ntorrents(void)
-{
-    return m_ntorrents;
-}
-
-struct torrent *
-btpd_get_torrent(const uint8_t *hash)
-{
-    struct torrent *tp = BTPDQ_FIRST(&m_torrents);
-    while (tp != NULL && bcmp(hash, tp->meta.info_hash, 20) != 0)
-        tp = BTPDQ_NEXT(tp, entry);
-    return tp;
-}
-
-struct torrent *
-btpd_get_torrent_num(unsigned num)
+int btpd_is_stopping(void)
 {
-    struct torrent *tp = BTPDQ_FIRST(&m_torrents);
-    while (tp != NULL && tp->num != num)
-        tp = BTPDQ_NEXT(tp, entry);
-    return tp;
+    return m_shutdown;
 }
 
 const uint8_t *
@@ -138,28 +78,18 @@ btpd_get_peer_id(void)
     return m_peer_id;
 }
 
-static int
-nodot(struct dirent *dp)
+void
+btpd_on_no_torrents(void)
 {
-    return !(strcmp(".", dp->d_name) == 0 || strcmp("..", dp->d_name) == 0);
+    if (m_shutdown)
+        btpd_exit(0);
 }
 
 static void
-load_library(void)
+signal_cb(int signal, short type, void *arg)
 {
-    int ne;
-    struct dirent **entries;
-    if ((ne = scandir("library", &entries, nodot, NULL)) < 0)
-        btpd_err("Couldn't open the library.\n");
-
-    for (int i = 0; i < ne; i++) {
-        struct torrent *tp;
-        struct dirent *e = entries[i];
-        if (torrent_load(&tp, e->d_name) == 0)
-            btpd_add_torrent(tp);
-        free(e);
-    }
-    free(entries);
+    btpd_log(BTPD_L_BTPD, "Got signal %d.\n", signal);
+    btpd_shutdown(30);
 }
 
 struct td_cb {
@@ -260,14 +190,6 @@ btpd_init(void)
     ul_init();
     cm_init();
 
-    load_library();
-
-#if 0
-    struct torrent *tp;
-    BTPDQ_FOREACH(tp, &m_torrents, entry)
-        torrent_activate(tp);
-#endif
-
     signal(SIGPIPE, SIG_IGN);
 
     signal_set(&m_sigint, SIGINT, signal_cb, NULL);
diff --git a/btpd/btpd.h b/btpd/btpd.h
index 1700248..c567c4e 100644
--- a/btpd/btpd.h
+++ b/btpd/btpd.h
@@ -48,14 +48,9 @@ void btpd_err(const char *fmt, ...);
 void *btpd_malloc(size_t size);
 void *btpd_calloc(size_t nmemb, size_t size);
 
-void btpd_shutdown(struct timeval *grace_tv);
-
-struct torrent *btpd_get_torrent(const uint8_t *hash);
-struct torrent *btpd_get_torrent_num(unsigned num);
-const struct torrent_tq *btpd_get_torrents(void);
-void btpd_add_torrent(struct torrent *tp);
-void btpd_del_torrent(struct torrent *tp);
-unsigned btpd_get_ntorrents(void);
+void btpd_shutdown(int grace_seconds);
+int btpd_is_stopping(void);
+
 const uint8_t *btpd_get_peer_id(void);
 
 void td_acquire_lock(void);
@@ -65,7 +60,6 @@ void td_release_lock(void);
 void td_post(void (*fun)(void *), void *arg);
 void td_post_end(void);
 
-void btpd_tp_activated(struct torrent *tp);
-void btpd_tp_deactivated(struct torrent *tp);
+void btpd_on_no_torrents(void);
 
 #endif
diff --git a/btpd/cli_if.c b/btpd/cli_if.c
index f2cdc0b..a5111c0 100644
--- a/btpd/cli_if.c
+++ b/btpd/cli_if.c
@@ -19,10 +19,39 @@ struct cli {
     struct event read;
 };
 
-#define buf_swrite(iob, s) buf_write(iob, s, sizeof(s) - 1)
-
 static struct event m_cli_incoming;
 
+enum ipc_code { // XXX: Same as in cli/btpd_if.h
+    IPC_OK,
+    IPC_FAIL,
+    IPC_ERROR,
+    IPC_COMMERR
+};
+
+static int
+write_buffer(struct cli *cli, struct io_buffer *iob)
+{
+    int err = 0;
+    if (!iob->error) {
+        uint32_t len = iob->buf_off;
+        write_fully(cli->sd, &len, sizeof(len));
+        err = write_fully(cli->sd, iob->buf, iob->buf_off);
+    } else
+        btpd_err("Out of memory.\n");
+    if (iob->buf != NULL)
+        free(iob->buf);
+    return err;
+}
+
+static int
+write_code_buffer(struct cli *cli, enum ipc_code code)
+{
+    struct io_buffer iob;
+    buf_init(&iob, 16);
+    buf_print(&iob, "d4:codei%uee", code);
+    return write_buffer(cli, &iob);
+}
+
 static int
 cmd_stat(struct cli *cli, int argc, const char *args)
 {
@@ -33,9 +62,9 @@ cmd_stat(struct cli *cli, int argc, const char *args)
     buf_swrite(&iob, "d");
     buf_swrite(&iob, "4:codei0e");
     buf_print(&iob, "6:npeersi%ue", net_npeers);
-    buf_print(&iob, "9:ntorrentsi%ue", btpd_get_ntorrents());
+    buf_print(&iob, "9:ntorrentsi%ue", torrent_count());
     buf_swrite(&iob, "8:torrentsl");
-    BTPDQ_FOREACH(tp, btpd_get_torrents(), entry) {
+    BTPDQ_FOREACH(tp, torrent_get_all(), entry) {
         if (tp->state == T_ACTIVE) {
             uint32_t seen_npieces = 0;
             for (uint32_t i = 0; i < tp->meta.npieces; i++)
@@ -49,183 +78,96 @@ cmd_stat(struct cli *cli, int argc, const char *args)
             buf_print(&iob, "4:havei%jde", (intmax_t)cm_get_size(tp));
             buf_print(&iob, "6:npeersi%ue", tp->net->npeers);
             buf_print(&iob, "7:npiecesi%ue", tp->meta.npieces);
-            buf_print(&iob, "3:numi%ue", tp->num);
             buf_print(&iob, "4:path%d:%s", (int)strlen(tp->meta.name),
                 tp->meta.name);
             buf_print(&iob, "2:rdi%lue", tp->net->rate_dwn);
             buf_print(&iob, "2:rui%lue", tp->net->rate_up);
             buf_print(&iob, "12:seen npiecesi%ue", seen_npieces);
-            buf_swrite(&iob, "5:state1:A");
+            buf_print(&iob, "5:statei%ue", tp->state);
             buf_print(&iob, "5:totali%jde", (intmax_t)tp->meta.total_length);
             buf_print(&iob, "2:upi%juee", (intmax_t)tp->net->uploaded);
         } else {
             buf_swrite(&iob, "d4:hash20:");
             buf_write(&iob, tp->meta.info_hash, 20);
-            buf_print(&iob, "3:numi%ue", tp->num);
             buf_print(&iob, "4:path%d:%s", (int)strlen(tp->meta.name),
                 tp->meta.name);
-            switch (tp->state) {
-            case T_INACTIVE:
-                buf_swrite(&iob, "5:state1:Ie");
-                break;
-            case T_STARTING:
-                buf_swrite(&iob, "5:state1:Be");
-                break;
-            case T_STOPPING:
-                buf_swrite(&iob, "5:state1:Ee");
-                break;
-            case T_ACTIVE:
-                abort();
-            }
+            buf_print(&iob, "5:statei%uee", tp->state);
         }
     }
     buf_swrite(&iob, "ee");
-
-    uint32_t len = iob.buf_off;
-    write_fully(cli->sd, &len, sizeof(len));
-    write_fully(cli->sd, iob.buf, iob.buf_off);
-    free(iob.buf);
-    return 0;
+    return write_buffer(cli, &iob);
 }
 
-#if 0
-static void
-cmd_add(int argc, const char *args, FILE *fp)
+static int
+cmd_add(struct cli *cli, int argc, const char *args)
 {
-    struct io_buffer iob;
-    buf_init(&iob, (1 << 10));
-
-    buf_write(&iob, "l", 1);
-    while (args != NULL) {
-        size_t plen;
-        char path[PATH_MAX];
-        const char *pathp;
-
-        if (!benc_isstr(args)) {
-            free(iob.buf);
-            return;
-        }
-
-        benc_str(args, &pathp, &plen, &args);
-
-        if (plen >= PATH_MAX) {
-            buf_print(&iob, "d4:codei%dee", ENAMETOOLONG);
-            continue;
-        }
+    if (argc != 1)
+        return EINVAL;
+    if (btpd_is_stopping())
+        return write_code_buffer(cli, IPC_FAIL);
 
-        bcopy(pathp, path, plen);
-        path[plen] = '\0';
-        btpd_log(BTPD_L_BTPD, "add request for %s.\n", path);
-        buf_print(&iob, "d4:codei%dee", torrent_load(path));
+    size_t hlen;
+    struct torrent *tp;
+    enum ipc_code code = IPC_OK;
+    const uint8_t *hash = benc_dget_mem(args, "hash", &hlen);
+    char *content = benc_dget_str(args, "content", NULL);
+    char *torrent = benc_dget_str(args, "torrent", NULL);
+
+    if (!(hlen == 20 && content != NULL && torrent != NULL)) {
+        code = IPC_COMMERR;
+        goto out;
     }
-    buf_write(&iob, "e", 1);
-
-    uint32_t len = iob.buf_off;
-    fwrite(&len, sizeof(len), 1, fp);
-    fwrite(iob.buf, 1, iob.buf_off, fp);
-    free(iob.buf);
-}
-
-static void
-cmd_del(int argc, const char *args, FILE *fp)
-{
-    struct io_buffer iob;
-    buf_init(&iob, (1 << 10));
-
-    buf_swrite(&iob, "l");
-
-    while (args != NULL) {
-        size_t len;
-        const char *hash;
-        struct torrent *tp;
-
-        if (!benc_isstr(args) ||
-            benc_str(args, &hash, &len, &args) != 0 || len != 20) {
-            free(iob.buf);
-            return;
-        }
-
-        tp = btpd_get_torrent(hash);
-        if (tp != NULL) {
-            btpd_log(BTPD_L_BTPD, "del request for %s.\n", tp->relpath);
-            torrent_unload(tp);
-            buf_swrite(&iob, "d4:codei0ee");
-        } else {
-            btpd_log(BTPD_L_BTPD, "del request didn't match.\n");
-            buf_print(&iob, "d4:codei%dee", ENOENT);
-        }
+    if ((tp = torrent_get(hash)) != NULL) {
+        code = tp->state == T_STOPPING ? IPC_FAIL : IPC_OK;
+        goto out;
     }
-    buf_swrite(&iob, "e");
-
-    uint32_t len = iob.buf_off;
-    fwrite(&len, sizeof(len), 1, fp);
-    fwrite(iob.buf, 1, iob.buf_off, fp);
-    free(iob.buf);
-}
+    if (torrent_set_links(hash, torrent, content) != 0) {
+        code = IPC_ERROR;
+        goto out;
+    }
+    if (torrent_start(hash) != 0)
+        code = IPC_ERROR;
 
-#endif
+out:
+    if (content != NULL)
+        free(content);
+    if (torrent != NULL)
+        free(torrent);
 
-static int
-cmd_die(struct cli *cli, int argc, const char *args)
-{
-    char res[] = "d4:codei0ee";
-    uint32_t len = sizeof(res) - 1;
-    write_fully(cli->sd, &len, sizeof(len));
-    write_fully(cli->sd, res, len);
-    btpd_log(BTPD_L_BTPD, "Someone wants me dead.\n");
-    btpd_shutdown((& (struct timeval) { 0, 0 }));
-    return 0;
+    if (code == IPC_COMMERR)
+        return EINVAL;
+    else
+        return write_code_buffer(cli, code);
 }
 
 static int
-cmd_start(struct cli *cli, int argc, const char *args)
+cmd_del(struct cli *cli, int argc, const char *args)
 {
-    if (argc != 1 || !benc_isint(args))
+    if (argc != 1 || !benc_isstr(args))
         return EINVAL;
 
-    int code;
-    unsigned num;
-    num = benc_int(args, NULL);
-    struct torrent *tp = btpd_get_torrent_num(num);
-    if (tp != NULL) {
-        torrent_activate(tp);
-        code = 0;
-    } else
-        code = 1;
-
-    struct io_buffer iob;
-    buf_init(&iob, 16);
-    buf_print(&iob, "d4:codei%dee", code);
-    uint32_t len = iob.buf_off;
-    write_fully(cli->sd, &len, sizeof(len));
-    write_fully(cli->sd, iob.buf, iob.buf_off);
-    return 0;
+    size_t hlen;
+    uint8_t *hash = (uint8_t *)benc_mem(args, &hlen, NULL);
+    if (hlen != 20)
+        return EINVAL;
+    struct torrent *tp = torrent_get(hash);
+    if (tp != NULL)
+        torrent_stop(tp);
+    return write_code_buffer(cli, IPC_OK);
 }
 
 static int
-cmd_stop(struct cli *cli, int argc, const char *args)
+cmd_die(struct cli *cli, int argc, const char *args)
 {
-    btpd_log(BTPD_L_BTPD, "%d\n", argc);
-    if (argc != 1 || !benc_isint(args))
-        return EINVAL;
-
-    int code;
-    unsigned num;
-    num = benc_int(args, NULL);
-    struct torrent *tp = btpd_get_torrent_num(num);
-    if (tp != NULL) {
-        torrent_deactivate(tp);
-        code = 0;
-    } else
-        code = 1;
-
-    struct io_buffer iob;
-    buf_init(&iob, 16);
-    buf_print(&iob, "d4:codei%dee", code);
-    uint32_t len = iob.buf_off;
-    write_fully(cli->sd, &len, sizeof(len));
-    write_fully(cli->sd, iob.buf, iob.buf_off);
-    return 0;
+    int err = write_code_buffer(cli, IPC_OK);
+    if (!btpd_is_stopping()) {
+        int grace_seconds = -1;
+        if (argc == 1 && benc_isint(args))
+            grace_seconds = benc_int(args, NULL);
+        btpd_log(BTPD_L_BTPD, "Someone wants me dead.\n");
+        btpd_shutdown(grace_seconds);
+    }
+    return err;
 }
 
 static struct {
@@ -233,14 +175,10 @@ static struct {
     int nlen;
     int (*fun)(struct cli *cli, int, const char *);
 } cmd_table[] = {
-#if 0
     { "add",    3, cmd_add },
     { "del",    3, cmd_del },
-#endif
     { "die",    3, cmd_die },
-    { "start",  5, cmd_start }, 
-    { "stat",   4, cmd_stat },
-    { "stop",   4, cmd_stop }
+    { "stat",   4, cmd_stat }
 };
 
 static int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]);
@@ -248,22 +186,19 @@ static int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]);
 static int
 cmd_dispatch(struct cli *cli, const char *buf)
 {
-    int err = 0;
     size_t cmdlen;
     const char *cmd;
     const char *args;
 
     cmd = benc_mem(benc_first(buf), &cmdlen, &args);
 
-    btpd_log(BTPD_L_BTPD, "%.*s\n", (int)cmdlen, cmd);
     for (int i = 0; i < ncmds; i++) {
         if ((cmdlen == cmd_table[i].nlen &&
                 strncmp(cmd_table[i].name, cmd, cmdlen) == 0)) {
-            err = cmd_table[i].fun(cli, benc_nelems(buf) - 1, args);
-            break;
+            return cmd_table[i].fun(cli, benc_nelems(buf) - 1, args);
         }
     }
-    return err;
+    return ENOENT;
 }
 
 static void
diff --git a/btpd/content.c b/btpd/content.c
index f132392..df19849 100644
--- a/btpd/content.c
+++ b/btpd/content.c
@@ -98,14 +98,14 @@ static int
 fd_cb_rd(const char *path, int *fd, void *arg)
 {
     struct torrent *tp = arg;
-    return vopen(fd, O_RDONLY, "library/%s/content/%s", tp->relpath, path);
+    return vopen(fd, O_RDONLY, "torrents/%s/content/%s", tp->relpath, path);
 }
 
 static int
 fd_cb_wr(const char *path, int *fd, void *arg)
 {
     struct torrent *tp = arg;
-    return vopen(fd, O_RDWR|O_CREAT, "library/%s/content/%s", tp->relpath,
+    return vopen(fd, O_RDWR|O_CREAT, "torrents/%s/content/%s", tp->relpath,
         path);
 }
 
@@ -464,7 +464,7 @@ test_hash(struct torrent *tp, uint8_t *hash, uint32_t piece)
         int fd;
         int err;
 
-        err = vopen(&fd, O_RDONLY, "library/%s/torrent", tp->relpath);
+        err = vopen(&fd, O_RDONLY, "torrents/%s/torrent", tp->relpath);
         if (err != 0)
             btpd_err("test_hash: %s\n", strerror(err));
 
@@ -531,7 +531,7 @@ test_torrent(struct torrent *tp, volatile sig_atomic_t *cancel)
     uint8_t (*hashes)[SHA_DIGEST_LENGTH];
     uint8_t hash[SHA_DIGEST_LENGTH];
 
-    if ((err = vfopen(&fp, "r", "library/%s/torrent", tp->relpath)) != 0)
+    if ((err = vfopen(&fp, "r", "torrents/%s/torrent", tp->relpath)) != 0)
         return err;
 
     hashes = btpd_malloc(tp->meta.npieces * SHA_DIGEST_LENGTH);
@@ -573,7 +573,7 @@ stat_and_adjust(struct torrent *tp, struct rstat ret[])
     char path[PATH_MAX];
     struct stat sb;
     for (int i = 0; i < tp->meta.nfiles; i++) {
-        snprintf(path, PATH_MAX, "library/%s/content/%s", tp->relpath,
+        snprintf(path, PATH_MAX, "torrents/%s/content/%s", tp->relpath,
             tp->meta.files[i].path);
 again:
         if (stat(path, &sb) == -1) {
@@ -603,7 +603,7 @@ load_resume(struct torrent *tp, struct rstat sbs[])
     size_t pfsiz = ceil(tp->meta.npieces / 8.0);
     size_t bfsiz = tp->meta.npieces * tp->cm->bppbf;
 
-    if ((err = vfopen(&fp, "r" , "library/%s/resume", tp->relpath)) != 0)
+    if ((err = vfopen(&fp, "r" , "torrents/%s/resume", tp->relpath)) != 0)
         return err;
 
     if (fscanf(fp, "%d\n", &ver) != 1)
@@ -636,7 +636,7 @@ save_resume(struct torrent *tp, struct rstat sbs[])
 {
     int err;
     FILE *fp;
-    if ((err = vfopen(&fp, "wb", "library/%s/resume", tp->relpath)) != 0)
+    if ((err = vfopen(&fp, "wb", "torrents/%s/resume", tp->relpath)) != 0)
         return err;
     fprintf(fp, "%d\n", 1);
     for (int i = 0; i < tp->meta.nfiles; i++)
diff --git a/btpd/main.c b/btpd/main.c
index 2e3e238..e397eee 100644
--- a/btpd/main.c
+++ b/btpd/main.c
@@ -7,7 +7,6 @@
 #include <fcntl.h>
 #include <getopt.h>
 #include <locale.h>
-#include <pwd.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -23,31 +22,17 @@ writepid(int pidfd)
     fclose(fp);
 }
 
-static char *
-find_homedir(void)
-{
-    char *res = getenv("BTPD_HOME");
-    if (res == NULL) {
-        char *home = getenv("HOME");
-        if (home == NULL) {
-            struct passwd *pwent = getpwuid(getuid());
-            if (pwent == NULL)
-                errx(1, "Can't find my home directory.\n");
-            home = pwent->pw_dir;
-            endpwent();
-        }
-        asprintf(&res, "%s/.btpd", home);
-    }
-    return res;
-}
-
 static void
-setup_daemon(const char *dir)
+setup_daemon(int daemonize, const char *dir, const char *log)
 {
     int pidfd;
 
+    if (log == NULL)
+        log = "log";
+
     if (dir == NULL)
-        dir = find_homedir();
+        if ((dir = find_btpd_dir()) == NULL)
+            errx(1, "Cannot find the btpd directory");
 
     btpd_dir = dir;
 
@@ -57,8 +42,8 @@ setup_daemon(const char *dir)
     if (chdir(dir) != 0)
         err(1, "Couldn't change working directory to '%s'", dir);
 
-    if (mkdir("library", 0777) == -1 && errno != EEXIST)
-        err(1, "Couldn't create library");
+    if (mkdir("torrents", 0777) == -1 && errno != EEXIST)
+        err(1, "Couldn't create torrents subdir");
 
     pidfd = open("pid", O_CREAT|O_WRONLY|O_NONBLOCK|O_EXLOCK, 0666);
     if (pidfd == -1) {
@@ -69,12 +54,12 @@ setup_daemon(const char *dir)
             err(1, "Couldn't open 'pid'");
     }
 
-    if (btpd_daemon) {
+    if (daemonize) {
         if (daemon(1, 1) != 0)
             err(1, "Failed to daemonize");
         freopen("/dev/null", "r", stdin);
-        if (freopen("log", "a", stdout) == NULL)
-            err(1, "Couldn't open 'log'");
+        if (freopen(log, "a", stdout) == NULL)
+            err(1, "Couldn't open '%s'", log);
         dup2(fileno(stdout), fileno(stderr));
         setlinebuf(stdout);
         setlinebuf(stderr);
@@ -89,11 +74,7 @@ usage(void)
     printf(
         "The BitTorrent Protocol Daemon.\n"
         "\n"
-        "Usage: btpd [options] [dir]\n"
-        "\n"
-        "Arguments:\n"
-        "dir\n"
-        "\tThe directory in which to run btpd. Default is '$HOME/.btpd'.\n"
+        "Usage: btpd [-d dir] [-p port] [more options...]\n"
         "\n"
         "Options:\n"
         "--bw-in n\n"
@@ -104,9 +85,8 @@ usage(void)
         "\tLimit outgoing BitTorrent traffic to n kB/s.\n"
         "\tDefault is 0 which means unlimited.\n"
         "\n"
-        "-d\n"
-        "\tKeep the btpd process in the foregorund and log to std{out,err}.\n"
-        "\tThis option is intended for debugging purposes.\n"
+        "-d dir\n"
+        "\tThe directory in which to run btpd. Default is '$HOME/.btpd'.\n"
         "\n"
         "--downloaders n\n"
         "\tControls the number of simultaneous uploads.\n"
@@ -119,9 +99,16 @@ usage(void)
         "--help\n"
         "\tShow this text.\n"
         "\n"
+        "--logfile file\n"
+        "\tWhere to put the logfile. By default it's put in the btpd dir.\n"
+        "\n"
         "--max-peers n\n"
         "\tLimit the amount of peers to n.\n"
         "\n"
+        "--no-daemon\n"
+        "\tKeep the btpd process in the foregorund and log to std{out,err}.\n"
+        "\tThis option is intended for debugging purposes.\n"
+        "\n"
         "-p n, --port n\n"
         "\tListen at port n. Default is 6881.\n"
         "\n"
@@ -142,6 +129,8 @@ static struct option longopts[] = {
     { "prealloc", required_argument,    &longval,       3 },
     { "downloaders", required_argument, &longval,       4 },
     { "max-peers", required_argument,   &longval,       5 },
+    { "no-daemon", no_argument,         &longval,       6 },
+    { "logfile", required_argument,     &longval,       7 },
     { "help",   no_argument,            &longval,       128 },
     { NULL,     0,                      NULL,           0 }
 };
@@ -149,16 +138,17 @@ static struct option longopts[] = {
 int
 main(int argc, char **argv)
 {
-    char *dir = NULL;
+    char *dir = NULL, *log = NULL;
+    int daemonize = 1;
 
     setlocale(LC_ALL, "");
 
     for (;;) {
-        switch (getopt_long(argc, argv, "dp:", longopts, NULL)) {
+        switch (getopt_long(argc, argv, "d:p:", longopts, NULL)) {
         case -1:
             goto args_done;
         case 'd':
-            btpd_daemon = 0;
+            dir = optarg;
             break;
         case 'p':
             net_port = atoi(optarg);
@@ -180,6 +170,12 @@ main(int argc, char **argv)
             case 5:
                 net_max_peers = atoi(optarg);
                 break;
+            case 6:
+                daemonize = 0;
+                break;
+            case 7:
+                log = optarg;
+                break;
             default:
                 usage();
             }
@@ -193,12 +189,10 @@ args_done:
     argc -= optind;
     argv += optind;
 
-    if (argc > 1)
-        usage();
     if (argc > 0)
-        dir = argv[0];
+        usage();
 
-    setup_daemon(dir);
+    setup_daemon(daemonize, dir, log);
 
     event_init();
 
diff --git a/btpd/net.c b/btpd/net.c
index 43b43f8..1fc25dc 100644
--- a/btpd/net.c
+++ b/btpd/net.c
@@ -448,9 +448,10 @@ net_connect2(struct sockaddr *sa, socklen_t salen, int *sd)
     set_nonblocking(*sd);
 
     if (connect(*sd, sa, salen) == -1 && errno != EINPROGRESS) {
-        btpd_log(BTPD_L_CONN, "Botched connection %s.", strerror(errno));
+        int err = errno;
+        btpd_log(BTPD_L_CONN, "Botched connection %s.\n", strerror(errno));
         close(*sd);
-        return errno;
+        return err;
     }
 
     return 0;
diff --git a/btpd/opts.c b/btpd/opts.c
index 7321727..2fc3d1c 100644
--- a/btpd/opts.c
+++ b/btpd/opts.c
@@ -1,6 +1,5 @@
 #include <btpd.h>
 
-short btpd_daemon = 1;
 const char *btpd_dir;
 #ifdef DEBUG
 uint32_t btpd_logmask = BTPD_L_ALL;
diff --git a/btpd/opts.h b/btpd/opts.h
index c47ea63..4f48fa2 100644
--- a/btpd/opts.h
+++ b/btpd/opts.h
@@ -1,7 +1,6 @@
 #ifndef BTPD_OPTS_H
 #define BTPD_OPTS_H
 
-extern short btpd_daemon;
 extern const char *btpd_dir;
 extern uint32_t btpd_logmask;
 extern int net_max_downloaders;
diff --git a/btpd/torrent.c b/btpd/torrent.c
index b65b481..1202039 100644
--- a/btpd/torrent.c
+++ b/btpd/torrent.c
@@ -19,14 +19,28 @@
 #include "tracker_req.h"
 #include "stream.h"
 
-static unsigned m_next_num;
+static unsigned m_ntorrents;
+static struct torrent_tq m_torrents = BTPDQ_HEAD_INITIALIZER(m_torrents);
 
-static unsigned
-num_get_next(void)
+const struct torrent_tq *
+torrent_get_all(void)
 {
-    if (m_next_num == UINT_MAX)
-        btpd_err("Reached maximum torrent number.\n");
-    return m_next_num++;
+    return &m_torrents;
+}
+
+unsigned
+torrent_count(void)
+{
+    return m_ntorrents;
+}
+
+struct torrent *
+torrent_get(const uint8_t *hash)
+{
+    struct torrent *tp = BTPDQ_FIRST(&m_torrents);
+    while (tp != NULL && bcmp(hash, tp->meta.info_hash, 20) != 0)
+        tp = BTPDQ_NEXT(tp, entry);
+    return tp;
 }
 
 off_t
@@ -58,22 +72,71 @@ torrent_block_size(struct torrent *tp, uint32_t piece, uint32_t nblocks,
     }
 }
 
-void
-torrent_activate(struct torrent *tp)
+static void
+torrent_relpath(const uint8_t *hash, char *buf)
+{
+    for (int i = 0; i < 20; i++)
+        snprintf(buf + i * 2, 3, "%.2x", hash[i]);
+}
+
+int
+torrent_set_links(const uint8_t *hash, const char *torrent,
+    const char *content)
 {
-    if (tp->state == T_INACTIVE) {
-        tp->state = T_STARTING;
-        cm_start(tp);
-        btpd_tp_activated(tp);
+    char relpath[RELPATH_SIZE];
+    char file[PATH_MAX];
+    torrent_relpath(hash, relpath);
+    snprintf(file, PATH_MAX, "torrents/%s", relpath);
+    if (mkdir(file, 0777) == -1 && errno != EEXIST)
+        return errno;
+    snprintf(file, PATH_MAX, "torrents/%s/torrent", relpath);
+    if (unlink(file) == -1 && errno != ENOENT)
+        return errno;
+    if (symlink(torrent, file) == -1)
+        return errno;
+    snprintf(file, PATH_MAX, "torrents/%s/content", relpath);
+    if (unlink(file) == -1 && errno != ENOENT)
+        return errno;
+    if (symlink(content, file) == -1)
+        return errno;
+    return 0;
+}
+
+int
+torrent_start(const uint8_t *hash)
+{
+    struct torrent *tp;
+    struct metainfo *mi;
+    int error;
+    char relpath[RELPATH_SIZE];
+    char file[PATH_MAX];
+
+    torrent_relpath(hash, relpath);
+    snprintf(file, PATH_MAX, "torrents/%s/torrent", relpath);
+
+    if ((error = load_metainfo(file, -1, 0, &mi)) != 0) {
+        btpd_log(BTPD_L_ERROR, "Couldn't load torrent file %s: %s.\n",
+            file, strerror(error));
+        return error;
     }
+
+    btpd_log(BTPD_L_BTPD, "Starting torrent '%s'.\n", mi->name);
+
+    tp = btpd_calloc(1, sizeof(*tp));
+    bcopy(relpath, tp->relpath, RELPATH_SIZE);
+    tp->meta = *mi;
+    free(mi);
+    BTPDQ_INSERT_TAIL(&m_torrents, tp, entry);
+    m_ntorrents++;
+    cm_start(tp);
+
+    return 0;
 }
 
 void
-torrent_deactivate(struct torrent *tp)
+torrent_stop(struct torrent *tp)
 {
     switch (tp->state) {
-    case T_INACTIVE:
-        break;
     case T_STARTING:
     case T_ACTIVE:
         tp->state = T_STOPPING;
@@ -88,69 +151,43 @@ torrent_deactivate(struct torrent *tp)
         if (tp->tr != NULL)
             tr_destroy(tp);
         break;
-    default:
-        abort();
     }
 }
 
-int
-torrent_load(struct torrent **res, const char *path)
+static void
+torrent_kill(struct torrent *tp)
 {
-    struct metainfo *mi;
-    int error;
-    char file[PATH_MAX];
-    snprintf(file, PATH_MAX, "library/%s/torrent", path);
-
-    if ((error = load_metainfo(file, -1, 0, &mi)) != 0) {
-        btpd_log(BTPD_L_ERROR, "Couldn't load metainfo file %s: %s.\n",
-            file, strerror(error));
-        return error;
-    }
-
-    if (btpd_get_torrent(mi->info_hash) != NULL) {
-        btpd_log(BTPD_L_BTPD,
-            "%s has same hash as an already loaded torrent.\n", path);
-        error = EEXIST;
-    }
-
-    if (error == 0) {
-        *res = btpd_calloc(1, sizeof(**res));
-        (*res)->relpath = strdup(path);
-        (*res)->meta = *mi;
-        (*res)->num = num_get_next();
-        free(mi);
-    } else {
-        clear_metainfo(mi);
-        free(mi);
-    }
-
-    return error;
+    btpd_log(BTPD_L_BTPD, "Removed torrent '%s'.\n", tp->meta.name);
+    assert(m_ntorrents > 0);
+    m_ntorrents--;
+    BTPDQ_REMOVE(&m_torrents, tp, entry);
+    clear_metainfo(&tp->meta);
+    free(tp);
+    if (m_ntorrents == 0)
+        btpd_on_no_torrents();
 }
 
 void
 torrent_on_cm_started(struct torrent *tp)
 {
-    net_add_torrent(tp);
-    tr_start(tp);
     tp->state = T_ACTIVE;
+    net_add_torrent(tp);
+    if (tr_start(tp) != 0)
+        torrent_stop(tp);
 }
 
 void
 torrent_on_cm_stopped(struct torrent *tp)
 {
     assert(tp->state == T_STOPPING);
-    if (tp->tr == NULL) {
-        tp->state = T_INACTIVE;
-        btpd_tp_deactivated(tp);
-    }
+    if (tp->tr == NULL)
+        torrent_kill(tp);
 }
 
 void
 torrent_on_tr_stopped(struct torrent *tp)
 {
     assert(tp->state == T_STOPPING);
-    if (tp->cm == NULL) {
-        tp->state = T_INACTIVE;
-        btpd_tp_deactivated(tp);
-    }
+    if (tp->cm == NULL)
+        torrent_kill(tp);
 }
diff --git a/btpd/torrent.h b/btpd/torrent.h
index 4a1542e..c9a942b 100644
--- a/btpd/torrent.h
+++ b/btpd/torrent.h
@@ -2,17 +2,16 @@
 #define BTPD_TORRENT_H
 
 #define PIECE_BLOCKLEN (1 << 14)
+#define RELPATH_SIZE 41
 
 enum torrent_state {
-    T_INACTIVE,
     T_STARTING,
     T_ACTIVE,
     T_STOPPING
 };
 
 struct torrent {
-    unsigned num;
-    const char *relpath;
+    char relpath[RELPATH_SIZE];
     struct metainfo meta;
 
     enum torrent_state state;
@@ -26,9 +25,14 @@ struct torrent {
 
 BTPDQ_HEAD(torrent_tq, torrent);
 
-int torrent_load(struct torrent **res, const char *path);
-void torrent_activate(struct torrent *tp);
-void torrent_deactivate(struct torrent *tp);
+unsigned torrent_count(void);
+const struct torrent_tq *torrent_get_all(void);
+struct torrent *torrent_get(const uint8_t *hash);
+
+int torrent_start(const uint8_t *hash);
+void torrent_stop(struct torrent *tp);
+int torrent_set_links(const uint8_t *hash, const char *torrent,
+    const char *content);
 
 off_t torrent_piece_size(struct torrent *tp, uint32_t piece);
 uint32_t torrent_piece_blocks(struct torrent *tp, uint32_t piece);
diff --git a/cli/btcli.c b/cli/btcli.c
index d5e20b6..3971d77 100644
--- a/cli/btcli.c
+++ b/cli/btcli.c
@@ -1,66 +1,179 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+
 #include <err.h>
 #include <errno.h>
+#include <fcntl.h>
 #include <getopt.h>
+#include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 #include "btpd_if.h"
+#include "metainfo.h"
+#include "subr.h"
 
-static const char *btpd_dir = "/usr/btpd";
-static struct ipc *ipc;
+const char *btpd_dir;
+struct ipc *ipc;
 
-static void
-handle_ipc_res(enum ipc_code code)
+void
+btpd_connect(void)
+{
+    if ((errno = ipc_open(btpd_dir, &ipc)) != 0)
+        err(1, "cannot open connection to btpd in %s", btpd_dir);
+}
+
+enum ipc_code
+handle_ipc_res(enum ipc_code code, const char *target)
 {
     switch (code) {
     case IPC_OK:
-        return;
+        break;
     case IPC_FAIL:
-        warnx("Ipc failed.\n");
+        warnx("btpd couldn't execute the requested operation for %s", target);
+        break;
+    case IPC_ERROR:
+        warnx("btpd encountered an error for %s", target);
         break;
-    case IPC_COMMERR:
-        errx(1, "Communication error.\n");
+    default:
+        errx(1, "fatal error in communication with btpd");
     }
+    return code;
 }
 
-static void
-btpd_connect(void)
+void
+print_state_name(struct tpstat *ts)
 {
-    if ((errno = ipc_open(btpd_dir, &ipc)) != 0)
-        errx(1, "Couldn't connect to btpd in %s (%s).\n",
-            btpd_dir, strerror(errno));
+    char statec[] = ">*<U";
+    int state = min(ts->state, 3);
+    printf("%c. %s", statec[state], ts->name);
+}
+
+void
+print_stat(struct tpstat *cur)
+{
+    printf("%5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %4u %5.1f%%",
+        100.0 * cur->have / cur->total,
+        (double)cur->downloaded / (1 << 20),
+        (double)cur->rate_down / (20 << 10),
+        (double)cur->uploaded / (1 << 20),
+        (double)cur->rate_up / (20 << 10),
+        cur->npeers,
+        100.0 * cur->nseen / cur->npieces);
+    if (cur->errors > 0)
+        printf(" E%u", cur->errors);
+    printf("\n");
 }
 
 void
 usage_add(void)
 {
     printf(
-        "Add a torrent to btpd.\n"
+        "Add torrents to btpd.\n"
         "\n"
-        "Usage: add [-a] [-s] [-c dir] -f file\n"
-        "\n"
-        "Options:\n"
-        "-a\n"
-        "\tAppend the torrent top directory (if any) to the content path.\n"
+        "Usage: add [--topdir] -d dir file\n"
+        "       add file ...\n"
         "\n"
-        "-c dir\n"
-        "\tThe directory where the content is (or will be downloaded to).\n"
-        "\tDefault is the directory containing the torrent file.\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tOne or more torrents to add.\n"
         "\n"
-        "-f file\n"
-        "\tThe torrent to add.\n"
+        "Options:\n"
+        "-d dir\n"
+        "\tUse the dir for content.\n"
         "\n"
-        "-s\n"
-        "\tStart the torrent.\n"
+        "--topdir\n"
+        "\tAppend the torrent top directory (if any) to the content path.\n"
+        "\tThis option cannot be used without the '-d' option.\n"
         "\n"
         );
     exit(1);
 }
 
+struct option add_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    { "topdir", no_argument, NULL, 'T'},
+    {NULL, 0, NULL, 0}
+};
+
+int
+content_link(uint8_t *hash, char *buf)
+{
+    int n;
+    char relpath[41];
+    char path[PATH_MAX];
+    for (int i = 0; i < 20; i++)
+        snprintf(relpath + i * 2, 3, "%.2x", hash[i]);
+    snprintf(path, PATH_MAX, "%s/torrents/%s/content", btpd_dir, relpath);
+    if ((n = readlink(path, buf, PATH_MAX)) == -1)
+        return errno;
+    buf[min(n, PATH_MAX)] = '\0';
+    return 0;
+}
+
 void
 cmd_add(int argc, char **argv)
 {
+    int ch, topdir = 0;
+    char *dir = NULL, bdir[PATH_MAX];
+
+    while ((ch = getopt_long(argc, argv, "d:", add_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'T':
+            topdir = 1;
+            break;
+        case 'd':
+            dir = optarg;
+            break;
+        default:
+            usage_add();
+        }
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc < 1 || (topdir == 1 && dir == NULL) || (dir != NULL && argc > 1))
+        usage_add();
+
+    if (dir != NULL)
+        if (realpath(dir, bdir) == NULL)
+            err(1, "path error on %s", bdir);
+
+    btpd_connect();
+    for (int i = 0; i < argc; i++) {
+        struct metainfo *mi;
+        char dpath[PATH_MAX], fpath[PATH_MAX];
+
+        if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) {
+            warn("error loading torrent %s", argv[i]);
+            continue;
+        }
+
+        if ((topdir &&
+                !(mi->nfiles == 1
+                    && strcmp(mi->name, mi->files[0].path) == 0)))
+            snprintf(dpath, PATH_MAX, "%s/%s", bdir, mi->name);
+        else if (dir != NULL)
+            strlcpy(dpath, bdir, PATH_MAX);
+        else {
+            if (content_link(mi->info_hash, dpath) != 0) {
+                warnx("unknown content dir for %s", argv[i]);
+                errx(1, "use the '-d' option");
+            }
+        }
+
+        if (mkdir(dpath, 0777) != 0 && errno != EEXIST)
+            err(1, "couldn't create directory %s", dpath);
+
+        if (realpath(argv[i], fpath) == NULL)
+            err(1, "path error on %s", fpath);
+
+        handle_ipc_res(btpd_add(ipc, mi->info_hash, fpath, dpath), argv[1]);
+        clear_metainfo(mi);
+        free(mi);
+    }
 }
 
 void
@@ -69,11 +182,11 @@ usage_del(void)
     printf(
         "Remove torrents from btpd.\n"
         "\n"
-        "Usage: del num ...\n"
+        "Usage: del file ...\n"
         "\n"
         "Arguments:\n"
-        "num\n"
-        "\tThe number of the torrent to remove.\n"
+        "file ...\n"
+        "\tThe torrents to remove.\n"
         "\n");
     exit(1);
 }
@@ -84,16 +197,17 @@ cmd_del(int argc, char **argv)
     if (argc < 2)
         usage_del();
 
-    unsigned nums[argc - 1];
-    char *endptr;
-    for (int i = 0; i < argc - 1; i++) {
-        nums[i] = strtoul(argv[i + 1], &endptr, 10);
-        if (strlen(argv[i + 1]) > endptr - argv[i + 1])
-            usage_del();
-    }
     btpd_connect();
-    for (int i = 0; i < argc -1; i++)
-        handle_ipc_res(btpd_del_num(ipc, nums[i]));
+    for (int i = 1; i < argc; i++) {
+        struct metainfo *mi;
+        if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) {
+            warn("error loading torrent %s", argv[i]);
+            continue;
+        }
+        handle_ipc_res(btpd_del(ipc, mi->info_hash), argv[i]);
+        clear_metainfo(mi);
+        free(mi);
+    }
 }
 
 void
@@ -118,24 +232,23 @@ cmd_kill(int argc, char **argv)
 {
     int seconds = -1;
     char *endptr;
-    if (argc == 1)
-        ;
-    else if (argc == 2) {
+
+    if (argc == 2) {
         seconds = strtol(argv[1], &endptr, 10);
         if (strlen(argv[1]) > endptr - argv[1] || seconds < 0)
             usage_kill();
-    } else
+    } else if (argc > 2)
         usage_kill();
 
     btpd_connect();
-    btpd_die(ipc, seconds);
+    handle_ipc_res(btpd_die(ipc, seconds), "kill");
 }
 
 void
 usage_list(void)
 {
     printf(
-        "List btpd's torrents.\n"
+        "List active torrents.\n"
         "\n"
         "Usage: list\n"
         "\n"
@@ -152,12 +265,13 @@ cmd_list(int argc, char **argv)
         usage_list();
 
     btpd_connect();
-    if ((errno = btpd_stat(ipc, &st)) != 0)
-        err(1, "btpd_stat");
-    for (int i = 0; i < st->ntorrents; i++)
-        printf("%u. %s (%c)\n", st->torrents[i].num, st->torrents[i].name,
-            st->torrents[i].state);
-    printf("Listed %u torrent%s.\n", st->ntorrents,
+    if (handle_ipc_res(btpd_stat(ipc, &st), "list") != IPC_OK)
+        exit(1);
+    for (int i = 0; i < st->ntorrents; i++) {
+        print_state_name(&st->torrents[i]);
+        putchar('\n');
+    }
+    printf("%u torrent%s.\n", st->ntorrents,
         st->ntorrents == 1 ? "" : "s");
 }
 
@@ -166,9 +280,9 @@ usage_stat(void)
 {
     printf(
         "Display stats for active torrents.\n"
-        "The stats displayed are:\n"
+        "The displayed stats are:\n"
         "%% got, MB down, rate down. MB up, rate up\n"
-        "peers, %% of pieces seen, tracker errors\n"
+        "peer count, %% of pieces seen, tracker errors\n"
         "\n"
         "Usage: stat [-i] [-w seconds]\n"
         "\n"
@@ -182,23 +296,6 @@ usage_stat(void)
     exit(1);
 }
 
-void
-print_stat(struct tpstat *cur)
-{
-    printf("%5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %4u %5.1f%%",
-        100.0 * cur->have / cur->total,
-        (double)cur->downloaded / (1 << 20),
-        (double)cur->rate_down / (20 << 10),
-        (double)cur->uploaded / (1 << 20),
-        (double)cur->rate_up / (20 << 10),
-        cur->npeers,
-        100.0 * cur->nseen / cur->npieces
-        );
-    if (cur->errors > 0)
-        printf(" E%u", cur->errors);
-    printf("\n");
-}
-
 void
 do_stat(int individual, int seconds)
 {
@@ -206,13 +303,11 @@ do_stat(int individual, int seconds)
     struct tpstat tot;
 again:
     bzero(&tot, sizeof(tot));
-    tot.num = -1;
-    if ((errno = btpd_stat(ipc, &st)) != 0)
-        err(1, "btpd_stat");
+    tot.state = T_ACTIVE;
+    if (handle_ipc_res(btpd_stat(ipc, &st), "stat") != IPC_OK)
+        exit(1);
     for (int i = 0; i < st->ntorrents; i++) {
         struct tpstat *cur = &st->torrents[i];
-        if (cur->state != 'A')
-            continue;
         tot.uploaded += cur->uploaded;
         tot.downloaded += cur->downloaded;
         tot.rate_up += cur->rate_up;
@@ -223,7 +318,8 @@ again:
         tot.have += cur->have;
         tot.total += cur->total;
         if (individual) {
-            printf("%u. %s:\n", cur->num, cur->name);
+            print_state_name(cur);
+            printf(":\n");
             print_stat(cur);
         }
     }
@@ -237,8 +333,8 @@ again:
     }
 }
 
-static struct option stat_opts [] = {
-    { "help", no_argument, NULL, 1 },
+struct option stat_opts [] = {
+    { "help", no_argument, NULL, 'H' },
     {NULL, 0, NULL, 0}
 };
 
@@ -272,73 +368,7 @@ cmd_stat(int argc, char **argv)
     do_stat(iflag, seconds);
 }
 
-void
-usage_start(void)
-{
-    printf(
-        "Activate torrents.\n"
-        "\n"
-        "Usage: start num ...\n"
-        "\n"
-        "Arguments:\n"
-        "num\n"
-        "\tThe number of the torrent to activate.\n"
-        "\n");
-    exit(1);
-}
-
-void
-cmd_start(int argc, char **argv)
-{
-    if (argc < 2)
-        usage_start();
-
-    unsigned nums[argc - 1];
-    char *endptr;
-    for (int i = 0; i < argc - 1; i++) {
-        nums[i] = strtoul(argv[i + 1], &endptr, 10);
-        if (strlen(argv[i + 1]) > endptr - argv[i + 1])
-            usage_start();
-    }
-    btpd_connect();
-    for (int i = 0; i < argc -1; i++)
-        handle_ipc_res(btpd_start_num(ipc, nums[i]));
-}
-
-void
-usage_stop(void)
-{
-    printf(
-        "Deactivate torrents.\n"
-        "\n"
-        "Usage: stop num ...\n"
-        "\n"
-        "Arguments:\n"
-        "num\n"
-        "\tThe number of the torrent to deactivate.\n"
-        "\n");
-    exit(1);
-}
-
-void
-cmd_stop(int argc, char **argv)
-{
-    if (argc < 2)
-        usage_stop();
-
-    unsigned nums[argc - 1];
-    char *endptr;
-    for (int i = 0; i < argc - 1; i++) {
-        nums[i] = strtoul(argv[i + 1], &endptr, 10);
-        if (strlen(argv[i + 1]) > endptr - argv[i + 1])
-            usage_stop();
-    }
-    btpd_connect();
-    for (int i = 0; i < argc -1; i++)
-        handle_ipc_res(btpd_stop_num(ipc, nums[i]));
-}
-
-static struct {
+struct {
     const char *name;
     void (*fun)(int, char **);
     void (*help)(void);
@@ -347,19 +377,16 @@ static struct {
     { "del", cmd_del, usage_del },
     { "kill", cmd_kill, usage_kill },
     { "list", cmd_list, usage_list },
-    { "start", cmd_start, usage_start },
-    { "stat", cmd_stat, usage_stat },
-    { "stop", cmd_stop, usage_stop }
+    { "stat", cmd_stat, usage_stat }
 };
 
-static int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]);
+int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]);
 
 void
 usage(void)
 {
     printf(
-        "btcli is the btpd command line interface. Use this tool to interact\n"
-        "with a btpd process.\n"
+        "btcli is the btpd command line interface.\n"
         "\n"
         "Usage: btcli [main options] command [command options]\n"
         "\n"
@@ -375,15 +402,13 @@ usage(void)
         "del\n"
         "kill\n"
         "list\n"
-        "start\n"
         "stat\n"
-        "stop\n"
         "\n");
     exit(1);
 }
 
-static struct option base_opts [] = {
-    { "help", no_argument, NULL, 1 },
+struct option base_opts [] = {
+    { "help", no_argument, NULL, 'H' },
     {NULL, 0, NULL, 0}
 };
 
@@ -400,7 +425,7 @@ main(int argc, char **argv)
         case 'd':
             btpd_dir = optarg;
             break;
-        case 1:
+        case 'H':
             help = 1;
             break;
         default:
@@ -413,6 +438,10 @@ main(int argc, char **argv)
     if (argc == 0)
         usage();
 
+    if (btpd_dir == NULL)
+        if ((btpd_dir = find_btpd_dir()) == NULL)
+            errx(1, "cannot find the btpd directory");
+
     optind = 0;
     int found = 0;
     for (int i = 0; !found && i < ncmds; i++) {
@@ -424,7 +453,7 @@ main(int argc, char **argv)
                 cmd_table[i].fun(argc, argv);
         }
     }
-    
+
     if (!found)
         usage();
 
diff --git a/cli/btpd_if.c b/cli/btpd_if.c
index a5fa40d..c328fb6 100644
--- a/cli/btpd_if.c
+++ b/cli/btpd_if.c
@@ -1,3 +1,7 @@
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
 #include <ctype.h>
 #include <err.h>
 #include <errno.h>
@@ -81,7 +85,7 @@ ipc_response(struct ipc *ipc, char **out, uint32_t *len)
     *len = size;
     return 0;
 }
- 
+
 static int
 ipc_req_res(struct ipc *ipc, const char *req, uint32_t qlen, char **res,
     uint32_t *rlen)
@@ -125,7 +129,7 @@ btpd_die(struct ipc *ipc, int seconds)
     if (seconds >= 0)
         buf_print(&iob, "l3:diei%dee", seconds);
     else
-        buf_print(&iob, "l3:diee");
+        buf_swrite(&iob, "l3:diee");
     return ipc_buf_req(ipc, &iob);
 }
 
@@ -150,21 +154,18 @@ parse_btstat(const uint8_t *res, struct btstat **out)
     int i = 0;
     for (const char *tp = benc_first(tlst); tp != NULL; tp = benc_next(tp)) {
         struct tpstat *ts = &st->torrents[i];
-        ts->num = benc_dget_int(tp, "num");
         ts->name = benc_dget_str(tp, "path", NULL);
-        ts->state = *benc_dget_str(tp, "state", NULL);
-        if (ts->state == 'A') {
-            ts->errors = benc_dget_int(tp, "errors");
-            ts->npieces = benc_dget_int(tp, "npieces");
-            ts->nseen = benc_dget_int(tp, "seen npieces");
-            ts->npeers = benc_dget_int(tp, "npeers");
-            ts->downloaded = benc_dget_int(tp, "downloaded");
-            ts->uploaded = benc_dget_int(tp, "uploaded");
-            ts->rate_down = benc_dget_int(tp, "rd");
-            ts->rate_up = benc_dget_int(tp, "ru");
-            ts->have = benc_dget_int(tp, "have");
-            ts->total = benc_dget_int(tp, "total");
-        }
+        ts->state = benc_dget_int(tp, "state");
+        ts->errors = benc_dget_int(tp, "errors");
+        ts->npieces = benc_dget_int(tp, "npieces");
+        ts->nseen = benc_dget_int(tp, "seen npieces");
+        ts->npeers = benc_dget_int(tp, "npeers");
+        ts->downloaded = benc_dget_int(tp, "down");
+        ts->uploaded = benc_dget_int(tp, "up");
+        ts->rate_down = benc_dget_int(tp, "rd");
+        ts->rate_up = benc_dget_int(tp, "ru");
+        ts->have = benc_dget_int(tp, "have");
+        ts->total = benc_dget_int(tp, "total");
         i++;
     }
     *out = st;
@@ -197,29 +198,26 @@ btpd_stat(struct ipc *ipc, struct btstat **out)
     return err;
 }
 
-static enum ipc_code
-btpd_common_num(struct ipc *ipc, const char *cmd, unsigned num)
-{
-    struct io_buffer iob;
-    buf_init(&iob, 16);
-    buf_print(&iob, "l%d:%si%uee", (int)strlen(cmd), cmd, num);
-    return ipc_buf_req(ipc, &iob);    
-}
-
 enum ipc_code
-btpd_del_num(struct ipc *ipc, unsigned num)
+btpd_add(struct ipc *ipc, const uint8_t *hash, const char *torrent,
+    const char *content)
 {
-    return btpd_common_num(ipc, "del", num);
-}
-
-enum ipc_code
-btpd_start_num(struct ipc *ipc, unsigned num)
-{
-    return btpd_common_num(ipc, "start", num);
+    struct io_buffer iob;
+    buf_init(&iob, (1 << 10));
+    buf_print(&iob, "l3:addd7:content%d:%s4:hash20:", (int)strlen(content),
+        content);
+    buf_write(&iob, hash, 20);
+    buf_print(&iob, "7:torrent%d:%see", (int)strlen(torrent), torrent);
+    return ipc_buf_req(ipc, &iob);
 }
 
 enum ipc_code
-btpd_stop_num(struct ipc *ipc, unsigned num)
+btpd_del(struct ipc *ipc, const uint8_t *hash)
 {
-    return btpd_common_num(ipc, "stop", num);
+    struct io_buffer iob;
+    buf_init(&iob, 32);
+    buf_swrite(&iob, "l3:del20:");
+    buf_write(&iob, hash, 20);
+    buf_write(&iob, "e", 1);
+    return ipc_buf_req(ipc, &iob);
 }
diff --git a/cli/btpd_if.h b/cli/btpd_if.h
index 64cdb4b..8c35cc7 100644
--- a/cli/btpd_if.h
+++ b/cli/btpd_if.h
@@ -1,15 +1,18 @@
 #ifndef BTPD_IF_H
 #define BTPD_IF_H
 
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <sys/un.h>
-
 struct ipc;
 
+enum torrent_state { //XXX: Same as in btpd/torrent.h
+    T_STARTING,
+    T_ACTIVE,
+    T_STOPPING
+};
+
 enum ipc_code {
     IPC_OK,
     IPC_FAIL,
+    IPC_ERROR,
     IPC_COMMERR
 };
 
@@ -17,8 +20,7 @@ struct btstat {
     unsigned ntorrents;
     struct tpstat {
         char *name;
-        unsigned num;
-        char state;
+        enum torrent_state state;
 
         unsigned errors;
         unsigned npeers;
@@ -32,13 +34,11 @@ struct btstat {
 int ipc_open(const char *dir, struct ipc **out);
 int ipc_close(struct ipc *ipc);
 
+enum ipc_code btpd_add(struct ipc *ipc, const uint8_t *hash,
+    const char *torrent, const char *content);
+enum ipc_code btpd_del(struct ipc *ipc, const uint8_t *hash);
 enum ipc_code btpd_die(struct ipc *ipc, int seconds);
 enum ipc_code btpd_stat(struct ipc *ipc, struct btstat **out);
-
-enum ipc_code btpd_del_num(struct ipc *ipc, unsigned num);
-enum ipc_code btpd_start_num(struct ipc *ipc, unsigned num);
-enum ipc_code btpd_stop_num(struct ipc *ipc, unsigned num);
-
 void free_btstat(struct btstat *stat);
 
 #endif