aboutsummaryrefslogtreecommitdiff
path: root/thumbnail
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi.dcr@tutanota.com>2023-07-23 00:31:17 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2025-09-24 11:03:39 +0200
commitb9537b1d1164b9f3fbe704512b1e324c2e37beb5 (patch)
treea2538bd8698b208431fa85a3f65cd8bafbf0bbeb /thumbnail
parent7407ddf0162cc32fee03a309186588047a65f42a (diff)
Add thumbnail generation tool
This new application runs separately from slcl and communicates with it via a named pipe. When files are added/removed to/from the user directory, slcl shall write to the named pipe. Then, this new tool shall process incoming entries and generate or remove thumbnails accordingly. Such thumbnails are stored into a new directory inside the database directory, namely thumbnails/, which replicates the same structure as user/.
Diffstat (limited to 'thumbnail')
-rw-r--r--thumbnail/CMakeLists.txt10
-rw-r--r--thumbnail/Makefile29
l---------thumbnail/cftw.c1
l---------thumbnail/cftw.h1
l---------thumbnail/crealpath.c1
l---------thumbnail/crealpath.h1
-rw-r--r--thumbnail/main.c759
7 files changed, 802 insertions, 0 deletions
diff --git a/thumbnail/CMakeLists.txt b/thumbnail/CMakeLists.txt
new file mode 100644
index 0000000..9df2f34
--- /dev/null
+++ b/thumbnail/CMakeLists.txt
@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 3.13)
+project(thumbnail)
+add_executable(${PROJECT_NAME} cftw.c crealpath.c main.c)
+add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../libweb/dynstr
+ ${CMAKE_BINARY_DIR}/dynstr)
+target_include_directories(${PROJECT_NAME} PRIVATE ..)
+target_compile_definitions(${PROJECT_NAME} PRIVATE _FILE_OFFSET_BITS=64)
+include(FindPkgConfig)
+pkg_check_modules(ImageMagick REQUIRED IMPORTED_TARGET ImageMagick)
+target_link_libraries(${PROJECT_NAME} PRIVATE dynstr PkgConfig::ImageMagick)
diff --git a/thumbnail/Makefile b/thumbnail/Makefile
new file mode 100644
index 0000000..d921e85
--- /dev/null
+++ b/thumbnail/Makefile
@@ -0,0 +1,29 @@
+.POSIX:
+
+PROJECT = thumbnail
+O = -Og
+CDEFS = -D_FILE_OFFSET_BITS=64 # Required for large file support on 32-bit.
+CFLAGS = $(O) $(CDEFS) -g -Wall -I../libweb/dynstr/include -I. \
+ $$(pkg-config --cflags ImageMagick) -MD -MF $(@:.o=.d)
+LDFLAGS = $(LIBS)
+DEPS = $(OBJECTS:.o=.d)
+DYNSTR = ../libweb/dynstr/libdynstr.a
+DYNSTR_LIBS = -L../libweb/dynstr -ldynstr
+LIBS = $$(pkg-config --libs ImageMagick) $(DYNSTR_LIBS)
+OBJECTS = \
+ crealpath.o \
+ main.o \
+ cftw.o
+
+all: $(PROJECT)
+
+clean:
+ rm -f $(OBJECTS) $(DEPS)
+
+$(PROJECT): $(OBJECTS) $(DYNSTR)
+ $(CC) $(OBJECTS) $(LDFLAGS) $(LIBS) -o $@
+
+$(DYNSTR):
+ +cd ../dynstr && $(MAKE)
+
+-include $(DEPS)
diff --git a/thumbnail/cftw.c b/thumbnail/cftw.c
new file mode 120000
index 0000000..36b7848
--- /dev/null
+++ b/thumbnail/cftw.c
@@ -0,0 +1 @@
+../cftw.c \ No newline at end of file
diff --git a/thumbnail/cftw.h b/thumbnail/cftw.h
new file mode 120000
index 0000000..81aca3b
--- /dev/null
+++ b/thumbnail/cftw.h
@@ -0,0 +1 @@
+../cftw.h \ No newline at end of file
diff --git a/thumbnail/crealpath.c b/thumbnail/crealpath.c
new file mode 120000
index 0000000..81bb5fa
--- /dev/null
+++ b/thumbnail/crealpath.c
@@ -0,0 +1 @@
+../crealpath.c \ No newline at end of file
diff --git a/thumbnail/crealpath.h b/thumbnail/crealpath.h
new file mode 120000
index 0000000..f6d37ff
--- /dev/null
+++ b/thumbnail/crealpath.h
@@ -0,0 +1 @@
+../crealpath.h \ No newline at end of file
diff --git a/thumbnail/main.c b/thumbnail/main.c
new file mode 100644
index 0000000..bbdc7f0
--- /dev/null
+++ b/thumbnail/main.c
@@ -0,0 +1,759 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "crealpath.h"
+#include <cftw.h>
+#include <dynstr.h>
+#include <magick/api.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <strings.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int print_error(const char *const func, const char *const ifunc,
+ const ExceptionInfo *const exc)
+{
+ const char *const reason = exc->reason ? exc->reason : "n/a",
+ *const description = exc->description ?
+ exc->description : "n/a";
+
+ return fprintf(stderr, "%s: %s failed: reason: %s, description: %s\n",
+ func, ifunc, reason, description);
+}
+
+static int create_thumbnail(const char *const src, const char *const dst,
+ const unsigned long size)
+{
+ int ret = -1;
+ const size_t slen = strlen(src), dlen = strlen(dst);
+ ImageInfo *info = NULL;
+ Image *i = NULL, *t = NULL;
+ ExceptionInfo *const exc = AcquireExceptionInfo();
+ /* Ensure permissions for files created by MagickCore.*/
+ const mode_t mode = umask(~(S_IRUSR | S_IWUSR));
+
+ if (slen >= sizeof i->filename)
+ {
+ fprintf(stderr, "%s: src maximum length exceeded (%zu, max %zu)\n",
+ __func__, slen, sizeof i->filename - 1);
+ goto end;
+ }
+ else if (dlen >= sizeof t->filename)
+ {
+ fprintf(stderr, "%s: dst maximum length exceeded (%zu, max %zu)\n",
+ __func__, slen, sizeof t->filename - 1);
+ goto end;
+ }
+ else if (!(info = CloneImageInfo(NULL)))
+ {
+ fprintf(stderr, "%s: CloneImageInfo failed\n", __func__);
+ goto end;
+ }
+
+ strcpy(info->filename, src);
+ info->adjoin = MagickTrue;
+
+ if (!(i = ReadImage(info, exc)))
+ {
+ switch (exc->severity)
+ {
+ case DelegateError:
+ case CorruptImageError:
+ case FileOpenError:
+ case MissingDelegateError:
+ ret = 1;
+ break;
+
+ default:
+ print_error(__func__, "ReadImage", exc);
+ break;
+ }
+
+ goto end;
+ }
+
+ if (!i->rows || !i->columns)
+ {
+ ret = 1;
+ goto end;
+ }
+
+ unsigned long x, y;
+
+ if (i->columns > i->rows)
+ {
+ x = i->columns > size ? size : i->columns;
+ y = x * i->rows / i->columns;
+ }
+ else
+ {
+ y = i->rows > size ? size : i->rows;
+ x = y * i->columns / i->rows;
+ }
+
+ if (!(t = ResizeImage(i, x, y, PointFilter, 1, exc)))
+ {
+ print_error(__func__, "ResizeImage", exc);
+ goto end;
+ }
+ else if (WriteImages(info, t, dst, exc) != MagickTrue)
+ {
+ print_error(__func__, "WriteImages", exc);
+ goto end;
+ }
+
+ printf("Created %s\n", dst);
+ ret = 0;
+
+end:
+ umask(mode);
+
+ if (info)
+ DestroyImageInfo(info);
+
+ if (i)
+ DestroyImage(i);
+
+ if (t)
+ DestroyImage(t);
+
+ if (exc)
+ DestroyExceptionInfo(exc);
+
+ return ret;
+}
+
+static char *fifo_getline(const int fd)
+{
+ char *ret = NULL;
+ size_t n = 0;
+
+ for (;;)
+ {
+ char c;
+ ssize_t sz;
+
+again:
+ sz = read(fd, &c, sizeof c);
+
+ if (sz < 0)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ goto again;
+ else
+ fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno));
+
+ goto failure;
+ }
+ else if (c == '\n')
+ break;
+
+ char *const aux = realloc(ret, n + 1);
+
+ if (!aux)
+ {
+ fprintf(stderr, "%s: realloc(3): %s\n",
+ __func__, strerror(errno));
+ goto failure;
+ }
+
+ aux[n++] = c;
+ ret = aux;
+ }
+
+ char *const aux = realloc(ret, n + 1);
+
+ if (!aux)
+ {
+ fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno));
+ goto failure;
+ }
+
+ aux[n++] = '\0';
+ ret = aux;
+ return ret;
+
+failure:
+ free(ret);
+ return NULL;
+}
+
+static int ensure_dir(const char *const path, const size_t len)
+{
+ int ret = -1;
+ struct dynstr d;
+ struct stat sb;
+
+ dynstr_init(&d);
+
+ if (dynstr_append(&d, "%.*s", (int)len, path))
+ {
+ fprintf(stderr, "%s: dynstr_append failed\n", __func__);
+ goto end;
+ }
+ else if (stat(d.str, &sb))
+ {
+ if (errno != ENOENT && errno != ENOTDIR)
+ {
+ fprintf(stderr, "%s: stat(2) %s: %s\n",
+ __func__, d.str, strerror(errno));
+ goto end;
+ }
+ else if (mkdir(d.str, S_IRWXU))
+ {
+ fprintf(stderr, "%s: mkdir(2): %s\n", __func__, strerror(errno));
+ goto end;
+ }
+ }
+
+ ret = 0;
+
+end:
+ dynstr_free(&d);
+ return ret;
+}
+
+static int ensure_dirs(const char *const path)
+{
+ for (const char *p = path; p; p = strchr(p, '/'))
+ {
+ const char *const next = p + 1;
+
+ if (*next == '/')
+ {
+ p = next;
+ continue;
+ }
+ else if (!*next)
+ {
+ fprintf(stderr, "%s: expected file path: %s\n", __func__, path);
+ return 1;
+ }
+ else if (!(p = strchr(next, '/')))
+ break;
+ else if (ensure_dir(path, p - path))
+ {
+ fprintf(stderr, "%s: ensure_dir failed\n", __func__);
+ return -1;
+ }
+
+ p = next;
+ }
+
+ return 0;
+}
+
+static bool extension_allowed(const char *const path)
+{
+ static const char *const extensions[] =
+ {
+ ".jpg", ".jpeg", ".png", ".bmp"
+ };
+
+ for (size_t i = 0; i < sizeof extensions / sizeof *extensions; i++)
+ {
+ const char *const ext = extensions[i];
+ const size_t len = strlen(path), elen = strlen(ext);
+
+ if (len < elen)
+ continue;
+ else if (!strcasecmp(path + len - elen, ext))
+ return true;
+ }
+
+ return false;
+}
+
+static int process_add(const char *const abspath, const char *const relpath,
+ const char *const user, const char *const thumbnail,
+ const unsigned long size)
+{
+ int ret = -1;
+ struct dynstr d;
+
+ dynstr_init(&d);
+
+ if (!extension_allowed(abspath))
+ ;
+ else if (dynstr_append(&d, "%s%s", thumbnail, relpath))
+ {
+ fprintf(stderr, "%s: dynstr_append failed\n", __func__);
+ goto end;
+ }
+ else if ((ret = ensure_dirs(d.str)))
+ {
+ fprintf(stderr, "%s: ensure_dirs failed\n", __func__);
+ goto end;
+ }
+ else if ((ret = create_thumbnail(abspath, d.str, size)))
+ {
+ if (ret < 0)
+ fprintf(stderr, "%s: create_thumbnail failed\n", __func__);
+
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ dynstr_free(&d);
+ return ret;
+}
+
+static int process_rm(const char *const abspath, const char *const relpath,
+ const char *const user, const char *const thumbnail,
+ const unsigned long size)
+{
+ int ret = -1;
+ struct dynstr d;
+
+ dynstr_init(&d);
+
+ if (dynstr_append(&d, "%s%s", thumbnail, relpath))
+ {
+ fprintf(stderr, "%s: dynstr_append failed\n", __func__);
+ goto end;
+ }
+ else if (remove(d.str) && errno != ENOENT && errno != ENOTDIR)
+ {
+ fprintf(stderr, "%s: remove(3) %s: %s\n",
+ __func__, d.str, strerror(errno));
+ goto end;
+ }
+ else
+ printf("Removed %s\n", d.str);
+
+ ret = 0;
+
+end:
+ dynstr_free(&d);
+ return ret;
+}
+
+static int process_line(const char *const line, const char *const user,
+ const char *const thumbnail, const unsigned long size)
+{
+ int ret = -1;
+ const char *p = line;
+ static const char template[] = "+ f";
+ const size_t len = strlen(p);
+
+ if (len < strlen(template))
+ {
+ fprintf(stderr, "%s: invalid input: %s\n", __func__, p);
+ ret = 1;
+ goto end;
+ }
+
+ int (*f)(const char *, const char *, const char *, const char *,
+ unsigned long) = NULL;
+
+ switch (*p)
+ {
+ case '+':
+ f = process_add;
+ break;
+
+ case '-':
+ f = process_rm;
+ break;
+
+ default:
+ fprintf(stderr, "%s: unexpected operator %c\n", __func__, *p);
+ ret = 1;
+ goto end;
+ }
+
+ if (*++p != ' ')
+ {
+ fprintf(stderr, "%s: expected whitespace: %s\n", __func__, line);
+ ret = 1;
+ goto end;
+ }
+
+ while (*p == ' ')
+ p++;
+
+ if (!*p)
+ {
+ fprintf(stderr, "%s: expected path: %s\n", __func__, line);
+ ret = 1;
+ goto end;
+ }
+
+ const char *const path = p, *rel;
+
+ if (strstr(path, user) != path)
+ {
+ fprintf(stderr, "%s: expected user directory (%s): %s\n",
+ __func__, user, line);
+ ret = 1;
+ goto end;
+ }
+
+ rel = path + strlen(user);
+
+ if ((ret = f(path, rel, user, thumbnail, size)) < 0)
+ fprintf(stderr, "%s: callback failed\n", __func__);
+
+end:
+ return ret;
+}
+
+static int process_input(const int fd, const char *const user,
+ const char *const thumbnail, const unsigned long size)
+{
+ int ret = -1;
+ char *const line = fifo_getline(fd);
+
+ if (!line)
+ {
+ fprintf(stderr, "%s: fifo_getline failed\n", __func__);
+ goto end;
+ }
+ else if (*line && (ret = process_line(line, user, thumbnail, size)))
+ {
+ if (ret < 0)
+ fprintf(stderr, "%s: process_line failed\n", __func__);
+
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ free(line);
+ return ret;
+}
+
+static volatile sig_atomic_t do_exit;
+
+static int process(const int fd, const char *const user,
+ const char *const thumbnail, const unsigned long size)
+{
+ while (!do_exit)
+ {
+ struct pollfd fds =
+ {
+ .fd = fd,
+ .events = POLLIN
+ };
+
+ int ret, res;
+
+again:
+ res = poll(&fds, 1, -1);
+
+ if (res < 0)
+ {
+ switch (errno)
+ {
+ case EAGAIN:
+ /* Fall through. */
+ case EINTR:
+ if (do_exit)
+ goto end;
+
+ goto again;
+
+ default:
+ fprintf(stderr, "%s: poll(2): %s\n",
+ __func__, strerror(errno));
+ return -1;
+ }
+ }
+ else if (!res)
+ {
+ fprintf(stderr, "%s: poll(2) returned zero\n", __func__);
+ return -1;
+ }
+ else if (fds.revents & POLLHUP)
+ {
+ printf("FIFO closed by external process\n");
+ break;
+ }
+ else if ((ret = process_input(fd, user, thumbnail, size)) < 0)
+ {
+ fprintf(stderr, "%s: process_input failed\n", __func__);
+ return ret;
+ }
+ }
+
+end:
+ printf("Exiting...\n");
+ return 0;
+}
+
+struct args
+{
+ const char *user, *thumbnail;
+ unsigned long size;
+ bool force;
+};
+
+static int do_generate(const char *fpath,
+ const struct stat *sb, bool *done, void *user)
+{
+ int ret = -1;
+ const struct args *const a = user;
+ const char *const relpath = fpath + strlen(a->user);
+ bool generate = false;
+ struct dynstr d;
+ struct stat tsb;
+
+ dynstr_init(&d);
+
+ if (do_exit)
+ *done = true;
+ else if (!S_ISREG(sb->st_mode) || !extension_allowed(fpath))
+ ;
+ else if (dynstr_append(&d, "%s%s", a->thumbnail, relpath))
+ {
+ fprintf(stderr, "%s: dynstr_append failed\n", __func__);
+ goto end;
+ }
+ else if (stat(d.str, &tsb))
+ {
+ if (errno != ENOENT && errno != ENOTDIR)
+ {
+ fprintf(stderr, "%s: stat(2) %s: %s\n",
+ __func__, d.str, strerror(errno));
+ goto end;
+ }
+ else
+ generate = true;
+ }
+ else if (a->force)
+ generate = true;
+
+ if (generate)
+ {
+ if (ensure_dirs(d.str))
+ {
+ fprintf(stderr, "%s: ensure_dirs failed\n", __func__);
+ goto end;
+ }
+ else if ((ret = create_thumbnail(fpath, d.str, a->size)) < 0)
+ {
+ fprintf(stderr, "%s: create_thumbnail faled\n", __func__);
+ goto end;
+ }
+ }
+
+ ret = 0;
+
+end:
+ dynstr_free(&d);
+ return ret;
+}
+
+static int generate_existing(const char *const user,
+ const char *const thumbnail, const unsigned long size, const bool force)
+{
+ struct args a =
+ {
+ .user = user,
+ .thumbnail = thumbnail,
+ .size = size,
+ .force = force
+ };
+
+ printf("Scanning existing files...\n");
+
+ if (cftw(user, do_generate, &a))
+ {
+ fprintf(stderr, "%s: cftw failed\n", __func__);
+ return -1;
+ }
+
+ printf("Finished scanning\n");
+ return 0;
+}
+
+static void handle_signal(const int signum)
+{
+ switch (signum)
+ {
+ case SIGINT:
+ /* Fall through. */
+ case SIGTERM:
+ do_exit = 1;
+ break;
+
+ default:
+ break;
+ }
+}
+
+static int init_signals(void)
+{
+ struct sigaction sa =
+ {
+ .sa_handler = handle_signal,
+ .sa_flags = SA_RESTART
+ };
+
+ sigemptyset(&sa.sa_mask);
+
+ if (sigaction(SIGINT, &sa, NULL))
+ {
+ fprintf(stderr, "%s: sigaction(2) SIGINT: %s\n",
+ __func__, strerror(errno));
+ return -1;
+ }
+ else if (sigaction(SIGTERM, &sa, NULL))
+ {
+ fprintf(stderr, "%s: sigaction(2) SIGTERM: %s\n",
+ __func__, strerror(errno));
+ return -1;
+ }
+ else if (sigaction(SIGPIPE, &sa, NULL))
+ {
+ fprintf(stderr, "%s: sigaction(2) SIGPIPE: %s\n",
+ __func__, strerror(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+static void usage(char *const argv[])
+{
+ fprintf(stderr, "%s [-f] [-s size] dir\n", *argv);
+}
+
+static int parse_args(const int argc, char *const argv[],
+ const char **const dir, unsigned long *const size, bool *const force)
+{
+ int opt;
+
+ /* Default values. */
+ *size = 96;
+ *force = false;
+
+ while ((opt = getopt(argc, argv, "fs:")) != -1)
+ {
+ switch (opt)
+ {
+ case 'f':
+ *force = true;
+ break;
+
+ case 's':
+ {
+ char *endptr;
+
+ errno = 0;
+ *size = strtoul(optarg, &endptr, 10);
+
+ if (errno || *endptr)
+ {
+ fprintf(stderr, "%s: invalid size %s\n", __func__, optarg);
+ return -1;
+ }
+ }
+ break;
+
+ default:
+ usage(argv);
+ return -1;
+ }
+ }
+
+ if (optind >= argc)
+ {
+ usage(argv);
+ return -1;
+ }
+
+ *dir = argv[optind];
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_FAILURE, fd = - 1;
+ char *rpath = NULL;
+ struct dynstr fifo, thumbnail, user;
+ const char *dir;
+ unsigned long size;
+ bool force;
+
+ MagickCoreGenesis(*argv, MagickTrue);
+ dynstr_init(&fifo);
+ dynstr_init(&thumbnail);
+ dynstr_init(&user);
+
+ if (parse_args(argc, argv, &dir, &size, &force))
+ {
+ usage(argv);
+ goto end;
+ }
+ else if (init_signals())
+ {
+ fprintf(stderr, "%s: init_signals failed\n", __func__);
+ goto end;
+ }
+ else if (!(rpath = crealpath(dir)))
+ {
+ fprintf(stderr, "%s: realpath(3): %s\n", __func__, strerror(errno));
+ goto end;
+ }
+ else if (dynstr_append(&thumbnail, "%s/thumbnails", rpath))
+ {
+ fprintf(stderr, "%s: dynstr_append thumbnail failed\n", __func__);
+ goto end;
+ }
+ else if (dynstr_append(&user, "%s/user", rpath))
+ {
+ fprintf(stderr, "%s: dynstr_append user failed\n", __func__);
+ goto end;
+ }
+ else if (dynstr_append(&fifo, "%s/slcl.fifo", rpath))
+ {
+ fprintf(stderr, "%s: dynstr_append fifo failed\n", __func__);
+ goto end;
+ }
+ else if ((fd = open(fifo.str, O_RDONLY | O_NONBLOCK)) < 0)
+ {
+ fprintf(stderr, "%s: open(2) %s: %s\n",
+ __func__, fifo.str, strerror(errno));
+ goto end;
+ }
+ else if (generate_existing(user.str, thumbnail.str, size, force))
+ {
+ fprintf(stderr, "%s: force_generate failed\n", __func__);
+ goto end;
+ }
+ else if (process(fd, user.str, thumbnail.str, size))
+ {
+ fprintf(stderr, "%s: process failed\n", __func__);
+ goto end;
+ }
+
+ ret = EXIT_SUCCESS;
+
+end:
+ free(rpath);
+ dynstr_free(&fifo);
+ dynstr_free(&thumbnail);
+ dynstr_free(&user);
+
+ if (fd >= 0 && close(fd))
+ {
+ fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno));
+ ret = EXIT_FAILURE;
+ }
+
+ MagickCoreTerminus();
+ return ret;
+}