aboutsummaryrefslogtreecommitdiff
path: root/zip.c
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-10-06 23:02:51 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2025-10-08 02:03:05 +0200
commit00dd37604d50cbf3fb27ec0631b4d4b6d2ee893a (patch)
tree81f9546b168078aa9bf54d4298aa76e99bb229af /zip.c
parent4ab3ee681607f0cc75cf56e4fcbeae85594bb630 (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.c356
1 files changed, 356 insertions, 0 deletions
diff --git a/zip.c b/zip.c
new file mode 100644
index 0000000..704bf36
--- /dev/null
+++ b/zip.c
@@ -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;
+}