Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/http/range_source.rs

4.8 KiB, 1 run

created by r1870400018:60842, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A remote file read by byte range over HTTP or HTTPS, as a PMTiles [`RangeSource`].
2//!
3//! `fe2o3_geom` reads a PMTiles archive through any [`RangeSource`] and carries no network
4//! code; this is the network half. One request per range, `Connection: close`, as the rest of
5//! this client does. A ranged request must come back `206 Partial Content` with exactly the
6//! bytes asked for; a server that ignores `Range` and sends the whole file is refused rather
7//! than read, since the file may be a hundred gigabytes.
8//!
9//! [`HttpRangeSource::read_async`] is the form for a caller already in an async runtime. The
10//! synchronous [`RangeSource::read`] runs the request on a runtime of its own and refuses,
11//! rather than panics, when called from inside another.
12
13use crate::http::{
14 client::{
15 http_request,
16 https_request,
17 },
18 header::{
19 HttpHeadline,
20 HttpMethod,
21 },
22 status::HttpStatus,
23};
24
25use oxedyne_fe2o3_core::prelude::*;
26use oxedyne_fe2o3_geom::tile::pmtiles::RangeSource;
27
28use std::sync::Arc;
29
30use tokio_rustls::rustls::ClientConfig;
31
32/// A remote file, addressed by URL, read by `Range` requests.
33pub struct HttpRangeSource {
34 host: String,
35 port: u16,
36 path: String,
37 tls: Option<Arc<ClientConfig>>, // None for plain HTTP
38}
39
40impl HttpRangeSource {
41 /// A source for an `https://` or `http://` URL. An `https://` URL needs `tls`, for which
42 /// [`crate::tls::default_client_config`] gives the host's own trust store.
43 pub fn new(url: &str, tls: Option<Arc<ClientConfig>>) -> Outcome<Self> {
44 let (secure, rest) = if let Some(r) = url.strip_prefix("https://") {
45 (true, r)
46 } else if let Some(r) = url.strip_prefix("http://") {
47 (false, r)
48 } else {
49 return Err(err!("{:?} is not an http or https URL.", url; Invalid, Input));
50 };
51 let (authority, path) = match rest.find('/') {
52 Some(i) => (&rest[..i], rest[i..].to_string()),
53 None => (rest, "/".to_string()),
54 };
55 let (host, port) = match authority.rsplit_once(':') {
56 Some((h, p)) => (h.to_string(), res!(p.parse::<u16>(), Invalid, Input)),
57 None => (authority.to_string(), if secure { 443 } else { 80 }),
58 };
59 if host.is_empty() {
60 return Err(err!("{:?} names no host.", url; Invalid, Input, Missing));
61 }
62 if secure && tls.is_none() {
63 return Err(err!("{:?} is https, and no TLS configuration was given.", url;
64 Invalid, Input, Missing));
65 }
66 Ok(Self { host, port, path, tls: if secure { tls } else { None } })
67 }
68
69 /// Reads `len` bytes from `offset`.
70 pub async fn read_async(&self, offset: u64, len: u64) -> Outcome<Vec<u8>> {
71 if len == 0 {
72 return Ok(Vec::new());
73 }
74 let last = match offset.checked_add(len - 1) {
75 Some(l) => l,
76 None => return Err(err!("Bytes {}+{} run past the end of any file.", offset, len;
77 Invalid, Input, Range)),
78 };
79 let range = fmt!("bytes={}-{}", offset, last);
80 let headers = [("Range", range.as_str()), ("Accept-Encoding", "identity")];
81 let msg = match &self.tls {
82 Some(tls) => res!(https_request(&self.host, self.port, HttpMethod::GET, &self.path,
83 &headers, &[], tls.clone()).await),
84 None => res!(http_request(&self.host, self.port, HttpMethod::GET, &self.path,
85 &headers, &[]).await),
86 };
87 match &msg.header.headline {
88 HttpHeadline::Response { status: HttpStatus::PartialContent } => (),
89 HttpHeadline::Response { status } => return Err(err!(
90 "{}{} answered {:?} to {}, not 206 Partial Content.", self.host, self.path,
91 status, range; Network, Unexpected)),
92 _ => return Err(err!("{} sent a request in reply.", self.host; Network, Unexpected)),
93 }
94 if msg.body.len() as u64 != len {
95 return Err(err!("{}{} sent {} bytes for {}, which is {} bytes.", self.host, self.path,
96 msg.body.len(), range, len; Network, Mismatch, Size));
97 }
98 Ok(msg.body)
99 }
100}
101
102impl RangeSource for HttpRangeSource {
103 fn read(&self, offset: u64, len: u64) -> Outcome<Vec<u8>> {
104 if tokio::runtime::Handle::try_current().is_ok() {
105 return Err(err!("A blocking range read was asked for inside an async runtime; \
106 use read_async there."; Invalid, Thread));
107 }
108 // A runtime a read, built and dropped here: one kept in the source would panic when
109 // dropped by a caller that had since moved into an async context.
110 let rt = res!(tokio::runtime::Builder::new_current_thread().enable_all().build(),
111 Init, Thread);
112 rt.block_on(self.read_async(offset, len))
113 }
114}