diff options
| author | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-06 23:02:51 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-08 02:03:05 +0200 |
| commit | 00dd37604d50cbf3fb27ec0631b4d4b6d2ee893a (patch) | |
| tree | 81f9546b168078aa9bf54d4298aa76e99bb229af /zip.c | |
| parent | 4ab3ee681607f0cc75cf56e4fcbeae85594bb630 (diff) | |
Implement directory download as ZIP
Thanks to the fdzipstream library [1] and zlib [2], it is possible to
generate ZIP files on-the-fly, therefore requiring no extra disk space
usage and only a small amount of memory.
Unfortunately, as of the time of this writing fdzipstream is not
packaged by any distributions yet [3], so it had to be imported as a git
submodule as a workaround.
While libarchive [4] could be an interesting alternative, writing ZIP
files is only supported by very recent versions (>= 3.8.0), which are
still not packaged by many distributions [5], either.
Moreover, libarchive is a package with several dependencies other than
zlib and is significantly larger compared to fdzipstreams, so
fdzipstreams was ultimately considered a better fit for this purpose.
[1]: https://github.com/CTrabant/fdzipstream.git
[2]: http://zlib.net/
[3]: https://repology.org/projects/?search=fdzipstream
[4]: https://www.libarchive.org/
[5]: https://repology.org/project/libarchive/versions
Diffstat (limited to 'zip.c')
| -rw-r--r-- | zip.c | 356 |
1 files changed, 356 insertions, 0 deletions
@@ -0,0 +1,356 @@ +#define _POSIX_C_SOURCE 200809L + +#include "zip.h" +#include "cftw.h" +#include <dynstr.h> +#include <libweb/http.h> +#include <fdzipstream.h> +#include <fcntl.h> +#include <libgen.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +struct zip +{ + bool finished; + char *basedir; + struct cftw *cftw; + ZIPstream *stream; + ZIPentry *entry; + int fds[2], fd; + off_t filesz, read, lread; + int (*next)(void *, size_t, bool *, void *, struct zip *); +}; + +static int recurse(void *, size_t, bool *, void *, struct zip *); +static int read_file(void *, size_t, bool *, void *, struct zip *); + +static void free_zip(void *const p) +{ + struct zip *const z = p; + + if (!z) + return; + else if (z->entry) + zs_entryend(z->stream, z->entry, NULL); + + zs_free(z->stream); + + if ((z->fds[0] >= 0 && close(z->fds[0])) + || (z->fds[1] >= 0 && close(z->fds[1])) + || (z->fd >= 0 && close(z->fd))) + fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); + + cftw_free(z->cftw); + free(z->basedir); + free(z); +} + +static int dump_final(void *const buf, const size_t n, bool *const done, + void *const user, struct zip *const z) +{ + const ssize_t r = read(z->fds[0], buf, z->lread); + + if (r < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + free_zip(z); + *done = true; + return 0; + } + else + fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); + } + + return r; +} + +static int dump_end(void *const buf, const size_t n, bool *const done, + void *const user, struct zip *const z) +{ + const ssize_t r = read(z->fds[0], buf, z->lread); + + if (r < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + if (close(z->fd)) + { + fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); + return -1; + } + + z->fd = -1; + z->next = recurse; + return 0; + } + } + + return r; +} + +static int dumpz(void *const buf, const size_t n, bool *const done, + void *const user, struct zip *const z) +{ + const ssize_t r = read(z->fds[0], buf, z->lread); + + if (r < 0) + { + if (errno != EAGAIN && errno != EWOULDBLOCK) + { + fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); + return -1; + } + else if (z->read == z->filesz) + { + if (!zs_entryend(z->stream, z->entry, NULL)) + { + fprintf(stderr, "%s: zs_entryend failed\n", __func__); + return -1; + } + + z->entry = NULL; + z->next = dump_end; + } + else + z->next = read_file; + + return 0; + } + + return r; +} + +static int read_file(void *const buf, const size_t n, bool *const done, + void *const user, struct zip *const z) +{ + const off_t pend = z->filesz - z->read; + size_t rem = pend > n ? n : pend; + ssize_t r; + + if ((r = read(z->fd, buf, rem)) < 0) + { + fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); + return -1; + } + else if (!zs_entrydata(z->stream, z->entry, buf, r, NULL)) + { + fprintf(stderr, "%s: zs_entrydata failed\n", __func__); + return -1; + } + + z->lread = r; + z->read += r; + z->next = dumpz; + return 0; +} + +static int setup_file(struct zip *const z, const char *const fpath, + const time_t t) +{ + struct stat sb; + ZIPentry *entry = NULL; + const int fd = open(fpath, O_RDONLY | O_NONBLOCK); + /* zs_entrybegin is not const-correct. */ + char *path = (char *)fpath + strlen(z->basedir); + + if (fd < 0) + { + fprintf(stderr, "%s: open(2) %s: %s\n", __func__, fpath, + strerror(errno)); + goto failure; + } + else if (fstat(fd, &sb)) + { + fprintf(stderr, "%s: fstat(2) %s: %s\n", __func__, fpath, + strerror(errno)); + goto failure; + } + else if (!(entry = zs_entrybegin(z->stream, path, t, ZS_DEFLATE, + NULL))) + { + fprintf(stderr, "%s: zs_entrybegin failed\n", __func__); + goto failure; + } + + z->fd = fd; + z->read = 0; + z->entry = entry; + z->filesz = sb.st_size; + z->next = read_file; + return 0; + +failure: + + if (fd && close(fd)) + fprintf(stderr, "%s: close(2) %s: %s\n", __func__, fpath, + strerror(errno)); + + return -1; +} + +static int setup(const char *const fpath, const struct stat *sb, + bool *const done, void *const user) +{ + struct zip *const z = user; + + if (S_ISREG(sb->st_mode) && setup_file(z, fpath, sb->st_ctim.tv_sec)) + { + fprintf(stderr, "%s: setup_file failed\n", __func__); + return -1; + } + + return 0; +} + +static int finalize(struct zip *const z) +{ + if (zs_finish(z->stream, NULL)) + { + fprintf(stderr, "%s: zs_finish failed\n", __func__); + return -1; + } + + z->finished = true; + z->next = dump_final; + return 0; +} + +static int recurse(void *const buf, const size_t n, bool *const done, + void *const user, struct zip *const z) +{ + switch (cftw_step(z->cftw)) + { + case CFTW_AGAIN: + break; + + case CFTW_FATAL: + fprintf(stderr, "%s: cftw_step failed\n", __func__); + return -1; + + case CFTW_OK: + if (finalize(z)) + { + fprintf(stderr, "%s: finalize end\n", __func__); + return -1; + } + + break; + } + + return 0; +} + +static int step(void *const buf, const size_t n, bool *const done, + void *const user, void *const args) +{ + struct zip *const z = args; + const int ret = z->next(buf, n, done, user, z); + + if (ret < 0) + free_zip(z); + + return ret; +} + +int zip(const char *const dir, struct http_response *const r) +{ + struct cftw *c = NULL; + ZIPstream *stream = NULL; + struct zip *z = NULL; + char *basedir = NULL, *bndup = NULL, *bn = NULL; + int fds[2] = {-1, -1}, flags; + struct dynstr d; + + dynstr_init(&d); + + if (pipe(fds)) + { + fprintf(stderr, "%s: pipe(2): %s\n", __func__, strerror(errno)); + goto failure; + } + else if ((flags = fcntl(fds[0], F_GETFL)) == -1 + || fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) == -1) + { + fprintf(stderr, "%s: fcntl(2): %s\n", __func__, strerror(errno)); + goto failure; + } + else if (!(z = malloc(sizeof *z))) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto failure; + } + else if (!(c = cftw(dir, setup, z))) + { + fprintf(stderr, "%s: cftw failed\n", __func__); + goto failure; + } + else if (!(basedir = strdup(dir)) + || !(bndup = strdup(dir))) + { + fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno)); + goto failure; + } + else if (!(bn = basename(bndup))) + { + fprintf(stderr, "%s: basename(3) failed\n", __func__); + goto failure; + } + else if (!(stream = zs_init(fds[1], NULL))) + { + fprintf(stderr, "%s: zs_init failed\n", __func__); + goto failure; + } + else if (dynstr_append(&d, "attachment; filename=\"%s.zip\"", bn)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto failure; + } + + *z = (const struct zip) + { + .fds = {[0] = fds[0], [1] = fds[1]}, + .basedir = basedir, + .stream = stream, + .cftw = c, + .next = recurse + }; + + *r = (const struct http_response) + { + .status = HTTP_STATUS_OK, + .chunk = step, + .args = z + }; + + if (http_response_add_header(r, "Content-Type", "application/zip") + || http_response_add_header(r, "Content-Disposition", d.str)) + { + fprintf(stderr, "%s: http_response_add_header failed\n", __func__); + goto failure; + } + + dynstr_free(&d); + free(bndup); + return 0; + +failure: + + if ((fds[0] >= 0 && close(fds[0])) + || (fds[1] >= 0 && close(fds[1]))) + fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); + + dynstr_free(&d); + zs_free(stream); + cftw_free(c); + free(basedir); + free(bndup); + free(z); + return -1; +} |
