#include #include #include #include #include #include #include #include #include #include #include #include #define DATABASE_PATH (FTAG_ROOT "/ftag.sqlite3") /* Structure that aims at making parsing commands and sub-commands options * easy. The work is done by the parse_args function. */ struct ftag_command { const char *name; /* Execute the command, eventually by parsing some options. FUNC may * shift ARGC and ARGV and call parse_args again. */ void (*func)(int argc, char **argv); }; /* Parse arguments using an array of available commands. ARGV[0] has to match * the "name" field of an entry of COMMANDS. */ static void parse_args(int argc, char ** argv, const struct ftag_command *commands, int command_count); static void __sqlite3_check(int rc, sqlite3 *db, const char *file, int line, const char *func) { if (rc == SQLITE_OK) return; fprintf(stderr, "%s:%d: %s: %s\n", file, line, func, sqlite3_errmsg(db)); sqlite3_close(db); exit(EXIT_FAILURE); } #define sqlite3_check(RC, DB) __sqlite3_check(RC, DB, __FILE__, __LINE__, __func__) static int file_exists(const char *path) { struct stat statbuf __attribute__((unused)); int rc = stat(path, &statbuf); if (rc == 0) return 1; else return 0; } static void assert_db_exists(void) { errno = 0; if (file_exists(DATABASE_PATH) && (errno == 0)) return; if (errno == ENOENT) { fprintf(stderr, "ftag: database not found at \"%s\", " "have you run \"ftag init\"?\n", DATABASE_PATH); } else { perror(DATABASE_PATH); } exit(EXIT_FAILURE); } /* Convert heap-allocated *STR to C string to SQL string, essentially by adding * single quotes to escape single quotes. */ static void sanitize_sql_str(char **str) { int nt = 0; /* total number to insert */ int len = strlen(*str); char *new_str; for (int i = 0; i < len; i++) { if ((*str)[i] == '\'') nt++; } if (nt == 0) return; new_str = malloc(len + 1 + nt); assert(new_str); int ni = 0; /* number inserted so far */ for (int i = 0; i < len; i++) { if ((*str)[i] == '\'') { new_str[i+ni] = '\''; ni++; } new_str[i+ni] = (*str)[i]; } free(*str); *str = new_str; } /* Compute something that identifies the content of FILE and write it to OUT. Algorithm from http://www.cse.yorku.ca/~oz/hash.html via https://stackoverflow.com/a/7666577/20138083. Return 0 on success, -1 on error. */ static int sum(uint64_t *out, const char *file) { FILE *stream = fopen(file, "r"); uint8_t buf[4096]; size_t buf_len; uint64_t sum = 5381; if (!file) { perror("fopen"); return -1; } while ((buf_len = fread(&buf, 1, sizeof(buf), stream)) != 0) { for (int i = 0; i < buf_len; i++) { uint8_t next = buf[i]; sum = sum*33 + next; } } if (ferror(stream)) { perror("fread"); fclose(stream); return -1; } fclose(stream); *out = sum; return 0; } static time_t time_from_str(const char *str) { int rc; struct tm tm; memset(&tm, 0, sizeof(tm)); rc = sscanf(str, "%d-%d-%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday); if (rc <= 0) { perror("sccanf"); exit(EXIT_FAILURE); } printf("%s: debug: year is %d\n", __func__, tm.tm_year); printf("%s: debug: month is %d\n", __func__, tm.tm_mon); printf("%s: debug: day is %d\n", __func__, tm.tm_mday); tm.tm_year -= 1900; tm.tm_mon -= 1; return mktime(&tm); } /* Write an id to *_ID, used by table_next_id. Write -1 if no id is found. */ static int table_next_id_callback(void *_id, int, char **cols, char **) { /* output has only one column, so our id is in COLS[0] */ int *id = _id; if (cols[0]) *id = atoi(cols[0]); else *id = -1; return 0; } /* Get the next id for inserting a row in TABLE. */ static int table_next_id(sqlite3 *db, const char *table) { int last_id = -1; /* last ID _currently used_ in the table */ int rc; char sql[128]; memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "SELECT MAX(id) FROM %s;", table); rc = sqlite3_exec(db, sql, table_next_id_callback, &last_id, NULL); sqlite3_check(rc, db); printf("%s: debug: last id for table \"%s\" is %d\n", __func__, table, last_id); return last_id + 1; } /* Create ftag's database and directories. */ static void ftag_init(int, char **) { int rc = mkdir(FTAG_ROOT "/files", 0755); if (rc == -1) { perror(FTAG_ROOT "/files"); exit(EXIT_FAILURE); } char cmd[1024]; memset(cmd, 0, sizeof(cmd)); snprintf(cmd, sizeof(cmd)-1, "sqlite3 %s < %s", DATABASE_PATH, FTAG_ROOT "/sql/init.sql"); execl("/usr/bin/sh", "/usr/bin/sh", "-c", cmd, NULL); perror("exec"); exit(EXIT_FAILURE); } /* Sqlite callback that prints the first column without header. */ static int ftag_print(void *, int, char **cols, char **) { assert(cols[0]); printf("%s\n", cols[0]); return 0; } static void ftag_list_table(const char *table, const char *col) { char sql[64]; sqlite3 *db = NULL; int rc; rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "SELECT %s FROM %s;", col, table); rc = sqlite3_exec(db, sql, ftag_print, NULL, NULL); sqlite3_check(rc, db); sqlite3_close(db); } /* Write to OUT a version of IN that does not contain any whitespace character * or uppercase letter. */ static void canonicalize(char *out, const char *in) { int i; for (i = 0; i < strlen(in); i++) { if (in[i] >= 'A' && in[i] <= 'Z') out[i] = in[i] - 'A' + 'a'; else if (in[i] == ' ') out[i] = '_'; else out[i] = in[i]; } out[i] = '\0'; } /* Add a new file to the databse, prompting the user for needed information. */ static void ftag_add_one_file(sqlite3 *db, int *next_id, const char *file) { char sql[2048]; int rc; char *full_name = NULL; char *canonical_name = NULL; char *description = NULL; time_t date = time(NULL); size_t line_len = 0; ssize_t read_len; if (!file_exists(file)) { perror(file); exit(EXIT_FAILURE); } printf("ftag file add: adding file \"%s\" to database\n", file); printf("Enter the full name, a version of the name that\n" "may contain blanks and every type of character.\n"); read_len = getline(&full_name, &line_len, stdin); if (read_len == -1) { perror("getline"); exit(EXIT_FAILURE); } sanitize_sql_str(&full_name); full_name[read_len-1] = '\0'; line_len = read_len+1; canonical_name = malloc(line_len); canonicalize(canonical_name, full_name); printf("Enter the canonical name, a version of the name that\n" "ideally contains no blank and, if possible, no upper\n" "case letters and no symbols. If no input is given,\n" "canonical name will be \"%s\".\n", canonical_name); read_len = getline(&canonical_name, &line_len, stdin); if (read_len == -1) { perror("getline"); exit(EXIT_FAILURE); } if (canonical_name[0] == '\n') canonicalize(canonical_name, full_name); else canonical_name[read_len-1] = '\0'; sanitize_sql_str(&canonical_name); printf("Enter the description.\n"); line_len = 0; read_len = getline(&description, &line_len, stdin); if (read_len == -1) { perror("getline"); exit(EXIT_FAILURE); } if (read_len == 1) { free(description); description = strdup(""); } else { description[read_len-1] = '\0'; } sanitize_sql_str(&description); line_len = 64; char *date_str = malloc(line_len); struct tm *now_tm = localtime(&date); strftime(date_str, line_len-1, "%Y-%m-%d", now_tm); printf("Enter the date in the format YYYY-MM-DD, day and month may be " "omitted. If no input is\n" "given, date will be \"%s\".\n", date_str); read_len = getline(&date_str, &line_len, stdin); if (read_len == -1) { perror("getline"); exit(EXIT_FAILURE); } if (read_len > 1) { date = time_from_str(date_str); } memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "INSERT INTO files VALUES(%d, '%s', '%s', '%s', %ld)", *next_id, canonical_name, full_name, description, date); rc = sqlite3_exec(db, sql, NULL, NULL, NULL); sqlite3_check(rc, db); (*next_id)++; char new_path[512]; memset(new_path, 0, sizeof(new_path)); snprintf(new_path, sizeof(new_path)-1, "%s/files/%s", FTAG_ROOT, canonical_name); int out_fd = open(new_path, O_WRONLY | O_CREAT, 0644); if (out_fd == -1) { perror("open"); exit(EXIT_FAILURE); } int in_fd = open(file, O_RDONLY); if (in_fd == -1) { perror("open"); exit(EXIT_FAILURE); } ssize_t written_bytes; while ((written_bytes = sendfile(out_fd, in_fd, NULL, 4096)) == 4096) ; if (written_bytes == -1) { perror("sendfile"); exit(EXIT_FAILURE); } close(in_fd); close(out_fd); free(date_str); free(full_name); free(canonical_name); free(description); } /* Add new files to the database. */ static void ftag_file_add(int argc, char **argv) { if (argc == 0) { fprintf(stderr, "ftag file add: must supply file names\n"); exit(EXIT_FAILURE); } sqlite3 *db = NULL; int rc; rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); int next_id = table_next_id(db, "files"); for (int i = 0; i < argc; i++) { ftag_add_one_file(db, &next_id, argv[i]); } sqlite3_close(db); } static void ftag_file_list(int argc, char **argv) { ftag_list_table("files", "full_name"); } static int get_id_by_col_callback(void *_id, int, char **cols, char **) { int *id = _id; if (cols[0]) { *id = atoi(cols[0]); return 0; } else { return 1; } } /* Write to *_ID the id of the entry of TABLE that has the value VAL for column * COL. */ static int get_id_by_col(sqlite3 *db, const char *table, const char *col, const char *val) { int id = -1; char sql[128]; memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "SELECT id FROM %s WHERE %s = '%s';", table, col, val); int rc = sqlite3_exec(db, sql, get_id_by_col_callback, &id, NULL); sqlite3_check(rc, db); assert(id >= 0); return id; } static void ftag_file_tag(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "ftag file tag: must supply a file name and tags\n"); exit(EXIT_FAILURE); } const char *file = argv[0]; char sql[64]; sqlite3 *db; int rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); int file_id = get_id_by_col(db, "files", "canonical_name", file); printf("%s: debug: file id is %d\n", __func__, file_id); for (int i = 1; i < argc; i++) { int tag_id = get_id_by_col(db, "tags", "name", argv[i]); printf("%s: debug: tag id is %d\n", __func__, tag_id); memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "INSERT INTO file_tags values(%d, %d);", file_id, tag_id); rc = sqlite3_exec(db, sql, NULL, NULL, NULL); sqlite3_check(rc, db); } sqlite3_close(db); } static void ftag_file(int argc, char **argv) { assert_db_exists(); const struct ftag_command file_commands[] = { {.name = "add", .func = ftag_file_add}, {.name = "list", .func = ftag_file_list}, {.name = "tag", .func = ftag_file_tag} /* TODO: add help command */ }; const int file_command_count = sizeof(file_commands)/ sizeof(struct ftag_command); parse_args(argc, argv, file_commands, file_command_count); } static void ftag_help(int, char **) { printf("Usage: ftag COMMAND [ARG]...\n"); printf("Available values for COMMAND:\n"); printf(" init initialize the database\n"); printf(" file manage files\n"); printf(" help print this message\n"); printf(" query query ftag database\n"); printf(" tag manage tags\n"); printf("Some commands also have their own help, try \"ftag COMMAND help\"\n"); printf("Configuration:\n"); printf(" root %s\n", FTAG_ROOT "/"); printf(" database %s\n", DATABASE_PATH); printf(" file storage %s\n", FTAG_ROOT "/files/"); } static void ftag_query_usage(void) { fprintf(stderr, "usage: ftag query [-a DATE] [-b DATE] [-t TAG]...\n"); } /* Debugging callback that prints all the output. */ static int print_all_callback(void *_print_header, int count, char **cols, char **col_names) { int *print_header = _print_header; if (*print_header) { for (int i = 0; i < count; i++) { printf("%s\t", col_names[i]); } printf("\n"); *print_header = 0; } for (int i = 0; i < count; i++) { printf("%s\t", cols[i]); } printf("\n"); return 0; } /* End of recursive calls to ftag_query_ */ static void ftag_query_date(sqlite3 *db, char *sql, int max_len, time_t before_date, time_t after_date) { char buf[128]; int need_and_keyword = 0; strncat(sql, "\t(SELECT id,canonical_name,full_name FROM files", max_len - strlen(sql)); assert(strlen(sql) < max_len); if (before_date != 0) { memset(buf, 0, sizeof(buf)); snprintf(buf, sizeof(buf)-1, " WHERE date < %ld", before_date); strncat(sql, buf, max_len - strlen(sql)); assert(strlen(sql) < max_len); need_and_keyword = 1; } if (after_date != 0) { if (need_and_keyword) strncat(sql, " AND", max_len - strlen(sql)); else strncat(sql, " WHERE", max_len - strlen(sql)); memset(buf, 0, sizeof(buf)); snprintf(buf, sizeof(buf)-1, " date > %ld", after_date); strncat(sql, buf, max_len - strlen(sql)); assert(strlen(sql) < max_len); } strncat(sql, ")\n", max_len - strlen(sql)); } /* Build the "join" part of the SQL query for the "ftag query" command. */ static void ftag_query_join(sqlite3 *db, char *sql, int max_len, char **tags, int tag_count, time_t before_date, time_t after_date) { assert(tag_count >= 0); if (tag_count == 0) { ftag_query_date(db, sql, max_len, before_date, after_date); return; } char *prefix = "\t(SELECT id,canonical_name,full_name FROM file_tags JOIN\n"; strncat(sql, prefix, max_len - strlen(sql)); assert(strlen(sql) < max_len); ftag_query_join(db, sql, max_len, tags, tag_count-1, before_date, after_date); int tag_id = get_id_by_col(db, "tags", "name", tags[tag_count-1]); char suffix[128]; memset(suffix, 0, sizeof(suffix)); snprintf(suffix, sizeof(suffix)-1, "\t\tON id = file\n\t\tWHERE tag = %d)\n", tag_id); strncat(sql, suffix, max_len - strlen(sql)); assert(strlen(sql) < max_len); } static void ftag_query(int argc, char **argv) { if (argc == 0) { ftag_query_usage(); exit(EXIT_FAILURE); } time_t after_date __attribute__((unused)) = 0; time_t before_date __attribute__((unused)) = 0; char **tags = NULL; int tag_count = 0; while (argc > 0) { if (strcmp(argv[0], "-a") == 0) { assert(argc >= 2); after_date = time_from_str(argv[1]); } else if (strcmp(argv[0], "-b") == 0) { assert(argc >= 2); before_date = time_from_str(argv[1]); } else if (strcmp(argv[0], "-t") == 0) { assert(argc >= 2); tag_count++; tags = realloc(tags, tag_count*sizeof(*tags)); tags[tag_count-1] = argv[1]; } else { fprintf(stderr, "ftag query: bad option \"%s\"\n", argv[0]); ftag_query_usage(); exit(EXIT_FAILURE); } argc -= 2; argv += 2; } char sql[4096]; sqlite3 *db; int rc; rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); char sql_join[2048]; memset(sql_join, 0, sizeof(sql_join)); ftag_query_join(db, sql_join, sizeof(sql_join)-1, tags, tag_count, before_date, after_date); memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "SELECT id,canonical_name,full_name FROM (\n%s);\n", sql_join); printf("%s: debug: SQL query is:\n%s", __func__, sql); int print_header = 1; rc = sqlite3_exec(db, sql, print_all_callback, &print_header, NULL); sqlite3_check(rc, db); sqlite3_close(db); free(tags); } /* Check that the tag we are trying to create does not exist yet. If this * callback is ever called, then it already exist, this is a fatal error. */ static int ftag_tag_check(void *, int, char **cols, char **) { assert(cols[0]); assert(cols[1]); fprintf(stderr, "ftag tag add: error: tag \"%s\" already exists, its description is:\n", cols[0]); fprintf(stderr, "%s\n", cols[1]); return 1; } static void ftag_tag_add(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "ftag tag add: error: must supply two arguments, " "the tag name and its description\n"); exit(EXIT_FAILURE); } char *new_tag_name = argv[0]; assert(strlen(new_tag_name) <= 255); sanitize_sql_str(&new_tag_name); char sql[1024]; sqlite3 *db = NULL; int rc; rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "SELECT name, description FROM tags WHERE name = '%s';", new_tag_name); rc = sqlite3_exec(db, sql, ftag_tag_check, NULL, NULL); sqlite3_check(rc, db); int next_id = table_next_id(db, "tags"); char *new_tag_desc = argv[1]; assert(strlen(new_tag_desc) <= 600); sanitize_sql_str(&new_tag_desc); memset(sql, 0, sizeof(sql)); snprintf(sql, sizeof(sql)-1, "INSERT INTO tags VALUES(%d, '%s', '%s');", next_id, new_tag_name, new_tag_desc); rc = sqlite3_exec(db, sql, NULL, NULL, NULL); sqlite3_check(rc, db); sqlite3_close(db); } static void ftag_tag_list(int argc, char **argv) { ftag_list_table("tags", "name"); } static void ftag_tag(int argc, char **argv) { assert_db_exists(); const struct ftag_command tag_commands[] = { {.name = "add", .func = ftag_tag_add}, {.name = "list", .func = ftag_tag_list} /* TODO: add an alias command */ /* TODO: add help command */ }; int tag_command_count = sizeof(tag_commands) / sizeof(struct ftag_command); parse_args(argc, argv, tag_commands, tag_command_count); } static void parse_args(int argc, char **argv, const struct ftag_command *commands, int command_count) { assert(argc > 0); assert(command_count > 0); for (int i = 0; i < command_count; i++) { if (strcmp(argv[0], commands[i].name) == 0) { commands[i].func(argc-1, argv+1); return; } } fprintf(stderr, "ftag: command \"%s\" unknown\n", argv[0]); for (int i = 0; i < command_count; i++) { if (strcmp(commands[i].name, "help") == 0) { commands[i].func(0, NULL); break; } } exit(EXIT_FAILURE); } int main(int argc, char *argv[]) { if (argc == 1) { ftag_help(0, NULL); exit(EXIT_FAILURE); } const struct ftag_command toplevel_commands[] = { {.name = "init", .func = ftag_init}, {.name = "file", .func = ftag_file}, {.name = "-h", .func = ftag_help}, {.name = "--help", .func = ftag_help}, {.name = "help", .func = ftag_help}, {.name = "query", .func = ftag_query}, {.name = "tag", .func = ftag_tag} /* TODO: add an export command For instance, "ftag export ARCHIVE" exports files read on stdin to ARCHIVE.tar.gz or ARCHIVE.zip. Typical usag would be "ftag query -t TAG0 -t TAG1 | ftag export ARCHIVE". Maybe adding a "-e" (for "export") option to ftag query is better.*/ }; const int toplevel_command_count = sizeof(toplevel_commands) / sizeof(struct ftag_command); parse_args(argc-1, argv+1, toplevel_commands, toplevel_command_count); return EXIT_SUCCESS; }