1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
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)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/http
7  
// Official repository: https://github.com/cppalliance/http
8  
//
8  
//
9  

9  

10  
#include <boost/http/server/send_file.hpp>
10  
#include <boost/http/server/send_file.hpp>
11  
#include <boost/http/server/etag.hpp>
11  
#include <boost/http/server/etag.hpp>
12  
#include <boost/http/server/fresh.hpp>
12  
#include <boost/http/server/fresh.hpp>
13  
#include <boost/http/server/mime_types.hpp>
13  
#include <boost/http/server/mime_types.hpp>
14  
#include <boost/http/server/range_parser.hpp>
14  
#include <boost/http/server/range_parser.hpp>
15  
#include <boost/http/field.hpp>
15  
#include <boost/http/field.hpp>
16  
#include <boost/http/status.hpp>
16  
#include <boost/http/status.hpp>
17  
#include <ctime>
17  
#include <ctime>
18  
#include <filesystem>
18  
#include <filesystem>
19  

19  

20  
namespace boost {
20  
namespace boost {
21  
namespace http {
21  
namespace http {
22  

22  

23  
namespace {
23  
namespace {
24  

24  

25  
// Get file stats
25  
// Get file stats
26  
bool
26  
bool
27  
get_file_stats(
27  
get_file_stats(
28  
    core::string_view path,
28  
    core::string_view path,
29  
    std::uint64_t& size,
29  
    std::uint64_t& size,
30  
    std::uint64_t& mtime)
30  
    std::uint64_t& mtime)
31  
{
31  
{
32  
    system::error_code ec;
32  
    system::error_code ec;
33  
    std::filesystem::path p(path.begin(), path.end());
33  
    std::filesystem::path p(path.begin(), path.end());
34  

34  

35  
    auto status = std::filesystem::status(p, ec);
35  
    auto status = std::filesystem::status(p, ec);
36  
    if(ec.failed() || ! std::filesystem::is_regular_file(status))
36  
    if(ec.failed() || ! std::filesystem::is_regular_file(status))
37  
        return false;
37  
        return false;
38  

38  

39  
    size = static_cast<std::uint64_t>(
39  
    size = static_cast<std::uint64_t>(
40  
        std::filesystem::file_size(p, ec));
40  
        std::filesystem::file_size(p, ec));
41  
    if(ec.failed())
41  
    if(ec.failed())
42  
        return false;
42  
        return false;
43  

43  

44  
    auto ftime = std::filesystem::last_write_time(p, ec);
44  
    auto ftime = std::filesystem::last_write_time(p, ec);
45  
    if(ec.failed())
45  
    if(ec.failed())
46  
        return false;
46  
        return false;
47  

47  

48  
    // Convert to Unix timestamp
48  
    // Convert to Unix timestamp
49  
    auto const sctp = std::chrono::time_point_cast<
49  
    auto const sctp = std::chrono::time_point_cast<
50  
        std::chrono::system_clock::duration>(
50  
        std::chrono::system_clock::duration>(
51  
            ftime - std::filesystem::file_time_type::clock::now() +
51  
            ftime - std::filesystem::file_time_type::clock::now() +
52  
            std::chrono::system_clock::now());
52  
            std::chrono::system_clock::now());
53  
    mtime = static_cast<std::uint64_t>(
53  
    mtime = static_cast<std::uint64_t>(
54  
        std::chrono::system_clock::to_time_t(sctp));
54  
        std::chrono::system_clock::to_time_t(sctp));
55  

55  

56  
    return true;
56  
    return true;
57  
}
57  
}
58  

58  

59  
} // (anon)
59  
} // (anon)
60  

60  

61  
std::string
61  
std::string
62  
format_http_date(std::uint64_t mtime)
62  
format_http_date(std::uint64_t mtime)
63  
{
63  
{
64  
    std::time_t t = static_cast<std::time_t>(mtime);
64  
    std::time_t t = static_cast<std::time_t>(mtime);
65  
    std::tm tm;
65  
    std::tm tm;
66  
#ifdef _WIN32
66  
#ifdef _WIN32
67  
    gmtime_s(&tm, &t);
67  
    gmtime_s(&tm, &t);
68  
#else
68  
#else
69  
    gmtime_r(&t, &tm);
69  
    gmtime_r(&t, &tm);
70  
#endif
70  
#endif
71  

71  

72  
    char buf[64];
72  
    char buf[64];
73  
    std::strftime(buf, sizeof(buf),
73  
    std::strftime(buf, sizeof(buf),
74  
        "%a, %d %b %Y %H:%M:%S GMT", &tm);
74  
        "%a, %d %b %Y %H:%M:%S GMT", &tm);
75  
    return std::string(buf);
75  
    return std::string(buf);
76  
}
76  
}
77  

77  

78  
void
78  
void
79  
send_file_init(
79  
send_file_init(
80  
    send_file_info& info,
80  
    send_file_info& info,
81  
    route_params& rp,
81  
    route_params& rp,
82  
    core::string_view path,
82  
    core::string_view path,
83  
    send_file_options const& opts)
83  
    send_file_options const& opts)
84  
{
84  
{
85  
    info = send_file_info{};
85  
    info = send_file_info{};
86  

86  

87  
    // Get file stats
87  
    // Get file stats
88  
    if(! get_file_stats(path, info.size, info.mtime))
88  
    if(! get_file_stats(path, info.size, info.mtime))
89  
    {
89  
    {
90  
        info.result = send_file_result::not_found;
90  
        info.result = send_file_result::not_found;
91  
        return;
91  
        return;
92  
    }
92  
    }
93  

93  

94  
    // Determine content type
94  
    // Determine content type
95  
    if(! opts.content_type.empty())
95  
    if(! opts.content_type.empty())
96  
    {
96  
    {
97  
        info.content_type = opts.content_type;
97  
        info.content_type = opts.content_type;
98  
    }
98  
    }
99  
    else
99  
    else
100  
    {
100  
    {
101  
        auto ct = mime_types::content_type(path);
101  
        auto ct = mime_types::content_type(path);
102  
        if(ct.empty())
102  
        if(ct.empty())
103  
            ct = "application/octet-stream";
103  
            ct = "application/octet-stream";
104  
        info.content_type = std::move(ct);
104  
        info.content_type = std::move(ct);
105  
    }
105  
    }
106  

106  

107  
    // Generate ETag if enabled
107  
    // Generate ETag if enabled
108  
    if(opts.etag)
108  
    if(opts.etag)
109  
    {
109  
    {
110  
        info.etag = etag(info.size, info.mtime);
110  
        info.etag = etag(info.size, info.mtime);
111  
        rp.res.set(field::etag, info.etag);
111  
        rp.res.set(field::etag, info.etag);
112  
    }
112  
    }
113  

113  

114  
    // Set Last-Modified if enabled
114  
    // Set Last-Modified if enabled
115  
    if(opts.last_modified)
115  
    if(opts.last_modified)
116  
    {
116  
    {
117  
        info.last_modified = format_http_date(info.mtime);
117  
        info.last_modified = format_http_date(info.mtime);
118  
        rp.res.set(field::last_modified, info.last_modified);
118  
        rp.res.set(field::last_modified, info.last_modified);
119  
    }
119  
    }
120  

120  

121  
    // Set Cache-Control
121  
    // Set Cache-Control
122  
    if(opts.max_age > 0)
122  
    if(opts.max_age > 0)
123  
    {
123  
    {
124  
        std::string cc = "public, max-age=" +
124  
        std::string cc = "public, max-age=" +
125  
            std::to_string(opts.max_age);
125  
            std::to_string(opts.max_age);
126  
        rp.res.set(field::cache_control, cc);
126  
        rp.res.set(field::cache_control, cc);
127  
    }
127  
    }
128  

128  

129  
    // Check freshness (conditional GET)
129  
    // Check freshness (conditional GET)
130  
    if(is_fresh(rp.req, rp.res))
130  
    if(is_fresh(rp.req, rp.res))
131  
    {
131  
    {
132  
        info.result = send_file_result::not_modified;
132  
        info.result = send_file_result::not_modified;
133  
        return;
133  
        return;
134  
    }
134  
    }
135  

135  

136  
    // Set Content-Type
136  
    // Set Content-Type
137  
    rp.res.set(field::content_type, info.content_type);
137  
    rp.res.set(field::content_type, info.content_type);
138  

138  

139  
    // Handle Range header
139  
    // Handle Range header
140  
    auto range_header = rp.req.value_or(field::range, "");
140  
    auto range_header = rp.req.value_or(field::range, "");
141  
    if(! range_header.empty())
141  
    if(! range_header.empty())
142  
    {
142  
    {
143  
        auto range_result = parse_range(
143  
        auto range_result = parse_range(
144  
            static_cast<std::int64_t>(info.size),
144  
            static_cast<std::int64_t>(info.size),
145  
            range_header);
145  
            range_header);
146  

146  

147  
        if(range_result.type == range_result_type::ok &&
147  
        if(range_result.type == range_result_type::ok &&
148  
            ! range_result.ranges.empty())
148  
            ! range_result.ranges.empty())
149  
        {
149  
        {
150  
            // Use first range only (simplification)
150  
            // Use first range only (simplification)
151  
            auto const& range = range_result.ranges[0];
151  
            auto const& range = range_result.ranges[0];
152  
            info.is_range = true;
152  
            info.is_range = true;
153  
            info.range_start = range.start;
153  
            info.range_start = range.start;
154  
            info.range_end = range.end;
154  
            info.range_end = range.end;
155  

155  

156  
            // Set 206 Partial Content
156  
            // Set 206 Partial Content
157  
            rp.res.set_status(status::partial_content);
157  
            rp.res.set_status(status::partial_content);
158  

158  

159  
            auto const content_length =
159  
            auto const content_length =
160  
                range.end - range.start + 1;
160  
                range.end - range.start + 1;
161  
            rp.res.set_payload_size(
161  
            rp.res.set_payload_size(
162  
                static_cast<std::uint64_t>(content_length));
162  
                static_cast<std::uint64_t>(content_length));
163  

163  

164  
            // Content-Range header
164  
            // Content-Range header
165  
            std::string cr = "bytes " +
165  
            std::string cr = "bytes " +
166  
                std::to_string(range.start) + "-" +
166  
                std::to_string(range.start) + "-" +
167  
                std::to_string(range.end) + "/" +
167  
                std::to_string(range.end) + "/" +
168  
                std::to_string(info.size);
168  
                std::to_string(info.size);
169  
            rp.res.set(field::content_range, cr);
169  
            rp.res.set(field::content_range, cr);
170  

170  

171  
            info.result = send_file_result::ok;
171  
            info.result = send_file_result::ok;
172  
            return;
172  
            return;
173  
        }
173  
        }
174  

174  

175  
        if(range_result.type == range_result_type::unsatisfiable)
175  
        if(range_result.type == range_result_type::unsatisfiable)
176  
        {
176  
        {
177  
            rp.res.set_status(
177  
            rp.res.set_status(
178  
                status::range_not_satisfiable);
178  
                status::range_not_satisfiable);
179  
            rp.res.set(field::content_range,
179  
            rp.res.set(field::content_range,
180  
                "bytes */" + std::to_string(info.size));
180  
                "bytes */" + std::to_string(info.size));
181  
            info.result = send_file_result::error;
181  
            info.result = send_file_result::error;
182  
            return;
182  
            return;
183  
        }
183  
        }
184  
        // If malformed, ignore and serve full content
184  
        // If malformed, ignore and serve full content
185  
    }
185  
    }
186  

186  

187  
    // Full content response
187  
    // Full content response
188  
    rp.res.set_status(status::ok);
188  
    rp.res.set_status(status::ok);
189  
    rp.res.set_payload_size(info.size);
189  
    rp.res.set_payload_size(info.size);
190  
    info.range_start = 0;
190  
    info.range_start = 0;
191  
    info.range_end = static_cast<std::int64_t>(info.size) - 1;
191  
    info.range_end = static_cast<std::int64_t>(info.size) - 1;
192  
    info.result = send_file_result::ok;
192  
    info.result = send_file_result::ok;
193  
}
193  
}
194  

194  

195  
} // http
195  
} // http
196  
} // boost
196  
} // boost