1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
|
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#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);
}
/* Execute command in a child process, then wait for the child to exit before
* returning. If any error occurs, including in the child process, then if EXIT
* is non-zero, exit, else return -1. Return 0 on success. */
static int ftag_execvp(char *const *cmd, int can_exit)
{
int status = 0;
int rc = fork();
if (rc == 0) {
execvp(cmd[0], cmd);
fprintf(stderr, "exec: %s: ", cmd[0]);
perror("");
}
else if (rc > 0) {
int status;
wait(&status);
if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) {
fprintf(stderr,
"ftag: child process exited abnormally\n");
if (can_exit)
exit(EXIT_FAILURE);
else
status = -1;
}
}
else {
perror("fork");
if (can_exit)
exit(EXIT_FAILURE);
else
status = -1;
}
return status;
}
/* 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)
{
char *crypt_param;
assert(encrypt == ENCRYPT || encrypt == DECRYPT);
if (encrypt == ENCRYPT)
crypt_param = "--encrypt";
else
crypt_param = "--decrypt";
/* use dups to suppress "discards const qualifier" warnings */
char *in_dup = strdup(in);
char *out_dup = strdup(out);
char *cmd[] = {
"gpg",
"--output", out_dup,
"--yes",
crypt_param, in_dup,
NULL
};
ftag_execvp(cmd, 1);
free(in_dup);
free(out_dup);
}
/* 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 */
char *cmd_tar[] = {
"tar",
"--directory", "/tmp",
"--create",
"--gzip",
"--file", archive_file,
archive_dir,
NULL
};
rc = ftag_execvp(cmd_tar, 0);
char *cmd_rm[] = {
"rm",
"--recursive",
"--force",
tmp_dir,
NULL
};
ftag_execvp(cmd_rm, 1);
if (rc != 0)
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);
getline(&line, &line_size, pipe);
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_mtime - remote_mtime;
}
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(void)
{
if (!ftag_sync_is_remote_newer())
return;
/* TODO: resume here */
}
static void ftag_sync(int argc, char **argv)
{
(void) argc;
(void) argv;
}
/* 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;
}
|