Skip to content

Commit 89b55d2

Browse files
authored
Add file cleanup callback for uploaded files (issues #264, #270) (#356)
Add a server-level callback that allows users to customize file cleanup behavior for uploaded files. Previously, all uploaded files were automatically deleted when the request completed, making it impossible to keep or move files to permanent storage. The new file_cleanup_callback option accepts a function with signature: bool(const std::string& key, const std::string& filename, const http::file_info& info) Return true to delete the file (default behavior) or false to keep it. If the callback throws an exception, the file is deleted as a safety measure. When no callback is set, files are deleted (backward compatible). Changes: - Add file_cleanup_callback_ptr typedef and builder method - Store callback in webserver and pass to http_request - Modify http_request destructor to invoke callback per file - Add 5 test cases covering callback behavior - Add file_upload_with_callback example - Update README with documentation and example
1 parent 4090b4f commit 89b55d2

File tree

9 files changed

+377
-6
lines changed

9 files changed

+377
-6
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ For example, if your connection limit is “1”, a browser may open a first con
269269
* `FILE_UPLOAD_MEMORY_AND_DISK`: The content of the file is stored in memory and on the file system.
270270
* _.file_upload_dir(**const std::string&** file_upload_dir):_ Specifies the directory to store all uploaded files. Default value is `/tmp`.
271271
* _.generate_random_filename_on_upload() and .no_generate_random_filename_on_upload():_ Enables/Disables the library to generate a unique and unused filename to store the uploaded file to. Otherwise the actually uploaded file name is used. `off` by default.
272+
* _.file_cleanup_callback(**file_cleanup_callback_ptr** callback):_ Sets a callback function to control what happens to uploaded files when the request completes. By default (when no callback is set), all uploaded files are automatically deleted. The callback signature is `bool(const std::string& key, const std::string& filename, const http::file_info& info)` where `key` is the form field name, `filename` is the original uploaded filename, and `info` contains file metadata including the filesystem path. Return `true` to delete the file (default behavior) or `false` to keep it (e.g., after moving it to permanent storage). If the callback throws an exception, the file will be deleted as a safety measure.
272273
* _.deferred()_ and _.no_deferred():_ Enables/Disables the ability for the server to suspend and resume connections. Simply put, it enables/disables the ability to use `deferred_response`. Read more [here](#building-responses-to-requests). `on` by default.
273274
* _.single_resource() and .no_single_resource:_ Sets or unsets the server in single resource mode. This limits all endpoints to be served from a single resource. The resultant is that the webserver will process the request matching to the endpoint skipping any complex semantic. Because of this, the option is incompatible with `regex_checking` and requires the resource to be registered against an empty endpoint or the root endpoint (`"/"`). The resource will also have to be registered as family. (For more information on resource registration, read more [here](#registering-resources)). `off` by default.
274275

@@ -718,6 +719,37 @@ Details on the `http::file_info` structure.
718719
* _**const std::string** get_content_type() **const**:_ Returns the content type of the file uploaded through the HTTP request.
719720
* _**const std::string** get_transfer_encoding() **const**:_ Returns the transfer encoding of the file uploaded through the HTTP request.
720721

722+
#### Example of keeping uploaded files
723+
By default, uploaded files are automatically deleted when the request completes. To keep files (e.g., move them to permanent storage), use the `file_cleanup_callback`:
724+
725+
```cpp
726+
#include <httpserver.hpp>
727+
#include <cstdio>
728+
729+
using namespace httpserver;
730+
731+
int main() {
732+
webserver ws = create_webserver(8080)
733+
.file_upload_target(FILE_UPLOAD_DISK_ONLY)
734+
.file_upload_dir("/tmp/uploads")
735+
.file_cleanup_callback([](const std::string& key,
736+
const std::string& filename,
737+
const http::file_info& info) {
738+
// Move file to permanent storage
739+
std::string dest = "/var/uploads/" + filename;
740+
std::rename(info.get_file_system_file_name().c_str(), dest.c_str());
741+
return false; // Don't delete - we moved it
742+
});
743+
744+
// ... register resources and start server
745+
}
746+
```
747+
To test file uploads, you can run the following command from a terminal:
748+
749+
curl -XPOST -F "file=@/path/to/your/file.txt" 'http://localhost:8080/upload'
750+
751+
You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/file_upload_with_callback.cpp).
752+
721753
Details on the `http_arg_value` structure.
722754

723755
* _**std::string_view** get_flat_value() **const**:_ Returns only the first value provided for the key.

examples/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
LDADD = $(top_builddir)/src/libhttpserver.la
2020
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/
2121
METASOURCES = AUTO
22-
noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload
22+
noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback
2323

2424
hello_world_SOURCES = hello_world.cpp
2525
service_SOURCES = service.cpp
@@ -42,6 +42,7 @@ benchmark_select_SOURCES = benchmark_select.cpp
4242
benchmark_threads_SOURCES = benchmark_threads.cpp
4343
benchmark_nodelay_SOURCES = benchmark_nodelay.cpp
4444
file_upload_SOURCES = file_upload.cpp
45+
file_upload_with_callback_SOURCES = file_upload_with_callback.cpp
4546

4647
if HAVE_GNUTLS
4748
LDADD += -lgnutls
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
#include <cstdio>
22+
#include <iostream>
23+
#include <memory>
24+
#include <string>
25+
26+
#include <httpserver.hpp>
27+
28+
class file_upload_resource : public httpserver::http_resource {
29+
public:
30+
std::shared_ptr<httpserver::http_response> render_GET(const httpserver::http_request&) {
31+
std::string get_response = "<html>\n";
32+
get_response += " <body>\n";
33+
get_response += " <h1>File Upload with Cleanup Callback Demo</h1>\n";
34+
get_response += " <p>Uploaded files will be moved to the permanent directory.</p>\n";
35+
get_response += " <form method=\"POST\" enctype=\"multipart/form-data\">\n";
36+
get_response += " <input type=\"file\" name=\"file\" multiple>\n";
37+
get_response += " <br><br>\n";
38+
get_response += " <input type=\"submit\" value=\"Upload\">\n";
39+
get_response += " </form>\n";
40+
get_response += " </body>\n";
41+
get_response += "</html>\n";
42+
43+
return std::shared_ptr<httpserver::http_response>(new httpserver::string_response(get_response, 200, "text/html"));
44+
}
45+
46+
std::shared_ptr<httpserver::http_response> render_POST(const httpserver::http_request& req) {
47+
std::string post_response = "<html>\n";
48+
post_response += "<body>\n";
49+
post_response += " <h1>Upload Complete</h1>\n";
50+
post_response += " <p>Files have been moved to permanent storage:</p>\n";
51+
post_response += " <ul>\n";
52+
53+
for (auto &file_key : req.get_files()) {
54+
for (auto &files : file_key.second) {
55+
post_response += " <li>" + files.first + " (" +
56+
std::to_string(files.second.get_file_size()) + " bytes)</li>\n";
57+
}
58+
}
59+
60+
post_response += " </ul>\n";
61+
post_response += " <a href=\"/\">Upload more</a>\n";
62+
post_response += "</body>\n</html>";
63+
return std::shared_ptr<httpserver::http_response>(new httpserver::string_response(post_response, 201, "text/html"));
64+
}
65+
};
66+
67+
int main(int argc, char** argv) {
68+
if (3 != argc) {
69+
std::cout << "Usage: file_upload_with_callback <temp_dir> <permanent_dir>" << std::endl;
70+
std::cout << std::endl;
71+
std::cout << " temp_dir: directory for temporary upload storage" << std::endl;
72+
std::cout << " permanent_dir: directory where files will be moved after upload" << std::endl;
73+
return -1;
74+
}
75+
76+
std::string temp_dir = argv[1];
77+
std::string permanent_dir = argv[2];
78+
79+
std::cout << "Starting file upload server on port 8080..." << std::endl;
80+
std::cout << " Temporary directory: " << temp_dir << std::endl;
81+
std::cout << " Permanent directory: " << permanent_dir << std::endl;
82+
std::cout << std::endl;
83+
std::cout << "Open http://localhost:8080 in your browser to upload files." << std::endl;
84+
85+
httpserver::webserver ws = httpserver::create_webserver(8080)
86+
.file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY)
87+
.file_upload_dir(temp_dir)
88+
.generate_random_filename_on_upload()
89+
.file_cleanup_callback([&permanent_dir](const std::string& key,
90+
const std::string& filename,
91+
const httpserver::http::file_info& info) {
92+
(void)key; // Unused in this example
93+
// Move the uploaded file to permanent storage
94+
std::string dest = permanent_dir + "/" + filename;
95+
int result = std::rename(info.get_file_system_file_name().c_str(), dest.c_str());
96+
97+
if (result == 0) {
98+
std::cout << "Moved: " << filename << " -> " << dest << std::endl;
99+
return false; // Don't delete - we moved it
100+
} else {
101+
std::cerr << "Failed to move " << filename << ", will be deleted" << std::endl;
102+
return true; // Delete the temp file on failure
103+
}
104+
});
105+
106+
file_upload_resource fur;
107+
ws.register_resource("/", &fur);
108+
ws.start(true);
109+
110+
return 0;
111+
}

src/http_request.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,21 @@ std::ostream &operator<< (std::ostream &os, const http_request &r) {
319319
}
320320

321321
http_request::~http_request() {
322-
for ( const auto &file_key : get_files() ) {
323-
for ( const auto &files : file_key.second ) {
324-
// C++17 has std::filesystem::remove()
325-
remove(files.second.get_file_system_file_name().c_str());
322+
for (const auto& file_key : get_files()) {
323+
for (const auto& files : file_key.second) {
324+
bool should_delete = true;
325+
if (file_cleanup_callback != nullptr) {
326+
try {
327+
should_delete = file_cleanup_callback(file_key.first, files.first, files.second);
328+
} catch (...) {
329+
// If callback throws, default to deleting the file
330+
should_delete = true;
331+
}
332+
}
333+
if (should_delete) {
334+
// C++17 has std::filesystem::remove()
335+
remove(files.second.get_file_system_file_name().c_str());
336+
}
326337
}
327338
}
328339
}

src/httpserver/create_webserver.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ typedef std::function<void(const std::string&)> log_access_ptr;
4848
typedef std::function<void(const std::string&)> log_error_ptr;
4949
typedef std::function<std::string(const std::string&)> psk_cred_handler_callback;
5050

51+
namespace http { class file_info; }
52+
53+
typedef std::function<bool(const std::string&, const std::string&, const http::file_info&)> file_cleanup_callback_ptr;
54+
5155
class create_webserver {
5256
public:
5357
create_webserver() = default;
@@ -364,6 +368,11 @@ class create_webserver {
364368
return *this;
365369
}
366370

371+
create_webserver& file_cleanup_callback(file_cleanup_callback_ptr callback) {
372+
_file_cleanup_callback = callback;
373+
return *this;
374+
}
375+
367376
private:
368377
uint16_t _port = DEFAULT_WS_PORT;
369378
http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT;
@@ -409,6 +418,7 @@ class create_webserver {
409418
render_ptr _not_found_resource = nullptr;
410419
render_ptr _method_not_allowed_resource = nullptr;
411420
render_ptr _internal_error_resource = nullptr;
421+
file_cleanup_callback_ptr _file_cleanup_callback = nullptr;
412422

413423
friend class webserver;
414424
};

src/httpserver/http_request.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "httpserver/http_arg_value.hpp"
4545
#include "httpserver/http_utils.hpp"
4646
#include "httpserver/file_info.hpp"
47+
#include "httpserver/create_webserver.hpp"
4748

4849
struct MHD_Connection;
4950

@@ -420,6 +421,12 @@ class http_request {
420421
// Populate the data cache unescaped_args
421422
void populate_args() const;
422423

424+
file_cleanup_callback_ptr file_cleanup_callback = nullptr;
425+
426+
void set_file_cleanup_callback(file_cleanup_callback_ptr callback) {
427+
file_cleanup_callback = callback;
428+
}
429+
423430
friend class webserver;
424431
friend struct details::modded_request;
425432
};

src/httpserver/webserver.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class webserver {
180180
const render_ptr not_found_resource;
181181
const render_ptr method_not_allowed_resource;
182182
const render_ptr internal_error_resource;
183+
const file_cleanup_callback_ptr file_cleanup_callback;
183184
std::shared_mutex registered_resources_mutex;
184185
std::map<details::http_endpoint, http_resource*> registered_resources;
185186
std::map<std::string, http_resource*> registered_resources_str;

src/webserver.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ webserver::webserver(const create_webserver& params):
165165
tcp_nodelay(params._tcp_nodelay),
166166
not_found_resource(params._not_found_resource),
167167
method_not_allowed_resource(params._method_not_allowed_resource),
168-
internal_error_resource(params._internal_error_resource) {
168+
internal_error_resource(params._internal_error_resource),
169+
file_cleanup_callback(params._file_cleanup_callback) {
169170
ignore_sigpipe();
170171
pthread_mutex_init(&mutexwait, nullptr);
171172
pthread_cond_init(&mutexcond, nullptr);
@@ -631,6 +632,7 @@ std::shared_ptr<http_response> webserver::internal_error_page(details::modded_re
631632

632633
MHD_Result webserver::requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr) {
633634
mr->dhr.reset(new http_request(connection, unescaper));
635+
mr->dhr->set_file_cleanup_callback(file_cleanup_callback);
634636

635637
if (!mr->has_body) {
636638
return MHD_YES;

0 commit comments

Comments
 (0)