GCC Code Coverage Report


Directory: ./
File: libs/http/src/server/send_file.cpp
Date: 2026-01-20 00:11:35
Exec Total Coverage
Lines: 0 85 0.0%
Functions: 0 3 0.0%
Branches: 0 68 0.0%

Line Branch Exec Source
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot 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 std::error_code ec;
33 std::filesystem::path p(path.begin(), path.end());
34
35 auto status = std::filesystem::status(p, ec);
36 if(ec || ! 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)
42 return false;
43
44 auto ftime = std::filesystem::last_write_time(p, ec);
45 if(ec)
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