diff options
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | browse.c | 15 | ||||
-rw-r--r-- | delete.c | 318 | ||||
-rw-r--r-- | delete.h | 54 | ||||
-rw-r--r-- | file.c | 20 | ||||
-rw-r--r-- | file.h | 6 | ||||
-rw-r--r-- | main.c | 8 | ||||
-rw-r--r-- | str.c | 75 | ||||
-rw-r--r-- | str.h | 18 | ||||
-rw-r--r-- | template/browse_head.html | 1 | ||||
-rw-r--r-- | template/browse_item.html | 1 | ||||
-rw-r--r-- | template/delete_head.html | 14 | ||||
-rw-r--r-- | template/head.html | 7 | ||||
-rw-r--r-- | test.c | 39 | ||||
-rw-r--r-- | upload.h | 2 |
15 files changed, 565 insertions, 18 deletions
@@ -47,10 +47,10 @@ all: main .c.o: $(CC) -o $@ -c $< $(CFLAGS) -main: str.o config.o mime.o url.o template.o file.o http.o upload.o download.o browse.o main.o +main: str.o config.o mime.o url.o template.o file.o http.o delete.o upload.o download.o browse.o main.o $(CC) -o $@ $> $(LDFLAGS) -test: str.o config.o mime.o url.o template.o file.o http.o upload.o download.o browse.o test.o +test: str.o config.o mime.o url.o template.o file.o http.o delete.o upload.o download.o browse.o test.o $(CC) -o $@ $> $(LDFLAGS) env: env.o @@ -67,6 +67,7 @@ install: install -D -o www -g www -m 0440 template/browse_foot.html /var/www/usr/share/vault/template/browse_foot.html install -D -o www -g www -m 0440 template/browse_item.html /var/www/usr/share/vault/template/browse_item.html install -D -o www -g www -m 0440 template/upload_head.html /var/www/usr/share/vault/template/upload_head.html + install -D -o www -g www -m 0440 template/delete_head.html /var/www/usr/share/vault/template/delete_head.html install -D -o www -g www -m 0440 fontawesome-6.5.1/css/fontawesome.css /var/www/vault-static/fontawesome-6.5.1/css/fontawesome.css gzip -fk /var/www/vault-static/fontawesome-6.5.1/css/fontawesome.css install -D -o www -g www -m 0440 fontawesome-6.5.1/css/solid.css /var/www/vault-static/fontawesome-6.5.1/css/solid.css @@ -197,7 +197,8 @@ header_template_callback(size_t index, void *arg) return 1; } -static const char *const file_template_keys[] = {"icon", "name", "url", "size"}; +static const char *const file_template_keys[] = {"icon", "name", "url", "size", +"delete_url"}; static int file_template_callback(size_t index, void *arg) @@ -253,6 +254,16 @@ file_template_callback(size_t index, void *arg) size_read[i - 1]), r); } break; + case 4: + /* delete url */ + if (build_delete_url(r, data->f) == 0) { + kutil_warnx(r, NULL, + "unable to build delete url for file %s", + data->f->path); + break; + } + K_OK(khtml_puts(html, data->f->action_url), r); + break; default: kutil_warnx(r, NULL, "Invalid key index for browse file template: %zd", @@ -352,7 +363,7 @@ browse(struct kreq * r) /* print file list */ template.key = file_template_keys; - template.keysz = 4; + template.keysz = 5; template.cb = file_template_callback; SLIST_FOREACH(file, &list, files) { diff --git a/delete.c b/delete.c new file mode 100644 index 0000000..29c5cb8 --- /dev/null +++ b/delete.c @@ -0,0 +1,318 @@ +/* + * Copyright 2024, Vincent Douillet <vincent@vdouillet.fr> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include <sys/types.h> +#include <sys/stat.h> +#include <fts.h> +#include <kcgi.h> +#include <kcgihtml.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "browse.h" +#include "cgi.h" +#include "delete.h" +#include "http.h" +#include "str.h" +#include "template.h" +#include "url.h" + +/* + * delete url = r->pname / DELETE_URL / file->path + */ +size_t +build_delete_url(const struct kreq * r, struct file * file) +{ + char action_url[PATH_MAX]; + size_t action_url_len; + + action_url_len = url_build(action_url, PATH_MAX, r->pname, DELETE_URL, + file->path, NULL); + if (action_url_len == 0 || action_url_len >= PATH_MAX) { + kutil_warn(r, NULL, + "delete: action URL overflow: %s", action_url); + return 0; + } + file->action_url = strndup(action_url, action_url_len); + if (file->action_url == NULL) { + kutil_warn(r, NULL, + "delete: unable to allocate file url buffer: %s", + action_url); + return 0; + } + return action_url_len; +} + +/* + * data required in the template functions + */ +struct template_data { + struct kreq *r; + struct khtmlreq *html; + struct file *f; +}; + +static const char *const header_template_keys[] = {"path", "submit_url"}; + +static int +header_template_callback(size_t index, void *arg) +{ + struct kreq *r; + struct khtmlreq *html; + struct template_data *data; + + if (arg == NULL) { + kutil_warn(NULL, NULL, + "Invalid data for delete header template"); + return 0; + } + data = arg; + r = data->r; + html = data->html; + + switch (index) { + case 0: + /* path */ + K_OK(khtml_puts(html, data->f->path), r); + break; + case 1: + /* submit_url */ + K_OK(khtml_puts(html, data->f->action_url), r); + break; + default: + kutil_warnx(r, NULL, + "Invalid key index for delete header template: %zd", + index); + return 0; + } + + return 1; +} + +/* + * GET request, we print delete form + */ +void +delete_get(struct kreq * r, struct http_ret * ret, struct file * file) +{ + struct khtmlreq html; + struct ktemplate template; + struct page_template *tmpl; + struct template_data data; + + tmpl = NULL; + + /* action url is form submit url */ + if (build_delete_url(r, file) == 0) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: Can't build delete url" + }; + goto end; + } + /* read template */ + tmpl = page_template_new(DELETE_URL); + if (tmpl == NULL) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: Unable to read template" + }; + goto end; + } + /* print delete form */ + http_open(r, KHTTP_200, r->mime); + + K_OK(khttp_puts(r, tmpl->header), r); + K_OK(khtml_open(&html, r, 0), r); + + template.key = header_template_keys; + template.keysz = 2; + template.cb = header_template_callback; + template.arg = &data; + data.r = r; + data.html = &html; + data.f = file; + K_OK(khttp_template_buf(r, &template, tmpl->page_header, + strlen(tmpl->page_header)), r); + + K_OK(khttp_puts(r, tmpl->page_footer), r); + K_OK(khttp_puts(r, tmpl->footer), r); + + K_OK(khtml_close(&html), r); + +end: + page_template_free(tmpl); +} + +/* + * POST request, we handle delete form + */ +void +delete_post(struct kreq * r, struct http_ret * ret, struct file * f) +{ + char path[PATH_MAX]; + char *paths[2]; + FTS *fts; + FTSENT *ftsent; + int fts_opts; + struct file *parent; + unsigned short fi; + + fts = NULL; + ftsent = NULL; + + /* prepare action url for return redirection in case of success */ + parent = file_get_parent(f); + if (parent == NULL || build_browse_url(r, parent) == 0) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: can't build return url" + }; + goto end; + } + + /* build file path on disk */ + if (file_get_data_path(f, path, sizeof(path), NULL) <= 0) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: can't build file data path" + }; + goto end; + } + + if (f->is_dir) { + /* don't follow symlinks */ + fts_opts = 0x0 | FTS_PHYSICAL; + paths[0] = path; + paths[1] = NULL; + if ((fts = fts_open(paths, fts_opts, NULL)) == NULL) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: can't open file hierarchy" + }; + goto end; + } + /* file and directory delete loop */ + while ((ftsent = fts_read(fts)) != NULL) { + fi = ftsent->fts_info; + + /* halt on fts errors */ + if (fi == FTS_DNR || fi == FTS_ERR || fi == FTS_NS) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: fts_read error" + }; + goto end; + } + /* delete regular files and empty folders */ + else if ((fi == FTS_DP || fi == FTS_F) && + remove(ftsent->fts_path) < 0) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: failed to delete file" + }; + goto end; + } + } + } else { + if (remove(path) < 0) { + *ret = (struct http_ret) { + KHTTP_500, + "delete: failed to delete file" + }; + goto end; + } + } + + /* on success, redirect to the parent directory of deleted files */ + khttp_head(r, kresps[KRESP_LOCATION], "%s", parent->action_url); + http_open(r, KHTTP_303, r->mime); + + *ret = (struct http_ret) { + KHTTP_303, + "" + }; +end: + if (parent != NULL) + file_free(parent); + if (fts != NULL) + fts_close(fts); +} + + +struct http_ret +del(struct kreq * r) +{ + char *path; + size_t suffix_len; + struct file *file; + struct http_ret ret; + + file = NULL; + ret = (struct http_ret) { + KHTTP_200, "" + }; + + /* build file corresponding to request (ie. delete) path */ + suffix_len = strlen(r->suffix); + if (suffix_len > 0) { + if (str_concat(&path, r->path, ".", r->suffix, NULL) <= 0) { + ret = (struct http_ret) { + KHTTP_404, + "delete: Unable to build file path" + }; + goto end; + } + } else { + path = r->path; + } + + file = file_new(path); + if (file == NULL) { + ret = (struct http_ret) { + KHTTP_404, + "delete: Unable to build data file" + }; + goto end; + } + /* print form or handle submission according to HTTP method */ + if (r->method == KMETHOD_POST) + delete_post(r, &ret, file); + else + delete_get(r, &ret, file); + +end: + if (suffix_len > 0) + free(path); + file_free(file); + return ret; +} diff --git a/delete.h b/delete.h new file mode 100644 index 0000000..8d499bb --- /dev/null +++ b/delete.h @@ -0,0 +1,54 @@ +/* + * Copyright 2023, Vincent Douillet <vincent@vdouillet.fr> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef DELETE_H +#define DELETE_H + +#include <kcgi.h> + +#include "file.h" + +#define DELETE_URL "delete" + +/* + * Build the URL to delete a file and assigns it to the file's action_url. + * All parameters are required + * Returns the length of the created URL, or 0 in case of failure. + */ +size_t +build_delete_url(const struct kreq *, struct file *); + +/* + * Print a delete form (http GET) or handle a delete form submission (http + * POST). + */ +struct http_ret del(struct kreq *); + +#endif /* DELETE_H */ @@ -140,8 +140,24 @@ file_get_basename(const struct file * f) if (strrchr(f->path, '/') == NULL) return f->path; - /* split for the basename */ - return str_split(f->path, '/'); + /* get the basename */ + return str_tail(f->path, '/'); +} + +struct file * +file_get_parent(const struct file * f) +{ + char *head; + struct file *parent; + + head = str_head(f->path, '/'); + if (head == NULL) + return file_new(""); + + parent = file_new(head); + + free(head); + return parent; } size_t @@ -70,6 +70,12 @@ char * file_get_basename(const struct file *); /* + * Get the parent file of the given file. Returns NULLL in case of error. + */ +struct file * +file_get_parent(const struct file *); + +/* * Build the data path (on the filesystem) for a file and optionally append * another component to the path. If no additional component is needed, it * should be NULL. @@ -36,6 +36,7 @@ #include "browse.h" #include "cgi.h" #include "config.h" +#include "delete.h" #include "download.h" #include "http.h" #include "upload.h" @@ -44,13 +45,15 @@ enum page { PAGE_BROWSE, PAGE_DOWNLOAD, PAGE_UPLOAD, + PAGE_DELETE, PAGE__MAX }; static const char *const pages[PAGE__MAX] = { BROWSE_URL, DOWNLOAD_URL, - UPLOAD_URL + UPLOAD_URL, + DELETE_URL, }; int @@ -102,6 +105,9 @@ main(void) case PAGE_UPLOAD: ret = upload(&r); break; + case PAGE_DELETE: + ret = del(&r); + break; default: http_exit(&r, KHTTP_404, NULL); } @@ -28,20 +28,43 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#include <stdarg.h> #include <stdlib.h> #include <string.h> char * -str_split(const char *str, char c) +str_head(const char *str, char c) +{ + char *occ, *result; + size_t head_len; + + occ = strrchr(str, c); + if (!occ || (occ == str && *occ != c)) + return NULL; + + /* copy head to new buffer */ + head_len = occ - str + 1; + result = malloc(sizeof(char) * head_len); + if (result == NULL) + return NULL; + if (strlcpy(result, str, head_len) < head_len) { + free(result); + return NULL; + } + return result; +} + +char * +str_tail(const char *str, char c) { char *right, *result; size_t right_len; right = strrchr(str, c); - if (!right || right == str) + if (!right || (right == str && *right != c)) return NULL; - /* move right part to new buffer */ + /* copy right part to new buffer */ right_len = strlen(right); result = malloc(sizeof(char) * right_len); if (result == NULL) @@ -51,9 +74,6 @@ str_split(const char *str, char c) free(result); return NULL; } - /* remove right part from src buffer */ - *right = '\0'; - return result; } @@ -70,3 +90,46 @@ str_replace(char *str, char a, char b) tmp++; } } + +size_t +str_concat(char **dst,...) +{ + va_list str_list; + const char *str; + char *concat; + size_t cur_size, dst_size, w_size; + + /* first compute required size */ + dst_size = 0; + va_start(str_list, dst); + while ((str = va_arg(str_list, char *)) != NULL) { + dst_size += strlen(str); + } + va_end(str_list); + dst_size++; + + /* alloc result string */ + *dst = malloc(sizeof(char) * dst_size); + if (*dst == NULL) + return 0; + + /* concatenate now */ + concat = *dst; + concat[0] = '\0'; + w_size = 0; + va_start(str_list, dst); + while ((str = va_arg(str_list, char *)) != NULL) { + cur_size = strlen(str); + if (strlcpy(concat, str, dst_size) >= dst_size) { + w_size = 0; + free(dst); + break; + } + w_size += cur_size; + concat += cur_size; + dst_size -= cur_size; + } + va_end(str_list); + + return w_size; +} @@ -29,12 +29,24 @@ */ /* - * Returns a newly allocated string that contains the beginning of the - * provided string up to the provided char. + * Returns a newly allocated string that contains the part of the provided + * string before the separator character. */ -char *str_split(const char *, char); +char *str_head(const char *, char); + +/* + * Returns a newly allocated string that contains the part of the provided + * string after the separator character. + */ +char *str_tail(const char *, char); /* * Replaces a char in-place in the provided string. */ void str_replace(char *, char, char); + +/* + * Concatenates the provided NULL-terminated list of strings into the given + * string pointer. Returns the size of the created string or 0 in case of error. + */ +size_t str_concat(char **,...); diff --git a/template/browse_head.html b/template/browse_head.html index 972ed51..6775012 100644 --- a/template/browse_head.html +++ b/template/browse_head.html @@ -7,6 +7,7 @@ <th class="file-icon"></th> <th class="file-name">Name</th> <th class="file-size">Size</th> + <th class="file-delete"></th> </tr> </thead> <tbody> diff --git a/template/browse_item.html b/template/browse_item.html index 4a6bd77..1af8c89 100644 --- a/template/browse_item.html +++ b/template/browse_item.html @@ -2,4 +2,5 @@ <td><i class="fa-regular fa-@@icon@@"></i></td> <td><a href="@@url@@">@@name@@</a></td> <td class="file-size">@@size@@</td> + <td><a href="@@delete_url@@"><i class="fa-regular fa-trash"></i></a></td> </tr> diff --git a/template/delete_head.html b/template/delete_head.html new file mode 100644 index 0000000..50f5426 --- /dev/null +++ b/template/delete_head.html @@ -0,0 +1,14 @@ +<h1><i class="fa-regular fa-folder-open"></i> Delete</h1> +<p>path</p> +<pre>@@path@@</pre> +<hr /> +<div> +<form action="@@submit_url@@" method="post" enctype="multipart/form-data"> + <div> + <p>Deleting content cannot be undone. Proceed anyway?</p> + </div> + <div> + <input type="submit" value="Delete" /> + </div> +</form> +</div> diff --git a/template/head.html b/template/head.html index 6c6ef5b..a36c475 100644 --- a/template/head.html +++ b/template/head.html @@ -24,7 +24,12 @@ tr:not(:last-child) {border-bottom:1px solid grey} th, td {padding:0.2em} th.file-name {text-align:left} th.file-icon {width:1em} -th.file-size {white-space:nowrap;} +th.file-size { + width:2em; + white-space:nowrap; + text-align:right; +} +th.file-delete {width:1em} td.file-size { text-align:right; white-space:nowrap; @@ -2,6 +2,7 @@ #include <string.h> #include "browse.h" +#include "delete.h" #include "http.h" #include "upload.h" #include "url.h" @@ -91,6 +92,42 @@ test_upload_post() } static char * +test_delete_get() +{ + struct kreq r; + struct http_ret ret; + + r = (struct kreq) { + .pname = "/vault", + .path = "a", + .suffix = "txt", + .method = KMETHOD_GET, + }; + ret = del(&r); + + mu_assert("error, GET delete failed!", ret.code <= KHTTP_400); + return 0; +} + +static char * +test_delete_post() +{ + struct kreq r; + struct http_ret ret; + + r = (struct kreq) { + .pname = "/vault", + .path = "Folder", + .suffix = "", + .method = KMETHOD_POST, + }; + ret = del(&r); + + mu_assert("error, POST delete failed!", ret.code <= KHTTP_400); + return 0; +} + +static char * test_url_build() { char dst[BUF_SZ], *expected; @@ -127,6 +164,8 @@ all_tests() mu_run_test(test_browse_path_too_long); mu_run_test(test_url_build); mu_run_test(test_upload_post); + mu_run_test(test_delete_get); + mu_run_test(test_delete_post); return 0; } @@ -38,7 +38,7 @@ #define UPLOAD_URL "upload" /* - * Build the URL to download a file and assigns it to the file's action_url. + * Build the URL to upload a file and assigns it to the file's action_url. * All parameters are required * Returns the length of the created URL, or 0 in case of failure. */ |