diff options
| author | Xavier Del Campo Romero <xavi.dcr@tutanota.com> | 2023-07-23 00:31:17 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-09-24 11:03:39 +0200 |
| commit | b9537b1d1164b9f3fbe704512b1e324c2e37beb5 (patch) | |
| tree | a2538bd8698b208431fa85a3f65cd8bafbf0bbeb /thumbnail | |
| parent | 7407ddf0162cc32fee03a309186588047a65f42a (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.txt | 10 | ||||
| -rw-r--r-- | thumbnail/Makefile | 29 | ||||
| l--------- | thumbnail/cftw.c | 1 | ||||
| l--------- | thumbnail/cftw.h | 1 | ||||
| l--------- | thumbnail/crealpath.c | 1 | ||||
| l--------- | thumbnail/crealpath.h | 1 | ||||
| -rw-r--r-- | thumbnail/main.c | 759 |
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; +} |
