#include #include #include #include #include #include #include #include #include #include #include #include #include #include "btpd_if.h" #include "metainfo.h" #include "subr.h" const char *btpd_dir; struct ipc *ipc; 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: break; case IPC_FAIL: warnx("btpd couldn't execute the requested operation for %s", target); break; case IPC_ERROR: warnx("btpd encountered an error for %s", target); break; default: errx(1, "fatal error in communication with btpd"); } return code; } char state_char(struct tpstat *ts) { switch (ts->state) { case T_STARTING: return '+'; case T_ACTIVE: return ts->pieces_got == ts->torrent_pieces ? 'S' : 'L'; case T_STOPPING: return '-'; default: return ' '; } } void print_stat(struct tpstat *ts) { printf("%c %5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %4u %5.1f%%", state_char(ts), floor(1000.0 * ts->content_got / ts->content_size) / 10, (double)ts->downloaded / (1 << 20), (double)ts->rate_down / (20 << 10), (double)ts->uploaded / (1 << 20), (double)ts->rate_up / (20 << 10), ts->peers, floor(1000.0 * ts->pieces_seen / ts->torrent_pieces) / 10); if (ts->tr_errors > 0) printf(" E%u", ts->tr_errors); printf("\n"); } void usage_add(void) { printf( "Add torrents to btpd.\n" "\n" "Usage: add [--topdir] -d dir file\n" " add file ...\n" "\n" "Arguments:\n" "file ...\n" "\tOne or more torrents to add.\n" "\n" "Options:\n" "-d dir\n" "\tUse the dir for content.\n" "\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; 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(); btpd_connect(); for (int i = 0; i < argc; i++) { struct metainfo *mi; char rdpath[PATH_MAX], 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", dir, mi->name); else if (dir != NULL) strncpy(dpath, dir, 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(dpath, rdpath) == NULL) err(1, "path error on %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, rdpath), argv[i]); clear_metainfo(mi); free(mi); } } void usage_del(void) { printf( "Remove torrents from btpd.\n" "\n" "Usage: del file ...\n" "\n" "Arguments:\n" "file ...\n" "\tThe torrents to remove.\n" "\n"); exit(1); } void cmd_del(int argc, char **argv) { if (argc < 2) usage_del(); btpd_connect(); 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 usage_kill(void) { printf( "Shutdown btpd.\n" "\n" "Usage: kill [seconds]\n" "\n" "Arguments:\n" "seconds\n" "\tThe number of seconds btpd waits before giving up on unresponsive\n" "\ttrackers.\n" "\n" ); exit(1); } void cmd_kill(int argc, char **argv) { int seconds = -1; char *endptr; if (argc == 2) { seconds = strtol(argv[1], &endptr, 10); if (strlen(argv[1]) > endptr - argv[1] || seconds < 0) usage_kill(); } else if (argc > 2) usage_kill(); btpd_connect(); handle_ipc_res(btpd_die(ipc, seconds), "kill"); } void usage_list(void) { printf( "List active torrents.\n" "\n" "Usage: list\n" "\n" ); exit(1); } void cmd_list(int argc, char **argv) { struct btstat *st; if (argc > 1) usage_list(); btpd_connect(); if (handle_ipc_res(btpd_stat(ipc, &st), "list") != IPC_OK) exit(1); for (int i = 0; i < st->ntorrents; i++) { struct tpstat *ts = &st->torrents[i]; printf("%c. %s\n", state_char(ts), ts->name); } printf("%u torrent%s.\n", st->ntorrents, st->ntorrents == 1 ? "" : "s"); } void usage_stat(void) { printf( "Display stats for active torrents.\n" "The displayed stats are:\n" "%% got, MB down, rate down. MB up, rate up\n" "peer count, %% of pieces seen, tracker errors\n" "\n" "Usage: stat [-i] [-w seconds] [file ...]\n" "\n" "Arguments:\n" "file ...\n" "\tOnly display stats for the given torrent(s).\n" "\n" "Options:\n" "-i\n" "\tDisplay individual lines for each torrent.\n" "\n" "-w n\n" "\tDisplay stats every n seconds.\n" "\n"); exit(1); } void do_stat(int individual, int seconds, int hash_count, uint8_t (*hashes)[20]) { struct btstat *st; struct tpstat tot; again: bzero(&tot, sizeof(tot)); tot.state = -1; 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 (hash_count > 0) { int found = 0; for (int h = 0; !found && h < hash_count; h++) if (bcmp(cur->hash, hashes[h], 20) == 0) found = 1; if (!found) continue; } tot.uploaded += cur->uploaded; tot.downloaded += cur->downloaded; tot.rate_up += cur->rate_up; tot.rate_down += cur->rate_down; tot.peers += cur->peers; tot.pieces_seen += cur->pieces_seen; tot.torrent_pieces += cur->torrent_pieces; tot.content_got += cur->content_got; tot.content_size += cur->content_size; if (cur->tr_errors > 0) tot.tr_errors++; if (individual) { printf("%s:\n", cur->name); print_stat(cur); } } free_btstat(st); if (individual) printf("Total:\n"); print_stat(&tot); if (seconds > 0) { sleep(seconds); goto again; } } struct option stat_opts [] = { { "help", no_argument, NULL, 'H' }, {NULL, 0, NULL, 0} }; void cmd_stat(int argc, char **argv) { int ch; int wflag = 0, iflag = 0, seconds = 0; uint8_t (*hashes)[20] = NULL; char *endptr; while ((ch = getopt_long(argc, argv, "iw:", stat_opts, NULL)) != -1) { switch (ch) { case 'i': iflag = 1; break; case 'w': wflag = 1; seconds = strtol(optarg, &endptr, 10); if (strlen(optarg) > endptr - optarg || seconds < 1) usage_stat(); break; default: usage_stat(); } } argc -= optind; argv += optind; if (argc > 0) { hashes = malloc(argc * 20); for (int i = 0; i < argc; i++) { struct metainfo *mi; if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) err(1, "error loading torrent %s", argv[i]); bcopy(mi->info_hash, hashes[i], 20); clear_metainfo(mi); free(mi); } } btpd_connect(); do_stat(iflag, seconds, argc, hashes); } struct { const char *name; void (*fun)(int, char **); void (*help)(void); } cmd_table[] = { { "add", cmd_add, usage_add }, { "del", cmd_del, usage_del }, { "kill", cmd_kill, usage_kill }, { "list", cmd_list, usage_list }, { "stat", cmd_stat, usage_stat } }; int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]); void usage(void) { printf( "btcli is the btpd command line interface.\n" "\n" "Usage: btcli [main options] command [command options]\n" "\n" "Main options:\n" "-d dir\n" "\tThe btpd directory.\n" "\n" "--help [command]\n" "\tShow this text or help for the specified command.\n" "\n" "Commands:\n" "add\n" "del\n" "kill\n" "list\n" "stat\n" "\n"); exit(1); } struct option base_opts [] = { { "help", no_argument, NULL, 'H' }, {NULL, 0, NULL, 0} }; int main(int argc, char **argv) { int ch, help = 0; if (argc < 2) usage(); while ((ch = getopt_long(argc, argv, "+d:", base_opts, NULL)) != -1) { switch (ch) { case 'd': btpd_dir = optarg; break; case 'H': help = 1; break; default: usage(); } } argc -= optind; argv += optind; 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++) { if (strcmp(argv[0], cmd_table[i].name) == 0) { found = 1; if (help) cmd_table[i].help(); else cmd_table[i].fun(argc, argv); } } if (!found) usage(); return 0; }