* Initial commit.
authorUrban Wallasch <urban.wallasch@freenet.de>
Sat, 5 Jun 2021 15:46:15 +0000 (17:46 +0200)
committerUrban Wallasch <urban.wallasch@freenet.de>
Sat, 5 Jun 2021 15:46:15 +0000 (17:46 +0200)
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
db.c [new file with mode: 0644]
db.h [new file with mode: 0644]
main.c [new file with mode: 0644]
queue.c [new file with mode: 0644]
queue.h [new file with mode: 0644]
util.c [new file with mode: 0644]
util.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..58be549
--- /dev/null
@@ -0,0 +1,4 @@
+*.d
+*.o
+*.db
+imgdupe
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..df44e12
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2021, Urban Wallasch
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..dd519c9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+
+PROJECT := imgdupe
+
+BIN     := $(PROJECT)
+SRC     := $(wildcard *.c)
+OBJ     := $(SRC:%.c=%.o)
+DEP     := $(OBJ:%.o=%.d)
+SELF    := $(lastword $(MAKEFILE_LIST))
+
+CC      ?= gcc
+CFLAGS  := -W -Wall -Wextra -O2 -std=gnu99 -pthread -MMD -MP
+CFLAGS  += $(shell pkg-config --cflags GraphicsMagickWand)
+CFLAGS  += -DDEBUG
+
+LD      := $(CC)
+LDFLAGS :=
+LIBS    := -lpthread $(shell pkg-config --libs GraphicsMagickWand)
+
+STRIP   := strip
+RM      := rm -f
+
+.PHONY: all clean distclean
+
+all: $(BIN)
+
+$(BIN): $(OBJ) $(SELF)
+       $(LD) $(LDFLAGS) $(OBJ) $(LIBS) -o $(BIN)
+       $(STRIP) $(BIN)
+
+%.o: %.c $(SELF)
+       $(CC) -c $(CFLAGS) -o $*.o $*.c
+
+clean:
+       $(RM) $(BIN) $(OBJ) $(DEP)
+
+distclean: clean
+       $(RM) config.h
+
+-include $(DEP)
+
+# EOF
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..f4cf95e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Imdupe
+
+Imdupe is a tool to find potentially duplicate images in a directory tree.
+
+
+## License
+
+FFpreview is distributed under the Modified ("3-clause") BSD License.
+See `LICENSE` file for more information.
+
+----------------------------------------------------------------------
diff --git a/db.c b/db.c
new file mode 100644 (file)
index 0000000..e2cbf97
--- /dev/null
+++ b/db.c
@@ -0,0 +1,282 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <unistd.h>
+
+#include <wand/magick_wand.h>
+
+#include "util.h"
+#include "db.h"
+
+
+/* primitive yet fast string hashing */
+static inline int_fast16_t nhash(const char *s) {
+    int_fast16_t h = 0;
+    while ( *s ) {
+        h <<= 1;
+        h |= (h >> NHASH_BITS) & 1;
+        h ^= *s++;
+    }
+    return h & NHASH_MASK;
+}
+
+/* hamming weight of a 64 bit entity */
+static inline int popcnt64(uint64_t x) {
+    x -= (x >> 1) & 0x5555555555555555ULL;
+    x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL);
+    x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0fULL;
+    return (x * 0x0101010101010101ULL) >> 56;
+}
+
+/* hamming distance of two perceptual hashes */
+static inline int hdist(uint64_t *h1, uint64_t*h2) {
+    return popcnt64(h1[0]^h2[0]) + popcnt64(h1[1]^h2[1])
+         + popcnt64(h1[2]^h2[2]) + popcnt64(h1[3]^h2[3]);
+}
+
+/* perceptual hash function */
+static int phash(db_entry_t *p, double blur) {
+    int rc = -1;
+    unsigned char pix[PHASH_BITS];
+    MagickWand *wand, *wand2;
+
+    if ( NULL == (wand = NewMagickWand()) )
+        goto ERR1;
+    if ( !MagickReadImage(wand, p->fname) )
+        goto ERR2;
+    wand2 = MagickFlattenImages(wand);
+    MagickSetImageColorspace(wand2, GRAYColorspace);
+    MagickResizeImage(wand2, 16, 16, GaussianFilter, blur);
+    MagickNormalizeImage(wand2);
+    MagickGetImagePixels(wand2, 0, 0, 16, 16, "I", CharPixel, pix);
+    uint64_t h = 0;
+    int hidx = 0;
+    p->hamw = 0;
+    for ( size_t i = 0; i < sizeof pix; ) {
+        h = (h << 1) | (pix[i] > 0x7f);
+        ++i;
+        if ( 0 == i % 64 ) {
+            p->phash[hidx++] = h;
+            p->hamw += popcnt64(h); /* hamming weight of hash */
+        }
+    }
+    rc = 0;
+    DestroyMagickWand(wand2);
+  ERR2:
+    DestroyMagickWand(wand);
+  ERR1:
+    return rc;
+}
+
+
+/* database handling functions */
+
+static inline int db_lock(db_t *db) {
+    return pthread_mutex_lock(&db->mtx);
+}
+
+static inline int db_unlock(db_t *db) {
+    return pthread_mutex_unlock(&db->mtx);
+}
+
+db_entry_t *db_entry_new(const char *fname) {
+    db_entry_t *entry = s_malloc(sizeof *entry);
+    memset(entry, 0, sizeof *entry);
+    entry->fname = NULL != fname ? s_strdup(fname) : "?";
+    return entry;
+}
+
+db_t *db_init(void) {
+    db_t *db = s_malloc(sizeof *db);
+    memset(db->p_ent, 0, sizeof db->p_ent);
+    memset(db->n_ent, 0, sizeof db->n_ent);
+    pthread_mutex_init(&db->mtx, NULL);
+    InitializeMagick(NULL);
+    return db;
+}
+
+void db_destroy(db_t **pdb) {
+    DestroyMagick();
+    db_clear(*pdb);
+    pthread_mutex_destroy(&(*pdb)->mtx);
+    s_free(*pdb);
+    *pdb = NULL;
+}
+
+int db_clear(db_t *db) {
+    int cnt = 0;
+    db_lock(db);
+    for ( size_t i = 0; i < ARR_SIZE(db->n_ent); ++i )  {
+        db_entry_t *p, *next;
+        p = db->n_ent[i];
+        while ( NULL != p ) {
+            ++cnt;
+            next = p->inext;
+            free(p->fname);
+            free(p);
+            p = next;
+        }
+    }
+    memset(db->p_ent, 0, sizeof db->p_ent);
+    memset(db->n_ent, 0, sizeof db->n_ent);
+    db_unlock(db);
+    return cnt;
+}
+
+int db_prune(db_t *db) {
+    int cnt = 0;
+    db_lock(db);
+    for ( size_t i = 0; i < ARR_SIZE(db->n_ent); ++i )  {
+        db_entry_t *p;
+        p = db->n_ent[i];
+        while ( NULL != p ) {
+            if ( !DB_ENTRY_ISDELETED(p) && 0 != access(p->fname, R_OK) ) {
+                dprintf("prune: '%s'\n", p->fname);
+                DB_ENTRY_DELETE(p);
+            }
+            p = p->inext;
+        }
+    }
+    db_unlock(db);
+    return cnt;
+}
+
+db_entry_t *db_find(db_t *db, const char *fname) {
+    db_entry_t *p;
+    int h = nhash(fname);
+    db_lock(db);
+    p = db->n_ent[h];
+    while ( NULL != p ) {
+        if ( !DB_ENTRY_ISDELETED(p) && 0 == strcmp(fname, p->fname) )
+            break;
+        p = p->inext;
+    }
+    db_unlock(db);
+    return p;
+}
+
+int db_insert(db_t *db, db_entry_t *entry, int replace, double blur) {
+    /* entry already in db? */
+    db_entry_t *en = db_find(db, entry->fname);
+    if ( NULL != en ) {
+        if ( !replace )
+            return 1;
+        DB_ENTRY_DELETE(en);
+    }
+    /* phash the image; nhash the file name */
+    if ( 0 != phash(entry, blur) )
+        return -1;
+    entry->nhash = nhash(entry->fname);
+    /* drop entry in the respective hash table buckets */
+    db_lock(db);
+    entry->inext = db->n_ent[entry->nhash];
+    db->n_ent[entry->nhash] = entry;
+    entry->pnext = db->p_ent[entry->hamw];
+    db->p_ent[entry->hamw] = entry;
+    db_unlock(db);
+    return 0;
+}
+
+int db_write(db_t *db, const char *dbf) {
+    FILE *fp;
+    int cnt = 0;
+
+    if ( NULL == (fp = fopen(dbf, "wb")) )
+        return -1;
+    db_lock(db);
+    for ( size_t i = 0; i < ARR_SIZE(db->n_ent); ++i )  {
+        db_entry_t *p;
+        for ( p = db->n_ent[i]; NULL != p; p = p->inext ) {
+            if ( DB_ENTRY_ISDELETED(p) )
+                continue;
+            ++cnt;
+            fwrite((void *)&p->phash, sizeof p->phash, 1, fp);
+            fwrite((void *)&p->nhash, sizeof p->nhash, 1, fp);
+            fwrite((void *)&p->flags, sizeof p->flags, 1, fp);
+            fwrite((void *)&p->hamw, sizeof p->hamw, 1, fp);
+            fwrite((void *)p->fname, strlen(p->fname) + 1, 1, fp);
+        }
+    }
+    db_unlock(db);
+    fclose(fp);
+    return cnt;
+}
+
+int db_read(db_t *db, const char *dbf) {
+    int cnt = 0, c;
+    size_t i;
+    char buf[4000];
+    FILE *fp;
+    db_entry_t *p;
+
+    if ( NULL == (fp = fopen(dbf, "rb")) )
+        return -1;
+    db_lock(db);
+    while ( !feof(fp) ) {
+        p = db_entry_new(NULL);
+        fread((void *)&p->phash, sizeof p->phash, 1, fp);
+        fread((void *)&p->nhash, sizeof p->nhash, 1, fp);
+        fread((void *)&p->flags, sizeof p->flags, 1, fp);
+        fread((void *)&p->hamw, sizeof p->hamw, 1, fp);
+        i = 0;
+        do {
+            c = fgetc(fp);
+            buf[i++] = c;
+        } while ( '\0' != c && EOF != c && i < sizeof buf );
+        if ( '\0' != c ) {
+            s_free(p);
+            break;
+        }
+        p->fname = s_strdup(buf);
+        p->inext = db->n_ent[p->nhash];
+        db->n_ent[p->nhash] = p;
+        p->pnext = db->p_ent[p->hamw];
+        db->p_ent[p->hamw] = p;
+        ++cnt;
+    }
+    db_unlock(db);
+    return cnt;
+}
+
+/* find duplicate or similar images */
+int db_find_dupes(db_t *db, int thresh, int(*cb)(db_entry_t *dupes) ) {
+    db_lock(db);
+    for ( size_t i = 0; i < ARR_SIZE(db->p_ent); ++i )  {
+        db_entry_t *p, *cmp, *dupes;
+        for ( p = db->p_ent[i]; NULL != p; p = p->pnext ) {
+            if ( DB_ENTRY_ISDELETED(p) )
+                continue;
+            dupes = NULL;
+            for ( cmp = p->pnext; NULL != cmp; cmp = cmp->pnext ) {
+                if ( !DB_ENTRY_ISDELETED(cmp)
+                  && thresh <= hdist(cmp->phash, p->phash) ) {
+                    cmp->aux = dupes;
+                    dupes = cmp;
+                }
+            }
+            if ( thresh < 256  && i < PHASH_BITS ) {
+                /* for lower thresh also look in the next bucket over */
+                for ( cmp = db->p_ent[i+1]; NULL != cmp; cmp = cmp->pnext ) {
+                    if ( !DB_ENTRY_ISDELETED(cmp)
+                      && thresh <= 256 - hdist(cmp->phash, p->phash) ) {
+                        cmp->aux = dupes;
+                        dupes = cmp;
+                    }
+                }
+            }
+            if ( NULL != dupes ) {
+                p->aux = dupes;
+                dupes = p;
+                if ( 0 != cb(dupes) )
+                    goto BREAK;
+            }
+        }
+    }
+  BREAK:
+    db_unlock(db);
+    return 0;
+}
+
+/* EOF */
+
diff --git a/db.h b/db.h
new file mode 100644 (file)
index 0000000..6a6b3b1
--- /dev/null
+++ b/db.h
@@ -0,0 +1,68 @@
+#ifndef DB_H_INCLUDED
+#define DB_H_INCLUDED
+
+#include <stdint.h>
+#include <pthread.h>
+
+#define NHASH_BITS 10
+#define NHASH_SIZE (1 << NHASH_BITS)
+#define NHASH_MASK (NHASH_SIZE-1)
+
+#define PHASH_BITS 256
+
+
+enum db_entry_flags {
+    DB_ENTRY_FLAG_DEL = 1,
+    DB_ENTRY_FLAG_MARK = 2,
+};
+
+#define DB_ENTRY_DELETE(p) ((p)->flags |= DB_ENTRY_FLAG_DEL)
+#define DB_ENTRY_ISDELETED(p) ((p)->flags & DB_ENTRY_FLAG_DEL)
+
+#define DB_ENTRY_MARK(p) ((p)->flags |= DB_ENTRY_FLAG_MARK)
+#define DB_ENTRY_UNMARK(p) ((p)->flags &= ~DB_ENTRY_FLAG_MARK)
+#define DB_ENTRY_ISMARKED(p) ((p)->flags & DB_ENTRY_FLAG_MARK)
+
+
+typedef
+    struct db_entry_struct
+    db_entry_t;
+
+struct db_entry_struct {
+    uint64_t phash[4];
+    uint16_t nhash;
+    uint16_t flags;
+    uint16_t hamw;
+    char *fname;
+    db_entry_t *pnext;
+    db_entry_t *inext;
+    db_entry_t *aux;
+};
+
+
+typedef
+    struct db_struct
+    db_t;
+
+struct db_struct {
+    db_entry_t *p_ent[PHASH_BITS + 1];
+    db_entry_t *n_ent[NHASH_SIZE];
+    pthread_mutex_t mtx;
+};
+
+
+extern db_entry_t *db_entry_new(const char *fname);
+extern db_t *db_init(void);
+extern void db_destroy(db_t **pdb);
+extern int db_clear(db_t *db);
+extern int db_prune(db_t *db);
+extern db_entry_t *db_find(db_t *db, const char *fname);
+extern int db_insert(db_t *db, db_entry_t *entry, int replace, double blur);
+extern int db_update(db_t *db, db_entry_t *entry);
+extern int db_write(db_t *db, const char *dbf);
+extern int db_read(db_t *db, const char *dbf);
+extern int db_find_dupes(db_t *db, int thresh, int(*cb)(db_entry_t *dupes) );
+
+#endif //ndef DB_H_INCLUDED
+
+/* EOF */
diff --git a/main.c b/main.c
new file mode 100644 (file)
index 0000000..2a82483
--- /dev/null
+++ b/main.c
@@ -0,0 +1,244 @@
+#define _XOPEN_SOURCE 500   /* for nftw() */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <limits.h>
+#include <inttypes.h>
+
+#include <libgen.h>
+#include <ftw.h>
+#include <sched.h>
+
+#include "util.h"
+#include "db.h"
+#include "queue.h"
+
+
+/* configuration */
+static struct {
+    char *db_outfile;
+    int db_prune;
+    int rescan;
+    double blur;
+    int thresh;
+    int max_fd;
+    int nthreads;
+} cfg = {
+    .db_outfile = NULL,
+    .db_prune = 0,
+    .rescan = 0,
+    .blur = 1.5,
+    .thresh = 256,
+    .max_fd = 1000,
+    .nthreads = 4,
+};
+
+/* queue used during directory scan; static for scan_dir_cb() */
+static queue_t *q;
+
+
+/* worker thread info */
+typedef struct {
+    pthread_t tid;
+    int i;
+    queue_t *q;
+    db_t *db;
+} thread_info_t;
+
+/* worker thread function for building db */
+void *worker(void *arg) {
+    int rc;
+    thread_info_t *thrinf = arg;
+    db_entry_t *entry = NULL;
+
+    while ( NULL != entry || !q_complete(thrinf->q) ) {
+        entry = q_deq(thrinf->q);
+        if ( NULL == entry ) {
+            sched_yield();
+            continue;
+        }
+        /* insert entry in db hash tables */
+        rc = db_insert(thrinf->db, entry, cfg.rescan, cfg.blur);
+        if ( 0 != rc ) {
+            if ( 0 > rc )
+                eprintf("not fingerprinting '%s'\n", entry->fname);
+            s_free(entry->fname);
+            s_free(entry);
+        }
+    }
+    return thrinf;
+}
+
+/* callback for nftw directory tree walker */
+int scan_dir_cb(const char *fpath, const struct stat *sb,
+                int typeflag, struct FTW *ftwbuf) {
+    (void)sb;
+    (void)ftwbuf;
+    if (FTW_F == typeflag) {
+        db_entry_t *entry;
+        entry = db_entry_new(fpath);
+        q_enq(q, entry);
+    }
+    return 0;
+}
+
+/* scan directory and build db */
+int scan_dir(const char *dir, queue_t *q, db_t *db) {
+    int rc = -1;
+    char *dirpath;
+
+    /* start worker threads */
+    thread_info_t thrinf[cfg.nthreads];
+    pthread_attr_t attr;
+    pthread_attr_init(&attr);
+    for ( int i = 0; i < cfg.nthreads; ++i ) {
+        thrinf[i].i = i;
+        thrinf[i].q = q;
+        thrinf[i].db = db;
+        pthread_create(&thrinf[i].tid, &attr, worker, &thrinf[i]);
+    }
+    pthread_attr_destroy(&attr);
+    /* main thread: walk directory tree */
+    errno = 0;
+    if ( NULL != (dirpath = realpath(dir, NULL)) ) {
+        rc = nftw(dirpath, scan_dir_cb, cfg.max_fd, FTW_PHYS);
+        s_free(dirpath);
+    }
+    else
+        eprintf("%s: '%s'\n", strerror(errno), dir);
+    /* wait for workers to finish */
+    q_set_complete(q);
+    for ( int i = 0; i < cfg.nthreads; ++i )
+        pthread_join(thrinf[i].tid, NULL);
+    return rc;
+}
+
+/* callback for db_find_dupes() */
+static int find_dupes_cb(db_entry_t *dupes) {
+    printf("VIEW");
+    db_entry_t *p;
+    for ( p = dupes; NULL != p; p = p->aux ) {
+        printf(" '%s'", p->fname);
+    }
+    puts("");
+    return 0;
+}
+
+static void usage(char *pname, int ec) {
+    char *prog = basename(pname);
+    printf("%s - find potentially duplicate images\n", prog);
+    printf("USAGE:  %s [OPTIONS] DIR ...\n", prog);
+    printf("OPTIONS:\n"
+           "  -h        display this help text and exit\n"
+           "  -b float  blur factor; needs -r when changed between runs; default: 1.5\n"
+           "  -f file   fingerprint database file\n"
+           "  -m file   merge additional fingerprint file\n"
+           "  -p        prune missing files from database\n"
+           "  -r        rescan files already in database\n"
+           "  -t n      similarity threshold in bits (0..256) or percent; default: 256\n"
+           "  -T num    number of scan threads (default: 4)\n"
+    );
+    exit(ec);
+}
+
+/* main function */
+int main(int argc, char *argv[]) {
+    int c, rc = 0, n;
+    db_t *db ;
+
+    db = db_init();
+    while ( ( c = getopt( argc, argv, "+:hb:f:m:prt:T:" ) ) != -1 ) {
+        switch (c) {
+        case 'h':
+            usage(argv[0], EXIT_SUCCESS);
+            break;
+        case 'b':
+            n = atoi(optarg);
+            cfg.blur = n < 1 ? 1 : n;
+            break;
+        case 'f':
+            cfg.db_outfile = optarg;
+            /* fall through */
+        case 'm':
+            dprintf("loading '%s'\n", optarg);
+            n = db_read(db, optarg);
+            dprintf("loaded %d entries\n", n>0?n:0);
+            break;
+        case 'p':
+            cfg.db_prune = 1;
+            break;
+        case 'r':
+            cfg.rescan = 1;
+            break;
+        case 't': {
+                char *ep;
+                n = strtol(optarg, &ep, 10);
+                if ( '%' == *ep ) {
+                    n = n < 0 ? 0 : n > 100 ? 100 : n;
+                    n = 256 * n / 100;
+                }
+                cfg.thresh = n < 0 ? 0 : n > 256 ? 256 : n;
+            }
+            break;
+        case 'T':
+            n = atoi(optarg);
+            cfg.nthreads = n > 0 ? n : 1;
+            break;
+        case ':':
+            eprintf("ERROR: missing argument for option '-%c'\n", optopt);
+            usage(argv[0], EXIT_FAILURE);
+            break;
+        case '?':
+            eprintf("ERROR: unknown option '-%c'\n", optopt);
+            usage( argv[0], EXIT_FAILURE );
+            break;
+        default:
+            eprintf("WARNING: option '-%c' not implemented, programmer PEBKAC.\n", c);
+            break;
+        }
+    }
+
+    if ( cfg.db_prune ) {
+        dprintf("pruning\n");
+        db_prune(db);
+    }
+
+    q = q_init();
+    if (optind >= argc) {
+        dprintf("scanning '.'\n");
+        rc = scan_dir(".", q, db);
+    } else {
+        while ( optind < argc && 0 == rc ) {
+            dprintf("scanning '%s'\n", argv[optind]);
+            rc = scan_dir(argv[optind++], q, db);
+        }
+    }
+    q_destroy(&q);
+    if ( 0 != rc )
+        goto DONE;
+
+    if ( NULL != cfg.db_outfile ) {
+        int cnt;
+        cnt = db_write(db, cfg.db_outfile);
+        if ( 0 <= cnt )
+            dprintf("%d entries written\n", cnt);
+        else
+            eprintf("writing '%s' failed\n", cfg.db_outfile);
+    }
+
+    dprintf("searching dupes ...\n");
+    printf("#!/bin/bash\n");
+    printf(". ~/bin/imgmultiview.inc\n");
+    rc = db_find_dupes(db, cfg.thresh, find_dupes_cb);
+    printf("END\n");
+
+  DONE:
+    db_destroy(&db);
+    dprintf("done.\n");
+    exit(rc ? EXIT_FAILURE : EXIT_SUCCESS);
+}
+
+/* EOF */
diff --git a/queue.c b/queue.c
new file mode 100644 (file)
index 0000000..240c9c6
--- /dev/null
+++ b/queue.c
@@ -0,0 +1,83 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+
+#include "util.h"
+#include "queue.h"
+
+
+static inline int q_lock(queue_t *q) {
+    return pthread_mutex_lock(&q->mtx);
+}
+
+static inline int q_unlock(queue_t *q) {
+    return pthread_mutex_unlock(&q->mtx);
+}
+
+queue_t *q_init(void) {
+    queue_t *q = s_malloc(sizeof (queue_t));
+    q->penq = q->pdeq = NULL;
+    q->complete = 0;
+    pthread_mutex_init(&q->mtx, NULL);
+    return q;
+}
+
+void q_destroy(queue_t **pq) {
+    queue_t *q = *pq;
+    q_lock(q);
+    db_entry_t *p = q->pdeq, *next;
+    while ( NULL != p ) {
+        next = p->aux;
+        free(p->fname);
+        free(p);
+        p = next;
+    }
+    q->pdeq = q->penq = NULL;
+    q_unlock(q);
+    pthread_mutex_destroy(&q->mtx);
+    s_free(q);
+    *pq = NULL;
+}
+
+int q_enq(queue_t *q, db_entry_t *entry) {
+    q_lock(q);
+    if ( NULL == q->penq )   /* insert in empty queue */
+        q->penq = q->pdeq = entry;
+    else {
+        q->penq->aux = entry;
+        q->penq = entry;
+    }
+    q_unlock(q);
+    return 0;
+}
+
+db_entry_t *q_deq(queue_t *q) {
+    db_entry_t *entry;
+    q_lock(q);
+    entry = q->pdeq;
+    if ( NULL != entry ) {
+        if ( q->pdeq == q->penq )  /* queue becomes empty */
+            q->pdeq = q->penq = NULL;
+        else
+            q->pdeq = q->pdeq->aux;
+        entry->aux = NULL;
+    }
+    q_unlock(q);
+    return entry;
+}
+
+int q_complete(queue_t *q) {
+    int qcpl = 0;
+    q_lock(q);
+    qcpl = q->complete;
+    q_unlock(q);
+    return qcpl;
+}
+
+void q_set_complete(queue_t *q) {
+    q_lock(q);
+    q->complete = 1;
+    q_unlock(q);
+}
+
+/* EOF */
diff --git a/queue.h b/queue.h
new file mode 100644 (file)
index 0000000..11ab79d
--- /dev/null
+++ b/queue.h
@@ -0,0 +1,28 @@
+#ifndef QUEUE_H_INCLUDED
+#define QUEUE_H_INCLUDED
+
+#include <pthread.h>
+
+#include "db.h"
+
+typedef
+    struct queue_struct
+    queue_t;
+
+struct queue_struct {
+    db_entry_t *penq;
+    db_entry_t *pdeq;
+    int complete;
+    pthread_mutex_t mtx;
+};
+
+extern queue_t *q_init(void);
+extern void q_destroy(queue_t **pq);
+extern int q_enq(queue_t *q, db_entry_t *entry);
+extern db_entry_t *q_deq(queue_t *q);
+extern int q_complete(queue_t *q);
+extern void q_set_complete(queue_t *q);
+
+#endif //ndef QUEUE_H_INCLUDED
+
+/* EOF */
diff --git a/util.c b/util.c
new file mode 100644 (file)
index 0000000..f43cef6
--- /dev/null
+++ b/util.c
@@ -0,0 +1,32 @@
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+void die(int eno) {
+    if ( eno )
+        eprintf("%s\n", strerror(eno));
+    exit(EXIT_FAILURE);
+}
+
+void *s_malloc(size_t sz) {
+    void *m = malloc(sz);
+    if ( NULL == m )
+        die(errno);
+    return m;
+}
+
+void *s_strdup(const char *s) {
+    void *d = strdup(s);
+    if ( NULL == d )
+        die(errno);
+    return d;
+}
+
+void s_free(void *p){
+    free(p);
+}
+
+/* EOF */
diff --git a/util.h b/util.h
new file mode 100644 (file)
index 0000000..7890c17
--- /dev/null
+++ b/util.h
@@ -0,0 +1,21 @@
+#ifndef UTIL_H_INCLUDED
+#define UTIL_H_INCLUDED
+
+#define ARR_SIZE(a) (sizeof(a)/sizeof(*a))
+
+#define eprintf(...)    fprintf(stderr, __VA_ARGS__)
+
+#ifdef DEBUG
+    #define dprintf(...)    do {                                       \
+        fprintf(stderr, "(%s|%s|%d) ", __FILE__, __func__, __LINE__);  \
+        fprintf(stderr, __VA_ARGS__);  } while(0)
+#else
+    #define dprintf(...)
+#endif
+
+extern void die(int eno);
+extern void *s_malloc(size_t sz);
+extern void *s_strdup(const char *s);
+extern void s_free(void *p);
+
+#endif //ndef UTIL_H_INCLUDED