#include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef FTAG_REMOTE_HOST #define FTAG_REMOTE_HOST "localhost" #endif #ifndef FTAG_REMOTE_ROOT /* HOME on remote host */ #define FTAG_REMOTE_ROOT "." #endif #define DATABASE_PATH (FTAG_ROOT "/ftag.sqlite3") /* Used when encrypting or decrpyting a file, see the copy_encrypted_file * function. */ enum encrypt { ENCRYPT, DECRYPT, }; /* TODO: read the configuration from a file This would allow working with different databases with the same ftag binary and moving the database. */ /* 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) { /* Note: "rc" stands for "return code" */ 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__) /* Return whether PATH exists in the file system. Exit if any non-"file not * fount" error occurs. */ static int file_exists(const char *path) { struct stat statbuf __attribute__((unused)); int rc = stat(path, &statbuf); if (rc == 0) return 1; else if ((rc == -1) && (errno == ENOENT)) return 0; fprintf(stderr, "stat: \"%s\": ", path); perror(""); exit(EXIT_FAILURE); } static void assert_db_exists(void) { if (file_exists(DATABASE_PATH)) 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); } /* Copy the file whose path is IN at path OUT. OUT is created or overwritten if * needed. */ static void copy_file(const char *in, const char *out) { int in_fd; int out_fd; int rc; ssize_t written_bytes; in_fd = open(in, O_RDONLY); if (in_fd == -1) { fprintf(stderr, "open: %s:", in); perror(""); exit(EXIT_FAILURE); } out_fd = open(out, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (out_fd == -1) { fprintf(stderr, "open: %s:", out); perror(""); exit(EXIT_FAILURE); } while ((written_bytes = sendfile(out_fd, in_fd, NULL, 4096)) == 4096) ; if (written_bytes == -1) { perror("sendfile"); exit(EXIT_FAILURE); } rc = fchmod(out_fd, 0400); if (rc == -1) { fprintf(stderr, "chmod: %s: ", out); perror(""); exit(EXIT_FAILURE); } close(in_fd); close(out_fd); } /* Like copy_file, but OUT is an encrypted version of IN. Encryption is done * using GPG. */ static void copy_file_with_encryption(const char *in, const char *out, enum encrypt encrypt) { int rc = fork(); char *crypt_param; assert(encrypt == ENCRYPT || encrypt == DECRYPT); if (encrypt == ENCRYPT) crypt_param = "--encrypt"; else crypt_param = "--decrypt"; if (rc == 0) { execlp("gpg", "gpg", "--output", out, "--yes", /* do not ask for overwriting files, maybe * dangerous if GPG asks security questions */ crypt_param, in, NULL); fprintf(stderr, "exec: gpg:"); perror(""); exit(EXIT_FAILURE); } else if (rc > 0) { int status; wait(&status); if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) { fprintf(stderr, "ftag file add: " "child process exited abnormally\n"); exit(EXIT_FAILURE); } } else { perror("fork"); exit(EXIT_FAILURE); } } /* Prompt the user for yes or no (default is yes). Before calling, a prompt * should be printed to stdout, eventually not with an ending newline. */ static int prompt_yes_no(void) { puts(" [Y/n]"); fflush(stdout); size_t line_size = 3; char *line = malloc(line_size); int attempts = 0; do { getline(&line, &line_size, stdin); char input = line[0]; if (input == '\n'/*no input*/ || input == 'y' || input == 'Y') return 1; else if (input == 'n' || input == 'N') return 0; attempts++; } while(attempts < 3); return 0; } static void remove_ending_newline(char *str) { int idx = strlen(str) - 1; assert(str[idx] == '\n'); str[idx] = '\0'; } /* Convert heap-allocated *STR from 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; } /* Safely create a formatted string and write it to BUF. BUF shall be a buffer * of size at least SIZE. BUF can be stack-allocated. If the formatting cannot * be performed, exit(3) is called. This function is not meant to be called * directly: the macro strbuild should be used. The code is adapted from the * make_message function of the vsnprintf(3) manual page, section "examples". */ static void __strbuild(char *buf, int size, const char *file, int line, const char *fmt, ...) { va_list ap; int len; va_start(ap, fmt); len = vsnprintf(NULL, 0, fmt, ap); va_end(ap); if (len < 0) { fprintf(stderr, "%s:%d: ", file, line); perror(""); exit(EXIT_FAILURE); } if (len >= size) { fprintf(stderr, "%s:%d: error: not enough space, " "have %d, need at least %d\n", file, line, size, len + 1); exit(EXIT_FAILURE); } va_start(ap, fmt); len = vsnprintf(buf, size, fmt, ap); va_end(ap); assert(len >= 0); } /* Convenience wrappers for __strbuild. */ #define strbuild(buf, fmt, ...) \ __strbuild(buf, sizeof(buf), __FILE__, __LINE__, fmt, __VA_ARGS__) #define strbuild_with_size(buf, size, fmt, ...) \ __strbuild(buf, size, __FILE__, __LINE__, fmt, __VA_ARGS__) static int str_has_suffix(const char *str, const char *suffix) { int str_len = strlen(str); int suffix_len = strlen(suffix); return ((str_len > suffix_len) && (strcmp(str + str_len - suffix_len, suffix) == 0)); } /* Compute something that identifies the content of FILE. Algorithm by Dan * Bernstein from http://www.cse.yorku.ca/~oz/hash.html via * https://stackoverflow.com/a/7666577/20138083. */ static uint32_t sum(const char *file) { FILE *stream = fopen(file, "r"); uint8_t buf[4096]; size_t buf_len; uint32_t sum = 5381; if (!file) { fprintf(stderr, "fopen: \"%s\": ", file); perror(""); exit(EXIT_FAILURE); } 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)) { fprintf(stderr, "fread: \"%s\": ", file); perror(""); exit(EXIT_FAILURE); } fclose(stream); return sum; } /* Return an integer timestamp from a date string of the format YYYY-MM-DD. */ 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("sscanf"); exit(EXIT_FAILURE); } 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]; strbuild(sql, "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; } static void ftag_export_usage(void) { puts("Usage: ftag export [OPTION]... ARCHIVE"); } static void ftag_export_help(void) { ftag_export_usage(); puts("Export files from the database to an archive. Files are read from"); puts("standard input, one per line. Files have to be canonical names that"); puts("exist in the database."); /* TODO: support files outside of the database A file name starting with a slash can be considered as a file to take from the filesystem, not from the database. */ puts("Available options:"); puts(" -f TODO: use files' full names"); puts(" -h print this help"); puts(" -n TODO: add user name as a suffix"); } /* Structure used to pass data from ftag_file_get_extension to its callback * ftag_file_get_extension_callback. */ struct ftag_file_get_extension_s { char *buf; int size; }; static int ftag_file_get_extension_callback(void *_ext, int, char **cols, char **) { struct ftag_file_get_extension_s *ext = _ext; assert(cols); assert(cols[0]); assert(strlen(cols[0]) < ext->size); strcpy(ext->buf, cols[0]); return 0; } /* Get extension for file whose canonical name is FILE and write it to OUT which * shall be of size at least SIZE. */ static void ftag_file_get_extension(char *out, int size, const char *file) { sqlite3 *db; int rc; char sql[256]; struct ftag_file_get_extension_s ext = { .buf = out, .size = size }; strbuild(sql, "SELECT extension FROM files WHERE canonical_name = '%s';", file); rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); rc = sqlite3_exec(db, sql, ftag_file_get_extension_callback, &ext, NULL); sqlite3_check(rc, db); sqlite3_close(db); } /* Get the path to FILE in the ftag database. */ static void ftag_file_get_path(char *out, int size, const char *file) { /* look for the encrypted version first */ strbuild_with_size(out, size, "%s/files/%s.gpg", FTAG_ROOT, file); if (file_exists(out)) return; /* look for the clear version after */ out[strlen(out) - 4] = '\0'; /* remove ".gpg" extension */ if (file_exists(out)) return; fprintf(stderr, "ftag export: cannot find \"%s\" in the database\n", file); exit(EXIT_FAILURE); } static void ftag_export(int argc, char **argv) { while ((argc > 0) && (argv[0][0] == '-')) { switch (argv[0][1]) { case 'h': ftag_export_help(); exit(EXIT_SUCCESS); default: ftag_export_usage(); exit(EXIT_FAILURE); } argv++; argc--; } if (argc != 1) { ftag_export_help(); exit(EXIT_FAILURE); } char *archive_dir = argv[0]; char archive_file[64]; strbuild(archive_file, "%s%s", archive_dir, ".tar.gz"); /* step 1: create temporary directory having the name of the archive */ char tmp_dir[64]; strbuild(tmp_dir, "/tmp/%s", archive_dir); int rc = mkdir(tmp_dir, 0755); if (rc == -1) { fprintf(stderr, "mkdir: %s:", tmp_dir); perror(""); exit(EXIT_FAILURE); } /* step 2: copy archive files to the temporary directory */ size_t line_size = 256; ssize_t line_len; char *line = malloc(line_size); char in[512]; char out[128]; char extension[16]; while ((line_len = getline(&line, &line_size, stdin)) != -1) { remove_ending_newline(line); ftag_file_get_extension(extension, sizeof(extension), line); ftag_file_get_path(in, sizeof(in), line); if (strlen(extension) > 0) strbuild(out, "%s/%s.%s", tmp_dir, line, extension); else strbuild(out, "%s/%s", tmp_dir, line); if (str_has_suffix(in, ".gpg")) copy_file_with_encryption(in, out, DECRYPT); else copy_file(in, out); } free(line); /* step 3: invoke tar to build the archive */ int error = 0; rc = fork(); if (rc == 0) { execlp("tar", "tar", "--directory", "/tmp", "--create", "--gzip", "--file", archive_file, archive_dir, NULL); fprintf(stderr, "exec: tar:"); perror(""); exit(EXIT_FAILURE); } else if (rc > 0) { int status; wait(&status); if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) { fprintf(stderr, "ftag export: child process exited abnormally\n"); error = 1; } } else { perror("fork"); error = 1; } /* step 4: clean /tmp */ execlp("rm", "rm", "--recursive", "--force", tmp_dir, NULL); if (error) exit(EXIT_FAILURE); } /* Create ftag's database and directories. */ static void ftag_init(int, char **) { int rc = mkdir(FTAG_ROOT "/files", 0755); if (rc == -1) { fprintf(stderr, "mkdir: %s/files: ", FTAG_ROOT); perror(""); exit(EXIT_FAILURE); } char cmd[1024]; strbuild(cmd, "sqlite3 %s < %s", DATABASE_PATH, FTAG_ROOT "/sql/init.sql"); execl("/usr/bin/sh", "/usr/bin/sh", "-c", cmd, NULL); fprintf(stderr, "exec: /usr/bin/sh -c \"%s\": ", cmd); perror(""); exit(EXIT_FAILURE); } /* Sqlite callback that prints the first column without header. Used for example * by ftag_list_table. */ static int ftag_print(void *, int, char **cols, char **) { assert(cols[0]); printf("%s\n", cols[0]); return 0; } /* For every row of TABLE, print the value for column COL. Typically used by * ftag_file_list for listing file names. */ 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); strbuild(sql, "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, uint32_t file_sum, int encrypt) { 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; /* TODO: possibly be non-interactive Maybe take canonical_name, full_name, and description as parameters. */ 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); remove_ending_newline(full_name); line_len = strlen(full_name) + 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 remove_ending_newline(canonical_name); 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 { remove_ending_newline(description); } 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); } char extension[16]; /* Find the position of the last dot character in file name. If found, * everything that follows is considered as the extension. */ int i = strlen(file) - 1; while ((i >= 1) && (file[i] != '.')) i--; if ((i >= 1) && (i < strlen(file))) { assert(strlen(file + i + 1) < 16); strcpy(extension, file + i + 1); } else { extension[0] = '\0'; } strbuild(sql, "INSERT INTO files VALUES(%d, '%s', '%s', '%s', " "%ld, %u, '%s')", *next_id, canonical_name, full_name, description, date, file_sum, extension); rc = sqlite3_exec(db, sql, NULL, NULL, NULL); sqlite3_check(rc, db); (*next_id)++; char new_path[512]; if (encrypt) { strbuild(new_path, "%s/files/%s.gpg", FTAG_ROOT, canonical_name); copy_file_with_encryption(file, new_path, ENCRYPT); } else { strbuild(new_path, "%s/files/%s", FTAG_ROOT, canonical_name); copy_file(file, new_path); } free(date_str); free(full_name); free(canonical_name); free(description); } static void ftag_file_add_usage(void) { puts("Usage: ftag file add [OPTION]... FILE..."); } static void ftag_file_add_help(void) { ftag_file_add_usage(); puts("Add files to the database. Non-files arguments are ignored."); puts("Available options (all options must precede arguments):"); puts(" -c clear, do not encrypt file"); puts(" -f force, do not search for duplicates"); puts(" -h print help message"); puts(" -i interactive, for each file, ask before adding it to the database"); puts("Tips:"); puts(" For adding all files in a directory DIR, use"); puts(" $ ftag file add -i DIR/*"); puts(" For doing it recursively, use"); puts(" $ find DIR -type f -fprint0 /tmp/files"); puts(" $ xargs -0 -a /tmp/files ftag file add -i"); } /* Structure only used by ftad_file_add and get_sums_callback, for building a * list of file sums. */ struct known_sums_s { uint32_t *array; int len; int capacity; }; static int get_sums_callback(void *_known_sums, int, char **cols, char **) { struct known_sums_s *known_sums = _known_sums; assert(cols[0]); known_sums->array[known_sums->len] = (uint32_t)atol(cols[0]); known_sums->len++; assert(known_sums->len <= known_sums->capacity); return 0; } /* Add new files to the database. Non-file arguments are ignored. */ static void ftag_file_add(int argc, char **argv) { /* step 0: parse options */ int interactive = 0; int eliminate_duplicates = 1; int encrypt = 1; while ((argc > 0) && (argv[0][0] == '-')) { switch (argv[0][1]) { case 'c': encrypt = 0; break; case 'f': eliminate_duplicates = 0; break; case 'h': ftag_file_add_help(); exit(EXIT_SUCCESS); case 'i': interactive = 1; break; default: ftag_file_add_usage(); exit(EXIT_FAILURE); } argv++; argc--; } if (argc == 0) { ftag_file_add_usage(); exit(EXIT_FAILURE); } /* step 1: compute file sums */ uint32_t *sums = malloc(argc*sizeof(*sums)); struct stat st; int rc; for (int i = 0; i < argc; i++) { char *file = argv[i]; rc = stat(file, &st); if (rc == -1) { fprintf(stderr, "stat: \"%s\": ", file); perror(""); exit(EXIT_FAILURE); } if (st.st_mode & S_IFREG) { sums[i] = sum(file); } else { /* skip non-file arguments */ argv[i] = NULL; } } /* step 2: retrieve sums of files already in the database */ sqlite3 *db; rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); int next_id = table_next_id(db, "files"); int file_count = next_id; /* number of files already in the database */ uint32_t known_sums_array[file_count]; struct known_sums_s known_sums = { .array = known_sums_array, .len = 0, .capacity = file_count }; if (!eliminate_duplicates) goto step4; rc = sqlite3_exec(db, "SELECT sum FROM files;", get_sums_callback, &known_sums, NULL); sqlite3_check(rc, db); /* step 3: eliminate duplicates with respect to file sum */ for (int i = 0; i < argc; i++) { const uint32_t original = sums[i]; /* step 3.1: eliminate duplicates found in database */ for (int j = 0; j < file_count; j++) { const uint32_t duplicate = known_sums_array[j]; if (duplicate == original) { printf("%s debug: file \"%s\" found in database\n", __func__, argv[i]); argv[i] = NULL; break; } } /* step 3.2: eliminate duplicates within given files */ if (argv[i] == NULL) continue; for (int j = i+1; j < argc; j++) { const uint32_t duplicate = sums[j]; if (duplicate == original) { printf("%s debug: file \"%s\" found in given files\n", __func__, argv[j]); argv[j] = NULL; } } } /* step 4: perform addition to database */ step4: for (int i = 0; i < argc; i++) { char *file = argv[i]; if (file == NULL) continue; if (interactive) { printf("Add \"%s\" to database ?", file); if (!prompt_yes_no()) continue; } ftag_add_one_file(db, &next_id, file, sums[i], encrypt); next_id++; } free(sums); sqlite3_close(db); } static void ftag_file_help(int, char **) { puts("Usage: ftag file COMMAND [ARG]..."); puts("Available values for COMMAND:"); puts(" add add files to the database"); puts(" help print this help"); puts(" list list files in the database"); puts(" tag add tags to files"); } 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; } } /* Return 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]; strbuild(sql, "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, "Usage: ftag file tag FILE TAG...\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); strbuild(sql, "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 = "help", .func = ftag_file_help}, {.name = "list", .func = ftag_file_list}, {.name = "tag", .func = ftag_file_tag} }; 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 **) { puts("Usage: ftag COMMAND [ARG]..."); puts("Available values for COMMAND:"); puts(" init initialize the database"); puts(" file manage files"); puts(" help print this message"); puts(" query query the database"); puts(" tag manage tags"); puts("Some commands also have their own help, try \"ftag COMMAND help\""); puts("Configuration:"); 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) { puts("usage: ftag query [-a DATE] [-b DATE] [-t TAG]..."); } /* 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_join. */ 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) { strbuild(buf, " 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)); strbuild(buf, " 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]; strbuild(suffix, "\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); strbuild(sql, "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); } /* Return: * - positive value if local ftag database is newer than the remote one; * - zero if both ftag databases have the same last modification time; * - negative value if remote ftag database is newer than the local one. */ static int ftag_sync_compare_mtimes(void) { char *remote_path = FTAG_REMOTE_ROOT "/ftag.sqlite3"; char cmd[1024]; strbuild(cmd, "ssh %s 'test -f %s && stat --format=%%Y %s || echo 0'", FTAG_REMOTE_HOST, remote_path, remote_path); FILE *pipe = popen(cmd, "r"); if (!pipe) { fprintf(stderr, "popen: ssh %s: ", FTAG_REMOTE_HOST); perror(""); exit(EXIT_FAILURE); } size_t line_size = 32; char *line = malloc(line_size); ssize_t line_len; if (ferror(pipe)) { perror("getline"); exit(EXIT_FAILURE); } int rc = pclose(pipe); if (rc == 1) { fprintf(stderr, "pclose: ssh %s: ", FTAG_REMOTE_HOST); perror(""); exit(EXIT_FAILURE); } time_t remote_mtime = atol(line); free(line); struct stat st; rc = stat(DATABASE_PATH, &st); time_t local_mtime = st.st_mtim.tv_sec; return local_time - remote_time; } static int ftag_sync_is_local_newer(void) { return ftag_sync_compare_mtimes() > 0; } static int ftag_sync_is_remote_newer(void) { return ftag_sync_compare_mtimes() < 0; } static void ftag_sync_pull(int argc, char **argv) { if (!ftag_sync_is_remote_newer()) return; /* TODO: resume here */ } /* 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%s\n", cols[0], cols[1]); return 1; } static void ftag_tag_add(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "Usage: ftag tag add NAME 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; int rc; rc = sqlite3_open(DATABASE_PATH, &db); sqlite3_check(rc, db); strbuild(sql, "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); strbuild(sql, "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_help(int, char **) { puts("Usage: ftag tag COMMAND [ARG]..."); puts("Available values for COMMAND:"); puts(" add create new tags"); puts(" help print this help"); puts(" list list available tags"); } 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 = "help", .func = ftag_tag_help}, {.name = "list", .func = ftag_tag_list} /* TODO: add an alias 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 = "export", .func = ftag_export}, {.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 = "sync", .func = ftag_sync}, {.name = "tag", .func = ftag_tag} /* TODO: add a "sync" command The aim is to be able to synchronize ftag data (database and files) across multiple machines. With the current approach, I think we cannot avoid transferring the whole database at every synchonization. For the files however, we should be able to only transfer the new files, maybe using rsync. */ }; 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; }