From fa97b1904c72c184b09c627c7ccab0d0db4a060c Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Romero Date: Fri, 24 Mar 2023 02:39:23 +0100 Subject: [PATCH] WIP thumbnail stuff --- .gitignore | 1 + CMakeLists.txt | 20 +++- Makefile | 36 ------- README.md | 11 +- configure | 131 ++++++++++++++++++++++++ doc/Makefile | 14 +++ main.c | 222 +++++++++++++++++++++++++++++------------ page.c | 254 +++++++++++++++++++++++++++++++++++++++-------- page.h | 10 +- thumbnail.c | 143 ++++++++++++++++++++++++++ thumbnail.h | 15 +++ thumbnail_stub.c | 17 ++++ usergen | 1 + 13 files changed, 730 insertions(+), 145 deletions(-) delete mode 100644 Makefile create mode 100755 configure create mode 100644 doc/Makefile create mode 100644 thumbnail.c create mode 100644 thumbnail.h create mode 100644 thumbnail_stub.c diff --git a/.gitignore b/.gitignore index 339e89f..6f7f1b9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ build/ slcl *.o *.d +/Makefile diff --git a/CMakeLists.txt b/CMakeLists.txt index 83c25b1..deb3ba2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.13) project(slcl) +option(THUMBNAILS "Enables thumbnail generation for images." OFF) add_executable(${PROJECT_NAME} auth.c base64.c @@ -13,9 +14,26 @@ add_executable(${PROJECT_NAME} page.c server.c ) + +if(NOT DEFINED THUMBNAIL_HEIGHT) + set(THUMBNAIL_HEIGHT 96) +endif() + +if(THUMBNAILS) + find_package(ImageMagick REQUIRED) + target_sources(${PROJECT_NAME} PRIVATE thumbnail.c) + target_compile_definitions(${PROJECT_NAME} + PRIVATE THUMBNAIL_HEIGHT=${THUMBNAIL_HEIGHT}) +elseif(THUMBNAIL_HEIGHT) + target_sources(${PROJECT_NAME} PRIVATE thumbnail_stub.c) +endif() + target_compile_options(${PROJECT_NAME} PRIVATE -Wall) target_compile_definitions(${PROJECT_NAME} PRIVATE _FILE_OFFSET_BITS=64) add_subdirectory(dynstr) +add_subdirectory(mkdir_r) find_package(cJSON 1.0 REQUIRED) find_package(OpenSSL 3.0 REQUIRED) -target_link_libraries(${PROJECT_NAME} PRIVATE dynstr cjson OpenSSL::SSL) +target_link_libraries(${PROJECT_NAME} + PRIVATE dynstr mkdir_r cjson OpenSSL::SSL ImageMagick) +#message(FATAL_ERROR "TODO") diff --git a/Makefile b/Makefile deleted file mode 100644 index 36bc50f..0000000 --- a/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -.POSIX: -.SUFFIXES: .c .o - -PROJECT = slcl -O = -Og -CDEFS = -D_FILE_OFFSET_BITS=64 # Required for large file support on 32-bit. -CFLAGS = $(O) $(CDEFS) -g -Wall -Idynstr/include -MD -MF - -LIBS = -lcjson -lssl -lm -lcrypto -LDFLAGS = $(LIBS) -DEPS = $(OBJECTS:.o=.d) -OBJECTS = \ - auth.o \ - base64.o \ - cftw.o \ - handler.o \ - hex.o \ - html.o \ - http.o \ - jwt.o \ - main.o \ - page.o \ - server.o \ - dynstr/dynstr.o - -all: $(PROJECT) - -clean: - rm -f $(OBJECTS) $(DEPS) - -$(PROJECT): $(OBJECTS) - $(CC) $(OBJECTS) $(LDFLAGS) -o $@ - -.c.o: - $(CC) $(CFLAGS) -c $< -o $@ > $(@:.o=.d) - --include $(DEPS) diff --git a/README.md b/README.md index 9f37f75..a6feb02 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ simplicity and efficiency. - Its own, tiny HTTP/1.0 and 1.1-compatible server. - A simple JSON file as the credentials database. - No JavaScript. +- Optional, resizable thumbnails (made available by +[GraphicsMagick](http://www.graphicsmagick.org/)). ### TLS @@ -42,9 +44,12 @@ to `slcl`. If required, encryption should be done before uploading e.g.: using - cJSON >= 1.7.15. - [`dynstr`](https://gitea.privatedns.org/xavi92/dynstr) (provided as a `git` submodule). +- [`mkdir_r`](https://gitea.privatedns.org/xavi92/mkdir_r) +(provided as a `git` submodule). - `xxd` (for [`usergen`](usergen) only). - `jq` (for [`usergen`](usergen) only). - CMake (optional). +- [GraphicsMagick](http://www.graphicsmagick.org/) >= 1.4 (optional). ### Ubuntu / Debian @@ -57,7 +62,7 @@ sudo apt install build-essential libcjson-dev libssl-dev #### Optional packages ```sh -sudo apt install cmake xxd jq +sudo apt install cmake xxd jq libgraphicsmagick1-dev ``` ## How to use @@ -66,7 +71,8 @@ sudo apt install cmake xxd jq Two build environments are provided for `slcl` - feel free to choose any of them: -- A mostly POSIX-compliant [`Makefile`](Makefile). +- A [`configure`](configure) POSIX shell script that generates a mostly +POSIX-compliant `Makefile`. - A [`CMakeLists.txt`](CMakeLists.txt). `slcl` can be built using the standard build process: @@ -74,6 +80,7 @@ them: #### Make ```sh +$ ./configure $ make ``` diff --git a/configure b/configure new file mode 100755 index 0000000..18f3986 --- /dev/null +++ b/configure @@ -0,0 +1,131 @@ +#! /bin/sh + +set -e + +DEFAULT_PREFIX=/usr/local +DEFAULT_THUMBNAIL_HEIGHT=96 + +usage() +{ + printf "$0 [OPTION]...\n\n" + printf "Configuration:\n" + printf "%s\t\t\t" "--prefix=PREFIX" + printf "Sets installation prefix (default: $DEFAULT_PREFIX).\n" + printf "%s\t\t\t" "--thumbnails" + printf "Enables thumbnail generation for images.\n" + printf "%s\t" "--thumbnail-height=HEIGHT" + printf "Sets thumbnail height, in pixels " + printf "(default: $DEFAULT_THUMBNAIL_HEIGHT). " + printf "Requires --thumbnails.\n" + printf "%s\t\t\t" "-h|--help" + printf "Prints this help page.\n" +} + +CC=${CC:-cc} +CFLAGS=${CFLAGS:-} + +for arg +do + case "$arg" in + --thumbnails) + THUMBNAILS=1 + ;; + --thumbnail-height=*) + THUMBNAIL_HEIGHT=${arg#*=} + ;; + --prefix=*) + PREFIX=${arg#*=} + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "Invalid argument $arg" >&2 + usage >&2 + exit 1 + ;; + esac +done + +THUMBNAILS=${THUMBNAILS:-0} +THUMBNAIL_HEIGHT=${THUMBNAIL_HEIGHT:-$DEFAULT_THUMBNAIL_HEIGHT} +PREFIX=${PREFIX:-$DEFAULT_PREFIX} + +OBJECTS="\ + auth.o \ + base64.o \ + cftw.o \ + handler.o \ + hex.o \ + html.o \ + http.o \ + jwt.o \ + main.o \ + page.o \ + server.o \ + dynstr/dynstr.o" + +LIBS="\ + -lcjson \ + -lssl \ + -lm \ + -lcrypto" + +CFLAGS=${CFLAGS:-'$(O) $(CDEFS) -g -Wall -Idynstr/include -Imkdir_r -MD -MF -'} + +if [ $THUMBNAILS -ne 0 ] +then + LIBS="$LIBS $(GraphicsMagick-config --libs)" + CFLAGS="$CFLAGS $(GraphicsMagick-config --cppflags | + tr '\n' ' ')" + CFLAGS="$CFLAGS -DTHUMBNAIL_HEIGHT=$THUMBNAIL_HEIGHT" + CFLAGS="$CFLAGS -Imkdir_r/private_include" + LDFLAGS="$LDFLAGS $(GraphicsMagick-config --ldflags)" + OBJECTS="$OBJECTS \ + thumbnail.o \ + mkdir_r/mkdir_r.o \ + mkdir_r/posix.o" +else + OBJECTS="$OBJECTS thumbnail_stub.o mkdir_r_stub.o" +fi + +cat > Makefile <<"EOF" +.POSIX: +.SUFFIXES: .c .o + +PROJECT = slcl +O = -Og +CDEFS = -D_FILE_OFFSET_BITS=64 # Required for large file support on 32-bit. +DEPS = $(OBJECTS:.o=.d) +EOF + +cat >> Makefile <> Makefile <<"EOF" +all: $(PROJECT) + +clean: + rm -f $(OBJECTS) $(DEPS) + +install: $(PROJECT) + mkdir -p $(PREFIX) $(PREFIX)/bin + cp $(PROJECT) $(PREFIX)/bin + chmod 0755 $(PREFIX)/bin/$(PROJECT) + +cd doc && $(MAKE) install PREFIX=$(PREFIX) + +$(PROJECT): $(OBJECTS) + $(CC) $(OBJECTS) $(LDFLAGS) $(LIBS) -o $@ + +.c.o: + $(CC) $(CFLAGS) -c $< -o $@ > $(@:.o=.d) + +-include $(DEPS) +EOF diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..ca581a3 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,14 @@ +.POSIX: + +DOCS1 = \ + man1/slcl.1 \ + man1/usergen.1 + +DOCSPREFIX=$(PREFIX)/share/man +DOCS1PREFIX=$(DOCSPREFIX)/man1 + +install: $(DOCS1) + test $(PREFIX) || (echo Please define PREFIX. >&2 ; exit 1) + mkdir -p $(DOCS1PREFIX) + cp $(DOCS1) $(DOCS1PREFIX) + for d in $(DOCS1); do chmod 0644 $(DOCSPREFIX)/$$d; done diff --git a/main.c b/main.c index 66328a8..b411a0f 100644 --- a/main.c +++ b/main.c @@ -5,7 +5,9 @@ #include "handler.h" #include "hex.h" #include "http.h" +#include "mkdir_r.h" #include "page.h" +#include "thumbnail.h" #include #include #include @@ -643,58 +645,54 @@ static int check_length(const unsigned long long len, static int getnode(const struct http_payload *const p, struct http_response *const r, void *const user) { + int ret = -1; struct auth *const a = user; + const char *adir, *const username = p->cookie.field; + struct dynstr d; + + dynstr_init(&d); if (auth_cookie(a, &p->cookie)) { fprintf(stderr, "%s: auth_cookie failed\n", __func__); - return page_forbidden(r); + ret = page_forbidden(r); + goto end; } - - const char *const username = p->cookie.field, - *const resource = p->resource + strlen("/user/"); - - if (path_isrel(resource)) + else if (path_isrel(p->resource)) { - fprintf(stderr, "%s: illegal relative path %s\n", __func__, resource); - return page_forbidden(r); + fprintf(stderr, "%s: illegal relative path %s\n", + __func__, p->resource); + ret = page_forbidden(r); + goto end; } - - int ret = -1; - struct dynstr dir, root, d; - const char *const adir = auth_dir(a), - *const sep = p->resource[strlen(p->resource) - 1] != '/' ? "/" : ""; - - dynstr_init(&dir); - dynstr_init(&d); - dynstr_init(&root); - - if (!adir) + else if (!(adir = auth_dir(a))) { fprintf(stderr, "%s: auth_dir failed\n", __func__); goto end; } - else if (dynstr_append(&dir, "%s%s", p->resource, sep)) - { - fprintf(stderr, "%s: dynstr_append dird failed\n", __func__); - goto end; - } - else if (dynstr_append(&root, "%s/user/%s/", adir, username) - || dynstr_append(&d, "%s%s", root.str, resource)) + else if (dynstr_append(&d, "%s/user/%s/", adir, username)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } + const struct page_resource res = + { + .root = adir, + .username = username, + .user_root = d.str, + .res = p->resource + strlen("/user") + }; + bool available; unsigned long long cur, max; - if (auth_quota(a, username, &available, &max)) + if (auth_quota(a, res.username, &available, &max)) { fprintf(stderr, "%s: quota_available failed\n", __func__); goto end; } - else if (available && quota_current(a, username, &cur)) + else if (available && quota_current(a, res.username, &cur)) { fprintf(stderr, "%s: quota_current failed\n", __func__); goto end; @@ -707,12 +705,10 @@ static int getnode(const struct http_payload *const p, .max = max } : NULL; - ret = page_resource(r, dir.str, root.str, d.str, ppq); + ret = page_resource(r, &res, ppq); end: - dynstr_free(&dir); dynstr_free(&d); - dynstr_free(&root); return ret; } @@ -797,25 +793,84 @@ static int rename_or_move(const char *const old, const char *const new) return res; } +static int ensure_dir(const char *const dir) +{ + struct stat sb; + + if (stat(dir, &sb)) + { + switch (errno) + { + case ENOENT: + if (mkdir(dir, S_IRWXU)) + { + fprintf(stderr, "%s: mkdir(2) %s: %s\n", + __func__, dir, strerror(errno)); + return -1; + } + + printf("Created empty directory at %s\n", dir); + break; + + default: + fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); + return -1; + } + } + else if (!S_ISDIR(sb.st_mode)) + { + fprintf(stderr, "%s: %s not a directory\n", __func__, dir); + return -1; + } + + return 0; +} + static int upload_file(const struct http_post_file *const f, const char *const user, const char *const root, const char *const dir) { int ret = -1; - struct dynstr d; + struct dynstr d, dird, th; dynstr_init(&d); + dynstr_init(&dird); + dynstr_init(&th); if (!root) { fprintf(stderr, "%s: auth_dir failed\n", __func__); goto end; } - else if (dynstr_append(&d, "%s/user/%s/%s%s", root, user, dir, f->filename)) + else if (dynstr_append(&d, "%s/user/%s%s%s", root, user, dir, f->filename)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } - else if (rename_or_move(f->tmpname, d.str)) + else if (thumbnail_configured()) + { + if (dynstr_append(&dird, "%s/thumbnails/%s%s", root, user, dir)) + { + fprintf(stderr, "%s: dynstr_append dird failed\n", __func__); + goto end; + } + else if (dynstr_append(&th, "%s%s", dird.str, f->filename)) + { + fprintf(stderr, "%s: dynstr_append th failed\n", __func__); + goto end; + } + else if (mkdir_r(dird.str, S_IRWXU)) + { + fprintf(stderr, "%s: ensure_dir failed\n", __func__); + goto end; + } + else if (thumbnail_create(f->tmpname, th.str) < 0) + { + fprintf(stderr, "%s: thumbnail_create failed\n", __func__); + goto end; + } + } + + if (rename_or_move(f->tmpname, d.str)) { fprintf(stderr, "%s: rename_or_move failed\n", __func__); goto end; @@ -825,6 +880,8 @@ static int upload_file(const struct http_post_file *const f, end: dynstr_free(&d); + dynstr_free(&dird); + dynstr_free(&th); return ret; } @@ -1129,47 +1186,74 @@ static int parse_args(const int argc, char *const argv[], return 0; } -static int ensure_dir(const char *const dir) +static int getthumbnail(const struct http_payload *const p, + struct http_response *const r, void *const user) { - struct stat sb; + int ret = -1; + struct auth *const a = user; + struct dynstr d, dir; + char *name; - if (stat(dir, &sb)) + dynstr_init(&d); + dynstr_init(&dir); + + if (auth_cookie(a, &p->cookie)) { - switch (errno) - { - case ENOENT: - if (mkdir(dir, S_IRWXU)) - { - fprintf(stderr, "%s: mkdir(2) %s: %s\n", - __func__, dir, strerror(errno)); - return -1; - } - - printf("Created empty directory at %s\n", dir); - break; - - default: - fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); - return -1; - } - } - else if (!S_ISDIR(sb.st_mode)) - { - fprintf(stderr, "%s: %s not a directory\n", __func__, dir); - return -1; + fprintf(stderr, "%s: auth_cookie failed\n", __func__); + ret = page_forbidden(r); + goto end; } - return 0; + const char *const adir = auth_dir(a); + + if (!adir) + { + fprintf(stderr, "%s: auth_dir failed\n", __func__); + goto end; + } + else if (dynstr_append(&d, "%s%s", adir, p->resource)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (dynstr_dup(&dir, &d)) + { + fprintf(stderr, "%s: dynstr_dup failed\n", __func__); + goto end; + } + else if (!(name = dirname(dir.str))) + { + fprintf(stderr, "%s: dirname(3) failed\n", __func__); + goto end; + } + else if (ensure_dir(name)) + { + fprintf(stderr, "%s: ensure_dir failed\n", __func__); + goto end; + } + else if (page_thumbnail(r, d.str)) + { + fprintf(stderr, "%s: page_public failed\n", __func__); + goto end; + } + + ret = 0; + +end: + dynstr_free(&d); + dynstr_free(&dir); + return ret; } static int init_dirs(const char *const dir) { int ret = -1; - struct dynstr user, public; + struct dynstr user, public, thumbnails; struct sb; dynstr_init(&user); dynstr_init(&public); + dynstr_init(&thumbnails); if (dynstr_append(&user, "%s/user", dir)) { @@ -1196,12 +1280,26 @@ static int init_dirs(const char *const dir) fprintf(stderr, "%s: ensure_dir public failed\n", __func__); goto end; } + if (thumbnail_configured()) + { + if (dynstr_append(&thumbnails, "%s/thumbnails", dir)) + { + fprintf(stderr, "%s: dynstr_append thumbnails failed\n", __func__); + goto end; + } + else if (ensure_dir(thumbnails.str)) + { + fprintf(stderr, "%s: ensure_dir thumbnails failed\n", __func__); + goto end; + } + } ret = 0; end: dynstr_free(&user); dynstr_free(&public); + dynstr_free(&thumbnails); return ret; } @@ -1237,6 +1335,8 @@ int main(const int argc, char *const argv[]) || handler_add(h, "/share", HTTP_OP_POST, share, a) || handler_add(h, "/upload", HTTP_OP_POST, upload, a) || handler_add(h, "/mkdir", HTTP_OP_POST, createdir, a) + || (thumbnail_configured() + && handler_add(h, "/thumbnails/*", HTTP_OP_GET, getthumbnail, a)) || handler_listen(h, port)) goto end; diff --git a/page.c b/page.c index 9776349..970a5bf 100644 --- a/page.c +++ b/page.c @@ -3,6 +3,7 @@ #include "page.h" #include "http.h" #include "html.h" +#include "thumbnail.h" #include #include #include @@ -45,7 +46,102 @@ " \n" #define MAXSIZEFMT "18446744073709551615.0 XiB" -static int prepare_name(struct html_node *const n, struct stat *const sb, +static const char *dir_separator(const char *const dir) +{ + return dir[strlen(dir) - 1] != '/' ? "/" : ""; +} + +static int prepare_thumbnail(struct html_node *const n, + const struct stat *const sb, + const struct page_resource *const r, const char *const name) +{ + int ret = -1, res; + const char *const sep = dir_separator(r->res); + struct html_node *img; + struct dynstr abs, rel; + struct stat tsb; + struct thumbnail_dim dim; + char w[sizeof "18446744073709551615"], h[sizeof w]; + + dynstr_init(&abs); + dynstr_init(&rel); + + if (!S_ISREG(sb->st_mode)) + { + ret = 0; + goto end; + } + else if (dynstr_append(&rel, "/thumbnails/%s%s%s%s", + r->username, r->res, sep, name)) + { + fprintf(stderr, "%s: dynstr_append rel failed\n", __func__); + goto end; + } + else if (dynstr_append(&abs, "%s%s", r->root, rel.str)) + { + fprintf(stderr, "%s: dynstr_append rel failed\n", __func__); + goto end; + } + else if (stat(abs.str, &tsb)) + { + if (errno == ENOENT) + ret = 0; + else + fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); + + goto end; + } + else if (thumbnail_dim(abs.str, &dim)) + { + fprintf(stderr, "%s: thumbnail_dim failed\n", __func__); + goto end; + } + + res = snprintf(w, sizeof w, "%lu", dim.w); + + if (res < 0 || res >= sizeof w) + { + fprintf(stderr, "%s: snprintf(3) w failed with %d\n", __func__, res); + goto end; + } + + res = snprintf(h, sizeof h, "%lu", dim.h); + + if (res < 0 || res >= sizeof h) + { + fprintf(stderr, "%s: snprintf(3) h failed with %d\n", __func__, res); + goto end; + } + else if (!(img = html_node_add_child(n, "img"))) + { + fprintf(stderr, "%s: html_node_add_child failed\n", __func__); + goto end; + } + else if (html_node_add_attr(img, "src", rel.str)) + { + fprintf(stderr, "%s: html_node_add_attr src failed\n", __func__); + goto end; + } + else if (html_node_add_attr(img, "width", w)) + { + fprintf(stderr, "%s: html_node_add_attr w failed\n", __func__); + goto end; + } + else if (html_node_add_attr(img, "height", h)) + { + fprintf(stderr, "%s: html_node_add_attr h failed\n", __func__); + goto end; + } + + ret = 0; + +end: + dynstr_free(&abs); + dynstr_free(&rel); + return ret; +} + +static int prepare_name(struct html_node *const n, const struct stat *const sb, const char *const dir, const char *const name) { int ret = -1; @@ -169,7 +265,7 @@ static int prepare_share(struct html_node *const n, const struct stat *const sb, const char *const dir, const char *const name) { int ret = -1; - const char *const fdir = dir + strlen("/user"); + const char *const sep = dir_separator(dir); struct html_node *form, *file, *submit; struct dynstr d; @@ -213,7 +309,7 @@ static int prepare_share(struct html_node *const n, fprintf(stderr, "%s: html_node_add_attr file name failed\n", __func__); goto end; } - else if (dynstr_append(&d, "%s%s", fdir, name)) + else if (dynstr_append(&d, "%s%s%s", dir, sep, name)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; @@ -243,18 +339,37 @@ end: return ret; } -static int add_element(struct html_node *const n, const char *const dir, - const char *const res, const char *const name) +static int user_resource(const struct page_resource *const r, + struct dynstr *const d) +{ + if (dynstr_append(d, "%s/user/%s%s", r->root, r->username, r->res)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + return -1; + } + + return 0; +} + +static int add_element(const struct page_resource *const r, + struct html_node *const n, const char *const absres, + const char *const name) { int ret = -1; - enum {NAME, SIZE, DATE, SHARE, COLUMNS}; + enum {THUMBNAIL, NAME, SIZE, DATE, SHARE, COLUMNS}; + const char *const sep = dir_separator(r->res); struct html_node *tr, *td[COLUMNS]; - struct dynstr path; - const char *const sep = res[strlen(res) - 1] != '/' ? "/" : ""; + struct dynstr path, rel; dynstr_init(&path); + dynstr_init(&rel); - if (dynstr_append(&path, "%s%s%s", res, sep, name)) + if (dynstr_append(&path, "%s%s%s", absres, sep, name)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (dynstr_append(&rel, "/user%s%s", r->res, sep)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; @@ -266,7 +381,9 @@ static int add_element(struct html_node *const n, const char *const dir, } for (size_t i = 0; i < sizeof td / sizeof *td; i++) - if (!(td[i] = html_node_add_child(tr, "td"))) + if (i == THUMBNAIL && !thumbnail_configured()) + continue; + else if (!(td[i] = html_node_add_child(tr, "td"))) { fprintf(stderr, "%s: html_node_add_child td[%zu] failed\n", __func__, i); @@ -281,7 +398,13 @@ static int add_element(struct html_node *const n, const char *const dir, __func__, path.str, strerror(errno)); goto end; } - else if (prepare_name(td[NAME], &sb, dir, name)) + else if (thumbnail_configured() + && prepare_thumbnail(td[THUMBNAIL], &sb, r, name)) + { + fprintf(stderr, "%s: prepare_thumbnail failed\n", __func__); + goto end; + } + else if (prepare_name(td[NAME], &sb, rel.str, name)) { fprintf(stderr, "%s: prepare_name failed\n", __func__); goto end; @@ -296,7 +419,7 @@ static int add_element(struct html_node *const n, const char *const dir, fprintf(stderr, "%s: prepare_date failed\n", __func__); goto end; } - else if (prepare_share(td[SHARE], &sb, dir, name)) + else if (prepare_share(td[SHARE], &sb, rel.str, name)) { fprintf(stderr, "%s: prepare_date failed\n", __func__); goto end; @@ -306,6 +429,7 @@ static int add_element(struct html_node *const n, const char *const dir, end: dynstr_free(&path); + dynstr_free(&rel); return ret; } @@ -743,7 +867,6 @@ end: static struct html_node *resource_layout(const char *const dir, const struct page_quota *const q, struct html_node **const table) { - const char *const fdir = dir + strlen("/user"); struct html_node *const html = html_node_alloc("html"), *ret = NULL, *head, *body; @@ -767,17 +890,17 @@ static struct html_node *resource_layout(const char *const dir, fprintf(stderr, "%s: html_node_add_child table failed\n", __func__); goto end; } - else if (common_head(head, fdir)) + else if (common_head(head, dir)) { fprintf(stderr, "%s: common_head failed\n", __func__); goto end; } - else if (prepare_upload_form(body, fdir)) + else if (prepare_upload_form(body, dir)) { fprintf(stderr, "%s: prepare_upload_form failed\n", __func__); goto end; } - else if (prepare_mkdir_form(body, fdir)) + else if (prepare_mkdir_form(body, dir)) { fprintf(stderr, "%s: prepare_upload_form failed\n", __func__); goto end; @@ -808,14 +931,21 @@ end: return ret; } -static int add_elements(const char *const root, const char *const res, - const char *const dir, struct html_node *const table) +static int add_elements(const struct page_resource *const r, + struct html_node *const table) { - int ret = -1; + int ret = -1, n = 0; + struct dynstr d; struct dirent **pde = NULL; - const int n = scandir(res, &pde, NULL, alphasort); - if (n < 0) + dynstr_init(&d); + + if (user_resource(r, &d)) + { + fprintf(stderr, "%s: user_resource failed\n", __func__); + goto end; + } + else if ((n = scandir(d.str, &pde, NULL, alphasort)) < 0) { fprintf(stderr, "%s: scandir(3): %s\n", __func__, strerror(errno)); goto end; @@ -827,9 +957,9 @@ static int add_elements(const char *const root, const char *const res, const char *const name = de->d_name; if (!strcmp(name, ".") - || (!strcmp(name, "..") && !strcmp(root, res))) + || (!strcmp(name, "..") && !strcmp(r->user_root, d.str))) continue; - else if (add_element(table, dir, res, name)) + else if (add_element(r, table, d.str, name)) { fprintf(stderr, "%s: add_element failed\n", __func__); goto end; @@ -844,25 +974,25 @@ end: free(pde[i]); free(pde); + dynstr_free(&d); return ret; } -static int list_dir(struct http_response *const r, const char *const dir, - const char *const root, const char *const res, - const struct page_quota *const q) +static int list_dir(struct http_response *const r, + const struct page_resource *const res, const struct page_quota *const q) { int ret = -1; struct dynstr out; - struct html_node *table, *const html = resource_layout(dir, q, &table); + struct html_node *table, *html = NULL; dynstr_init(&out); - if (!html) + if (!(html = resource_layout(res->res, q, &table))) { fprintf(stderr, "%s: resource_layout failed\n", __func__); goto end; } - else if (add_elements(root, res, dir, table)) + else if (add_elements(res, table)) { fprintf(stderr, "%s: read_elements failed\n", __func__); goto end; @@ -907,14 +1037,14 @@ static int serve_file(struct http_response *const r, const struct stat *const sb, const char *const res) { int ret = -1; - FILE *const f = fopen(res, "rb"); + FILE *f = NULL; struct dynstr b, d; char *bn; dynstr_init(&b); dynstr_init(&d); - if (!f) + if (!(f = fopen(res, "rb"))) { fprintf(stderr, "%s: fopen(3): %s\n", __func__, strerror(errno)); goto end; @@ -987,32 +1117,43 @@ static int page_not_found(struct http_response *const r) return 0; } -int page_resource(struct http_response *const r, const char *const dir, - const char *const root, const char *const res, - const struct page_quota *const q) +int page_resource(struct http_response *const r, + const struct page_resource *const res, const struct page_quota *const q) { + int ret = -1; struct stat sb; + struct dynstr d; - if (stat(res, &sb)) + dynstr_init(&d); + + if (user_resource(res, &d)) + { + fprintf(stderr, "%s: user_resource failed\n", __func__); + goto end; + } + else if (stat(d.str, &sb)) { fprintf(stderr, "%s: stat(2) %s: %s\n", - __func__, res, strerror(errno)); + __func__, res->res, strerror(errno)); if (errno == ENOENT) - return page_not_found(r); - else - return -1; + ret = page_not_found(r); + + goto end; } const mode_t m = sb.st_mode; if (S_ISDIR(m)) - return list_dir(r, dir, root, res, q); + ret = list_dir(r, res, q); else if (S_ISREG(m)) - return serve_file(r, &sb, res); + ret = serve_file(r, &sb, d.str); + else + fprintf(stderr, "%s: unexpected st_mode %jd\n", __func__, (intmax_t)m); - fprintf(stderr, "%s: unexpected st_mode %jd\n", __func__, (intmax_t)m); - return -1; +end: + dynstr_free(&d); + return ret; } static char *resolve_link(const char *const res) @@ -1403,3 +1544,30 @@ end: return ret; } + +int page_thumbnail(struct http_response *const r, const char *const res) +{ + struct stat sb; + + if (stat(res, &sb)) + { + if (errno == ENOENT) + return page_not_found(r); + else + { + fprintf(stderr, "%s: stat(2) %s: %s\n", + __func__, res, strerror(errno)); + return -1; + } + } + + const mode_t m = sb.st_mode; + + if (!S_ISREG(m)) + { + fprintf(stderr, "%s: only regular files are supported\n", __func__); + return 1; + } + + return serve_file(r, &sb, res); +} diff --git a/page.h b/page.h index 094a21e..3b09c75 100644 --- a/page.h +++ b/page.h @@ -8,16 +8,22 @@ struct page_quota unsigned long long cur, max; }; +struct page_resource +{ + const char *root, *user_root, *username, *res; +}; + int page_login(struct http_response *r); int page_style(struct http_response *r); int page_failed_login(struct http_response *r); int page_forbidden(struct http_response *r); int page_bad_request(struct http_response *r); -int page_resource(struct http_response *r, const char *dir, const char *root, - const char *res, const struct page_quota *q); +int page_resource(struct http_response *r, const struct page_resource *res, + const struct page_quota *q); int page_public(struct http_response *r, const char *res); int page_share(struct http_response *r, const char *path); int page_quota_exceeded(struct http_response *r, unsigned long long len, unsigned long long quota); +int page_thumbnail(struct http_response *r, const char *res); #endif /* PAGE_H */ diff --git a/thumbnail.c b/thumbnail.c new file mode 100644 index 0000000..9e425cb --- /dev/null +++ b/thumbnail.c @@ -0,0 +1,143 @@ +#include "thumbnail.h" +#include +#include +#include +#include +#include + +/* http://www.graphicsmagick.org/api/api.html */ + +int thumbnail_create(const char *const src, const char *const dst) +{ + int ret = -1; + const size_t slen = strlen(src), dlen = strlen(dst); + ImageInfo *info = NULL; + Image *i = NULL, *t = NULL; + ExceptionInfo exc; + + InitializeMagick(NULL); + GetExceptionInfo(&exc); + + 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))) + { + if (exc.severity == MissingDelegateError) + /* Non-image file format. */ + ret = 1; + else + fprintf(stderr, "%s: ReadImage failed: " + "reason: %s, description: %s\n", + __func__, exc.reason, exc.description); + + goto end; + } + + const unsigned long y = i->rows > THUMBNAIL_HEIGHT ? + THUMBNAIL_HEIGHT : i->rows, + x = y * i->columns / i->rows; + + if (!(t = ResizeImage(i, x, y, PointFilter, 1, &exc))) + { + fprintf(stderr, "%s: ResizeImage failed, " + "reason: %s, description: %s\n", + __func__, exc.reason, exc.description); + goto end; + } + else if (WriteImages(info, t, dst, &exc) != MagickPass) + { + fprintf(stderr, "%s: WriteImages failed, " + "reason: %s, description: %s\n", + __func__, exc.reason, exc.description); + goto end; + } + + ret = 0; + +end: + DestroyImageInfo(info); + DestroyImage(i); + DestroyImage(t); + DestroyExceptionInfo(&exc); + DestroyMagick(); + return ret; +} + +bool thumbnail_configured(void) +{ + return true; +} + +int thumbnail_dim(const char *const path, struct thumbnail_dim *const d) +{ + int ret = -1; + const size_t slen = strlen(path); + ImageInfo *info = NULL; + Image *i = NULL; + ExceptionInfo exc; + + InitializeMagick(NULL); + GetExceptionInfo(&exc); + + 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 (!(info = CloneImageInfo(NULL))) + { + fprintf(stderr, "%s: CloneImageInfo failed\n", __func__); + goto end; + } + + strcpy(info->filename, path); + info->adjoin = MagickTrue; + + if (!(i = ReadImage(info, &exc))) + { + if (exc.severity == MissingDelegateError) + /* Non-image file format. */ + ret = 1; + else + fprintf(stderr, "%s: ReadImage failed: " + "reason: %s, description: %s\n", + __func__, exc.reason, exc.description); + + goto end; + } + + *d = (const struct thumbnail_dim) + { + .w = i->columns, + .h = i->rows + }; + + ret = 0; + +end: + DestroyImageInfo(info); + DestroyImage(i); + DestroyExceptionInfo(&exc); + DestroyMagick(); + return ret; +} diff --git a/thumbnail.h b/thumbnail.h new file mode 100644 index 0000000..f4cfc58 --- /dev/null +++ b/thumbnail.h @@ -0,0 +1,15 @@ +#ifndef THUMBNAIL_H +#define THUMBNAIL_H + +#include + +struct thumbnail_dim +{ + unsigned long w, h; +}; + +bool thumbnail_configured(void); +int thumbnail_create(const char *src, const char *dst); +int thumbnail_dim(const char *path, struct thumbnail_dim *d); + +#endif /* THUMBNAIL_H */ diff --git a/thumbnail_stub.c b/thumbnail_stub.c new file mode 100644 index 0000000..6319942 --- /dev/null +++ b/thumbnail_stub.c @@ -0,0 +1,17 @@ +#include "thumbnail.h" +#include + +int thumbnail_create(const char *const src, const char *const dst) +{ + return -1; +} + +bool thumbnail_configured(void) +{ + return false; +} + +int thumbnail_dim(const char *const path, struct thumbnail_dim *const d) +{ + return -1; +} diff --git a/usergen b/usergen index 42c18fe..735196a 100755 --- a/usergen +++ b/usergen @@ -64,3 +64,4 @@ jq ".users += [ mv $TMP "$DB" mkdir "$DIR/user/$USER" +test -d "$DIR/thumbnails" && mkdir -p "$DIR/thumbnails/$USER"