libs/http/src/server/send_file.cpp

0.0% Lines (0/85) 0.0% Functions (0/3) 0.0% Branches (0/71)
libs/http/src/server/send_file.cpp
Line Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/http
8 //
9
10 #include <boost/http/server/send_file.hpp>
11 #include <boost/http/server/etag.hpp>
12 #include <boost/http/server/fresh.hpp>
13 #include <boost/http/server/mime_types.hpp>
14 #include <boost/http/server/range_parser.hpp>
15 #include <boost/http/field.hpp>
16 #include <boost/http/status.hpp>
17 #include <ctime>
18 #include <filesystem>
19
20 namespace boost {
21 namespace http {
22
23 namespace {
24
25 // Get file stats
26 bool
27 ✗ get_file_stats(
28 core::string_view path,
29 std::uint64_t& size,
30 std::uint64_t& mtime)
31 {
32 ✗ system::error_code ec;
33 ✗ std::filesystem::path p(path.begin(), path.end());
34
35 ✗ auto status = std::filesystem::status(p, ec);
36 ✗ if(ec.failed() || ! std::filesystem::is_regular_file(status))
37 ✗ return false;
38
39 ✗ size = static_cast<std::uint64_t>(
40 ✗ std::filesystem::file_size(p, ec));
41 ✗ if(ec.failed())
42 ✗ return false;
43
44 ✗ auto ftime = std::filesystem::last_write_time(p, ec);
45 ✗ if(ec.failed())
46 ✗ return false;
47
48 // Convert to Unix timestamp
49 auto const sctp = std::chrono::time_point_cast<
50 ✗ std::chrono::system_clock::duration>(
51 ✗ ftime - std::filesystem::file_time_type::clock::now() +
52 ✗ std::chrono::system_clock::now());
53 ✗ mtime = static_cast<std::uint64_t>(
54 ✗ std::chrono::system_clock::to_time_t(sctp));
55
56 ✗ return true;
57 ✗ }
58
59 } // (anon)
60
61 std::string
62 ✗ format_http_date(std::uint64_t mtime)
63 {
64 ✗ std::time_t t = static_cast<std::time_t>(mtime);
65 std::tm tm;
66 #ifdef _WIN32
67 gmtime_s(&tm, &t);
68 #else
69 ✗ gmtime_r(&t, &tm);
70 #endif
71
72 char buf[64];
73 ✗ std::strftime(buf, sizeof(buf),
74 "%a, %d %b %Y %H:%M:%S GMT", &tm);
75 ✗ return std::string(buf);
76 }
77
78 void
79 ✗ send_file_init(
80 send_file_info& info,
81 route_params& rp,
82 core::string_view path,
83 send_file_options const& opts)
84 {
85 ✗ info = send_file_info{};
86
87 // Get file stats
88 ✗ if(! get_file_stats(path, info.size, info.mtime))
89 {
90 ✗ info.result = send_file_result::not_found;
91 ✗ return;
92 }
93
94 // Determine content type
95 ✗ if(! opts.content_type.empty())
96 {
97 ✗ info.content_type = opts.content_type;
98 }
99 else
100 {
101 ✗ auto ct = mime_types::content_type(path);
102 ✗ if(ct.empty())
103 ✗ ct = "application/octet-stream";
104 ✗ info.content_type = std::move(ct);
105 ✗ }
106
107 // Generate ETag if enabled
108 ✗ if(opts.etag)
109 {
110 ✗ info.etag = etag(info.size, info.mtime);
111 ✗ rp.res.set(field::etag, info.etag);
112 }
113
114 // Set Last-Modified if enabled
115 ✗ if(opts.last_modified)
116 {
117 ✗ info.last_modified = format_http_date(info.mtime);
118 ✗ rp.res.set(field::last_modified, info.last_modified);
119 }
120
121 // Set Cache-Control
122 ✗ if(opts.max_age > 0)
123 {
124 std::string cc = "public, max-age=" +
125 ✗ std::to_string(opts.max_age);
126 ✗ rp.res.set(field::cache_control, cc);
127 ✗ }
128
129 // Check freshness (conditional GET)
130 ✗ if(is_fresh(rp.req, rp.res))
131 {
132 ✗ info.result = send_file_result::not_modified;
133 ✗ return;
134 }
135
136 // Set Content-Type
137 ✗ rp.res.set(field::content_type, info.content_type);
138
139 // Handle Range header
140 ✗ auto range_header = rp.req.value_or(field::range, "");
141 ✗ if(! range_header.empty())
142 {
143 auto range_result = parse_range(
144 ✗ static_cast<std::int64_t>(info.size),
145 ✗ range_header);
146
147 ✗ if(range_result.type == range_result_type::ok &&
148 ✗ ! range_result.ranges.empty())
149 {
150 // Use first range only (simplification)
151 ✗ auto const& range = range_result.ranges[0];
152 ✗ info.is_range = true;
153 ✗ info.range_start = range.start;
154 ✗ info.range_end = range.end;
155
156 // Set 206 Partial Content
157 ✗ rp.res.set_status(status::partial_content);
158
159 ✗ auto const content_length =
160 ✗ range.end - range.start + 1;
161 ✗ rp.res.set_payload_size(
162 static_cast<std::uint64_t>(content_length));
163
164 // Content-Range header
165 ✗ std::string cr = "bytes " +
166 ✗ std::to_string(range.start) + "-" +
167 ✗ std::to_string(range.end) + "/" +
168 ✗ std::to_string(info.size);
169 ✗ rp.res.set(field::content_range, cr);
170
171 ✗ info.result = send_file_result::ok;
172 ✗ return;
173 ✗ }
174
175 ✗ if(range_result.type == range_result_type::unsatisfiable)
176 {
177 ✗ rp.res.set_status(
178 status::range_not_satisfiable);
179 ✗ rp.res.set(field::content_range,
180 ✗ "bytes */" + std::to_string(info.size));
181 ✗ info.result = send_file_result::error;
182 ✗ return;
183 }
184 // If malformed, ignore and serve full content
185 ✗ }
186
187 // Full content response
188 ✗ rp.res.set_status(status::ok);
189 ✗ rp.res.set_payload_size(info.size);
190 ✗ info.range_start = 0;
191 ✗ info.range_end = static_cast<std::int64_t>(info.size) - 1;
192 ✗ info.result = send_file_result::ok;
193 }
194
195 } // http
196 } // boost
197