Line data Source code
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 0 : get_file_stats(
28 : core::string_view path,
29 : std::uint64_t& size,
30 : std::uint64_t& mtime)
31 : {
32 0 : std::error_code ec;
33 0 : std::filesystem::path p(path.begin(), path.end());
34 :
35 0 : auto status = std::filesystem::status(p, ec);
36 0 : if(ec || ! std::filesystem::is_regular_file(status))
37 0 : return false;
38 :
39 0 : size = static_cast<std::uint64_t>(
40 0 : std::filesystem::file_size(p, ec));
41 0 : if(ec)
42 0 : return false;
43 :
44 0 : auto ftime = std::filesystem::last_write_time(p, ec);
45 0 : if(ec)
46 0 : return false;
47 :
48 : // Convert to Unix timestamp
49 : auto const sctp = std::chrono::time_point_cast<
50 0 : std::chrono::system_clock::duration>(
51 0 : ftime - std::filesystem::file_time_type::clock::now() +
52 0 : std::chrono::system_clock::now());
53 0 : mtime = static_cast<std::uint64_t>(
54 0 : std::chrono::system_clock::to_time_t(sctp));
55 :
56 0 : return true;
57 0 : }
58 :
59 : } // (anon)
60 :
61 : std::string
62 0 : format_http_date(std::uint64_t mtime)
63 : {
64 0 : 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 0 : gmtime_r(&t, &tm);
70 : #endif
71 :
72 : char buf[64];
73 0 : std::strftime(buf, sizeof(buf),
74 : "%a, %d %b %Y %H:%M:%S GMT", &tm);
75 0 : return std::string(buf);
76 : }
77 :
78 : void
79 0 : 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 0 : info = send_file_info{};
86 :
87 : // Get file stats
88 0 : if(! get_file_stats(path, info.size, info.mtime))
89 : {
90 0 : info.result = send_file_result::not_found;
91 0 : return;
92 : }
93 :
94 : // Determine content type
95 0 : if(! opts.content_type.empty())
96 : {
97 0 : info.content_type = opts.content_type;
98 : }
99 : else
100 : {
101 0 : auto ct = mime_types::content_type(path);
102 0 : if(ct.empty())
103 0 : ct = "application/octet-stream";
104 0 : info.content_type = std::move(ct);
105 0 : }
106 :
107 : // Generate ETag if enabled
108 0 : if(opts.etag)
109 : {
110 0 : info.etag = etag(info.size, info.mtime);
111 0 : rp.res.set(field::etag, info.etag);
112 : }
113 :
114 : // Set Last-Modified if enabled
115 0 : if(opts.last_modified)
116 : {
117 0 : info.last_modified = format_http_date(info.mtime);
118 0 : rp.res.set(field::last_modified, info.last_modified);
119 : }
120 :
121 : // Set Cache-Control
122 0 : if(opts.max_age > 0)
123 : {
124 : std::string cc = "public, max-age=" +
125 0 : std::to_string(opts.max_age);
126 0 : rp.res.set(field::cache_control, cc);
127 0 : }
128 :
129 : // Check freshness (conditional GET)
130 0 : if(is_fresh(rp.req, rp.res))
131 : {
132 0 : info.result = send_file_result::not_modified;
133 0 : return;
134 : }
135 :
136 : // Set Content-Type
137 0 : rp.res.set(field::content_type, info.content_type);
138 :
139 : // Handle Range header
140 0 : auto range_header = rp.req.value_or(field::range, "");
141 0 : if(! range_header.empty())
142 : {
143 : auto range_result = parse_range(
144 0 : static_cast<std::int64_t>(info.size),
145 0 : range_header);
146 :
147 0 : if(range_result.type == range_result_type::ok &&
148 0 : ! range_result.ranges.empty())
149 : {
150 : // Use first range only (simplification)
151 0 : auto const& range = range_result.ranges[0];
152 0 : info.is_range = true;
153 0 : info.range_start = range.start;
154 0 : info.range_end = range.end;
155 :
156 : // Set 206 Partial Content
157 0 : rp.res.set_status(status::partial_content);
158 :
159 0 : auto const content_length =
160 0 : range.end - range.start + 1;
161 0 : rp.res.set_payload_size(
162 : static_cast<std::uint64_t>(content_length));
163 :
164 : // Content-Range header
165 0 : std::string cr = "bytes " +
166 0 : std::to_string(range.start) + "-" +
167 0 : std::to_string(range.end) + "/" +
168 0 : std::to_string(info.size);
169 0 : rp.res.set(field::content_range, cr);
170 :
171 0 : info.result = send_file_result::ok;
172 0 : return;
173 0 : }
174 :
175 0 : if(range_result.type == range_result_type::unsatisfiable)
176 : {
177 0 : rp.res.set_status(
178 : status::range_not_satisfiable);
179 0 : rp.res.set(field::content_range,
180 0 : "bytes */" + std::to_string(info.size));
181 0 : info.result = send_file_result::error;
182 0 : return;
183 : }
184 : // If malformed, ignore and serve full content
185 0 : }
186 :
187 : // Full content response
188 0 : rp.res.set_status(status::ok);
189 0 : rp.res.set_payload_size(info.size);
190 0 : info.range_start = 0;
191 0 : info.range_end = static_cast<std::int64_t>(info.size) - 1;
192 0 : info.result = send_file_result::ok;
193 : }
194 :
195 : } // http
196 : } // boost
|