itsi 0.1.14 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +124 -109
- data/Cargo.toml +6 -0
- data/crates/itsi_error/Cargo.toml +1 -0
- data/crates/itsi_error/src/lib.rs +100 -10
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
- data/crates/itsi_server/Cargo.toml +8 -10
- data/crates/itsi_server/src/default_responses/html/401.html +68 -0
- data/crates/itsi_server/src/default_responses/html/403.html +68 -0
- data/crates/itsi_server/src/default_responses/html/404.html +68 -0
- data/crates/itsi_server/src/default_responses/html/413.html +71 -0
- data/crates/itsi_server/src/default_responses/html/429.html +68 -0
- data/crates/itsi_server/src/default_responses/html/500.html +71 -0
- data/crates/itsi_server/src/default_responses/html/502.html +71 -0
- data/crates/itsi_server/src/default_responses/html/503.html +68 -0
- data/crates/itsi_server/src/default_responses/html/504.html +69 -0
- data/crates/itsi_server/src/default_responses/html/index.html +238 -0
- data/crates/itsi_server/src/default_responses/json/401.json +6 -0
- data/crates/itsi_server/src/default_responses/json/403.json +6 -0
- data/crates/itsi_server/src/default_responses/json/404.json +6 -0
- data/crates/itsi_server/src/default_responses/json/413.json +6 -0
- data/crates/itsi_server/src/default_responses/json/429.json +6 -0
- data/crates/itsi_server/src/default_responses/json/500.json +6 -0
- data/crates/itsi_server/src/default_responses/json/502.json +6 -0
- data/crates/itsi_server/src/default_responses/json/503.json +6 -0
- data/crates/itsi_server/src/default_responses/json/504.json +6 -0
- data/crates/itsi_server/src/default_responses/mod.rs +11 -0
- data/crates/itsi_server/src/lib.rs +58 -26
- data/crates/itsi_server/src/prelude.rs +2 -0
- data/crates/itsi_server/src/ruby_types/README.md +21 -0
- data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
- data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
- data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
- data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
- data/crates/itsi_server/src/server/binds/mod.rs +4 -0
- data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
- data/crates/itsi_server/src/server/http_message_types.rs +97 -0
- data/crates/itsi_server/src/server/io_stream.rs +2 -1
- data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
- data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
- data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
- data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
- data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
- data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
- data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
- data/crates/itsi_server/src/server/mod.rs +3 -9
- data/crates/itsi_server/src/server/process_worker.rs +21 -3
- data/crates/itsi_server/src/server/request_job.rs +2 -2
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
- data/crates/itsi_server/src/server/signal.rs +24 -41
- data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/crates/itsi_server/src/server/thread_worker.rs +59 -28
- data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/crates/itsi_server/src/services/mime_types.rs +1416 -0
- data/crates/itsi_server/src/services/mod.rs +6 -0
- data/crates/itsi_server/src/services/password_hasher.rs +83 -0
- data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
- data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
- data/crates/itsi_tracing/src/lib.rs +145 -55
- data/{Itsi.rb → foo/Itsi.rb} +6 -9
- data/gems/scheduler/Cargo.lock +7 -0
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/helpers/test_helper.rb +0 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_network_io.rb +1 -1
- data/gems/scheduler/test/test_process_wait.rb +0 -1
- data/gems/server/Cargo.lock +124 -109
- data/gems/server/exe/itsi +65 -19
- data/gems/server/itsi-server.gemspec +4 -3
- data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/gems/server/lib/itsi/http_request.rb +116 -17
- data/gems/server/lib/itsi/http_response.rb +2 -0
- data/gems/server/lib/itsi/passfile.rb +109 -0
- data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
- data/gems/server/lib/itsi/server/config.rb +58 -23
- data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
- data/gems/server/lib/itsi/server/default_app/index.html +113 -89
- data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/gems/server/lib/itsi/server/route_tester.rb +107 -0
- data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +82 -12
- data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/gems/server/lib/shell_completions/completions.rb +26 -0
- data/gems/server/test/helpers/test_helper.rb +2 -1
- data/lib/itsi/version.rb +1 -1
- data/sandbox/README.md +5 -0
- data/sandbox/itsi_file/Gemfile +4 -2
- data/sandbox/itsi_file/Gemfile.lock +48 -6
- data/sandbox/itsi_file/Itsi.rb +326 -129
- data/sandbox/itsi_file/call.json +1 -0
- data/sandbox/itsi_file/echo_client/Gemfile +10 -0
- data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
- data/sandbox/itsi_file/echo_client/README.md +95 -0
- data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
- data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
- data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
- data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
- data/sandbox/itsi_sandbox_async/config.ru +0 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
- data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
- data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
- data/sandbox/itsi_sinatra/app.rb +0 -1
- data/sandbox/static_files/.env +1 -0
- data/sandbox/static_files/404.html +25 -0
- data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
- data/sandbox/static_files/about.html +68 -0
- data/sandbox/static_files/tiny.html +1 -0
- data/sandbox/static_files/writebook.zip +0 -0
- data/tasks.txt +28 -33
- metadata +87 -26
- data/crates/itsi_error/src/from.rs +0 -68
- data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
- data/crates/itsi_server/src/server/itsi_service.rs +0 -172
- data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
- data/crates/itsi_server/src/server/types.rs +0 -43
- data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
- data/sandbox/itsi_file/public/assets/index.html +0 -1
- /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
- /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
- /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -1,25 +1,42 @@
|
|
1
|
-
use
|
2
|
-
|
3
|
-
|
1
|
+
use crate::{
|
2
|
+
default_responses::{INTERNAL_SERVER_ERROR_RESPONSE, NOT_FOUND_RESPONSE},
|
3
|
+
prelude::*,
|
4
|
+
server::{
|
5
|
+
http_message_types::{HttpRequest, HttpResponse, RequestExt, ResponseFormat},
|
6
|
+
middleware_stack::ErrorResponse,
|
7
|
+
},
|
4
8
|
};
|
9
|
+
use base64::{engine::general_purpose, Engine};
|
5
10
|
use bytes::Bytes;
|
6
11
|
use chrono::{DateTime, Utc};
|
7
|
-
use http::{
|
12
|
+
use http::{
|
13
|
+
header::{
|
14
|
+
self, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, LAST_MODIFIED,
|
15
|
+
},
|
16
|
+
HeaderValue, Response, StatusCode,
|
17
|
+
};
|
8
18
|
use http_body_util::{combinators::BoxBody, Full};
|
9
19
|
use itsi_error::Result;
|
10
20
|
use moka::sync::Cache;
|
21
|
+
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
|
11
22
|
use serde::Deserialize;
|
23
|
+
use serde_json::json;
|
24
|
+
use sha2::{Digest, Sha256};
|
12
25
|
use std::{
|
26
|
+
borrow::Cow,
|
27
|
+
cmp::Ordering,
|
13
28
|
collections::HashMap,
|
14
29
|
convert::Infallible,
|
15
30
|
fs::Metadata,
|
31
|
+
ops::Deref,
|
16
32
|
path::{Path, PathBuf},
|
17
33
|
sync::{Arc, LazyLock},
|
18
34
|
time::{Duration, Instant, SystemTime},
|
19
35
|
};
|
20
36
|
use tokio::sync::Mutex;
|
21
37
|
use tokio::{fs::File, io::AsyncReadExt};
|
22
|
-
|
38
|
+
|
39
|
+
use super::mime_types::get_mime_type;
|
23
40
|
|
24
41
|
pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|| {
|
25
42
|
StaticFileServer::new(StaticFileServerConfig {
|
@@ -29,7 +46,9 @@ pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|
|
|
29
46
|
recheck_interval: Duration::from_secs(1),
|
30
47
|
try_html_extension: true,
|
31
48
|
auto_index: true,
|
32
|
-
not_found_behavior: NotFoundBehavior::Error(ErrorResponse::
|
49
|
+
not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
|
50
|
+
serve_hidden_files: false,
|
51
|
+
allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
|
33
52
|
})
|
34
53
|
});
|
35
54
|
|
@@ -61,6 +80,8 @@ pub struct StaticFileServerConfig {
|
|
61
80
|
pub try_html_extension: bool,
|
62
81
|
pub auto_index: bool,
|
63
82
|
pub not_found_behavior: NotFoundBehavior,
|
83
|
+
pub serve_hidden_files: bool,
|
84
|
+
pub allowed_extensions: Vec<String>,
|
64
85
|
}
|
65
86
|
|
66
87
|
#[derive(Debug, Clone)]
|
@@ -70,13 +91,55 @@ pub struct StaticFileServer {
|
|
70
91
|
cache: Cache<PathBuf, CacheEntry>,
|
71
92
|
}
|
72
93
|
|
94
|
+
impl Deref for StaticFileServer {
|
95
|
+
type Target = StaticFileServerConfig;
|
96
|
+
|
97
|
+
fn deref(&self) -> &Self::Target {
|
98
|
+
&self.config
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
73
102
|
#[derive(Clone, Debug)]
|
74
103
|
struct CacheEntry {
|
75
104
|
content: Arc<Bytes>,
|
105
|
+
br_encoded: Option<Arc<Bytes>>,
|
106
|
+
zstd_encoded: Option<Arc<Bytes>>,
|
107
|
+
gzip_encoded: Option<Arc<Bytes>>,
|
108
|
+
deflate_encoded: Option<Arc<Bytes>>,
|
109
|
+
etag: String,
|
76
110
|
last_modified: SystemTime,
|
77
111
|
last_checked: Instant,
|
78
112
|
}
|
79
113
|
|
114
|
+
impl CacheEntry {
|
115
|
+
pub fn suggest_content_for(&self, supported_encodings: &[HeaderValue]) -> (Arc<Bytes>, &str) {
|
116
|
+
for encoding_header in supported_encodings {
|
117
|
+
if let Ok(header_value) = encoding_header.to_str() {
|
118
|
+
for header_value in header_value.split(",").map(|hv| hv.trim()) {
|
119
|
+
for algo in header_value.split(";").map(|hv| hv.trim()) {
|
120
|
+
match algo {
|
121
|
+
"zstd" if self.zstd_encoded.is_some() => {
|
122
|
+
return (self.zstd_encoded.clone().unwrap(), "zstd")
|
123
|
+
}
|
124
|
+
"gzip" if self.gzip_encoded.is_some() => {
|
125
|
+
return (self.gzip_encoded.clone().unwrap(), "gzip")
|
126
|
+
}
|
127
|
+
"br" if self.br_encoded.is_some() => {
|
128
|
+
return (self.br_encoded.clone().unwrap(), "br")
|
129
|
+
}
|
130
|
+
"deflate" if self.deflate_encoded.is_some() => {
|
131
|
+
return (self.deflate_encoded.clone().unwrap(), "deflate")
|
132
|
+
}
|
133
|
+
_ => {}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
(self.content.clone(), "identity")
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
80
143
|
#[derive(Debug, Clone)]
|
81
144
|
pub enum ServeRange {
|
82
145
|
Range(u64, u64),
|
@@ -86,21 +149,48 @@ pub enum ServeRange {
|
|
86
149
|
impl CacheEntry {
|
87
150
|
async fn new(path: PathBuf) -> Result<Self> {
|
88
151
|
let (bytes, last_modified) = read_entire_file(&path).await?;
|
152
|
+
let etag = {
|
153
|
+
let mut hasher = Sha256::new();
|
154
|
+
hasher.update(&bytes);
|
155
|
+
let result = hasher.finalize();
|
156
|
+
general_purpose::STANDARD.encode(result)
|
157
|
+
};
|
89
158
|
Ok(CacheEntry {
|
90
159
|
content: Arc::new(bytes),
|
160
|
+
gzip_encoded: read_variant(&path, "gz").await.map(Arc::new),
|
161
|
+
br_encoded: read_variant(&path, "br").await.map(Arc::new),
|
162
|
+
zstd_encoded: read_variant(&path, "zstd").await.map(Arc::new),
|
163
|
+
deflate_encoded: read_variant(&path, "deflate").await.map(Arc::new),
|
91
164
|
last_modified,
|
165
|
+
etag,
|
92
166
|
last_checked: Instant::now(),
|
93
167
|
})
|
94
168
|
}
|
95
169
|
|
96
|
-
async fn new_virtual_listing(
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
170
|
+
async fn new_virtual_listing(
|
171
|
+
path: PathBuf,
|
172
|
+
config: &StaticFileServerConfig,
|
173
|
+
accept: ResponseFormat,
|
174
|
+
) -> Self {
|
175
|
+
let directory_listing: Bytes =
|
176
|
+
generate_directory_listing(path.parent().unwrap(), config, accept)
|
177
|
+
.await
|
178
|
+
.unwrap_or("".to_owned())
|
179
|
+
.into();
|
180
|
+
let etag = {
|
181
|
+
let mut hasher = Sha256::new();
|
182
|
+
hasher.update(&directory_listing);
|
183
|
+
let result = hasher.finalize();
|
184
|
+
general_purpose::STANDARD.encode(result)
|
185
|
+
};
|
101
186
|
CacheEntry {
|
102
187
|
content: Arc::new(directory_listing),
|
188
|
+
gzip_encoded: None,
|
189
|
+
br_encoded: None,
|
190
|
+
zstd_encoded: None,
|
191
|
+
deflate_encoded: None,
|
103
192
|
last_modified: SystemTime::now(),
|
193
|
+
etag,
|
104
194
|
last_checked: Instant::now(),
|
105
195
|
}
|
106
196
|
}
|
@@ -115,6 +205,7 @@ struct ServeCacheArgs<'a>(
|
|
115
205
|
Option<SystemTime>,
|
116
206
|
bool,
|
117
207
|
&'a Path,
|
208
|
+
&'a [HeaderValue],
|
118
209
|
);
|
119
210
|
|
120
211
|
impl StaticFileServer {
|
@@ -128,6 +219,7 @@ impl StaticFileServer {
|
|
128
219
|
}
|
129
220
|
}
|
130
221
|
|
222
|
+
#[allow(clippy::too_many_arguments)]
|
131
223
|
pub async fn serve(
|
132
224
|
&self,
|
133
225
|
request: &HttpRequest,
|
@@ -136,8 +228,11 @@ impl StaticFileServer {
|
|
136
228
|
serve_range: ServeRange,
|
137
229
|
if_modified_since: Option<SystemTime>,
|
138
230
|
is_head_request: bool,
|
231
|
+
supported_encodings: &[HeaderValue],
|
139
232
|
) -> Option<HttpResponse> {
|
140
|
-
let
|
233
|
+
let accept: ResponseFormat = request.accept().into();
|
234
|
+
let resolved = self.resolve(path, abs_path, accept.clone()).await;
|
235
|
+
|
141
236
|
Some(match resolved {
|
142
237
|
Ok(ResolvedAsset {
|
143
238
|
path,
|
@@ -160,6 +255,7 @@ impl StaticFileServer {
|
|
160
255
|
if_modified_since,
|
161
256
|
is_head_request,
|
162
257
|
&path,
|
258
|
+
supported_encodings,
|
163
259
|
))
|
164
260
|
} else {
|
165
261
|
self.serve_stream_content(ServeStreamArgs(
|
@@ -184,27 +280,58 @@ impl StaticFileServer {
|
|
184
280
|
.unwrap(),
|
185
281
|
Err(not_found_behavior) => match not_found_behavior {
|
186
282
|
NotFoundBehavior::Error(error_response) => {
|
187
|
-
error_response
|
283
|
+
error_response
|
284
|
+
.to_http_response(request.accept().into())
|
285
|
+
.await
|
188
286
|
}
|
189
287
|
NotFoundBehavior::FallThrough => return None,
|
190
288
|
NotFoundBehavior::IndexFile(index_file) => {
|
191
|
-
self.serve_single(index_file.to_str().unwrap())
|
289
|
+
self.serve_single(index_file.to_str().unwrap(), accept, supported_encodings)
|
290
|
+
.await
|
192
291
|
}
|
193
292
|
NotFoundBehavior::Redirect(redirect) => Response::builder()
|
194
293
|
.status(StatusCode::MOVED_PERMANENTLY)
|
195
294
|
.header(header::LOCATION, redirect.to)
|
196
295
|
.body(BoxBody::new(Full::new(Bytes::new())))
|
197
296
|
.unwrap(),
|
198
|
-
NotFoundBehavior::InternalServerError =>
|
199
|
-
|
200
|
-
|
201
|
-
|
297
|
+
NotFoundBehavior::InternalServerError => {
|
298
|
+
INTERNAL_SERVER_ERROR_RESPONSE
|
299
|
+
.to_http_response(request.accept().into())
|
300
|
+
.await
|
301
|
+
}
|
202
302
|
},
|
203
303
|
})
|
204
304
|
}
|
205
305
|
|
206
|
-
pub async fn
|
207
|
-
|
306
|
+
pub async fn serve_single_abs(
|
307
|
+
&self,
|
308
|
+
path: &str,
|
309
|
+
accept: ResponseFormat,
|
310
|
+
supported_encodings: &[HeaderValue],
|
311
|
+
) -> HttpResponse {
|
312
|
+
if let (Ok(root), Ok(path_buf)) = (
|
313
|
+
self.root_dir.canonicalize(),
|
314
|
+
PathBuf::from(path).canonicalize(),
|
315
|
+
) {
|
316
|
+
// Check that the path is under root.
|
317
|
+
if let Ok(stripped) = path_buf.strip_prefix(root) {
|
318
|
+
if let Some(stripped_str) = stripped.to_str() {
|
319
|
+
return self
|
320
|
+
.serve_single(stripped_str, accept, supported_encodings)
|
321
|
+
.await;
|
322
|
+
}
|
323
|
+
}
|
324
|
+
}
|
325
|
+
NOT_FOUND_RESPONSE.to_http_response(accept).await
|
326
|
+
}
|
327
|
+
|
328
|
+
pub async fn serve_single(
|
329
|
+
&self,
|
330
|
+
path: &str,
|
331
|
+
accept: ResponseFormat,
|
332
|
+
supported_encodings: &[HeaderValue],
|
333
|
+
) -> HttpResponse {
|
334
|
+
let resolved = self.resolve(path, path, accept).await;
|
208
335
|
if let Ok(ResolvedAsset {
|
209
336
|
path,
|
210
337
|
cache_entry: Some(cache_entry),
|
@@ -219,6 +346,7 @@ impl StaticFileServer {
|
|
219
346
|
None,
|
220
347
|
false,
|
221
348
|
&path,
|
349
|
+
supported_encodings,
|
222
350
|
));
|
223
351
|
} else if let Ok(ResolvedAsset { path, metadata, .. }) = resolved {
|
224
352
|
return self
|
@@ -245,6 +373,7 @@ impl StaticFileServer {
|
|
245
373
|
&self,
|
246
374
|
key: &str,
|
247
375
|
abs_path: &str,
|
376
|
+
accept: ResponseFormat,
|
248
377
|
) -> std::result::Result<ResolvedAsset, NotFoundBehavior> {
|
249
378
|
// First check if we have a cached mapping for this key
|
250
379
|
if let Some(path) = self.key_to_path.lock().await.get(key) {
|
@@ -290,7 +419,20 @@ impl StaticFileServer {
|
|
290
419
|
}
|
291
420
|
|
292
421
|
// No valid cached entry, resolve the key to a file path
|
293
|
-
let
|
422
|
+
let decoded_key = percent_decode_str(key).decode_utf8_lossy();
|
423
|
+
let normalized_path =
|
424
|
+
normalize_path(decoded_key).ok_or(NotFoundBehavior::InternalServerError)?;
|
425
|
+
|
426
|
+
if !self.config.serve_hidden_files
|
427
|
+
&& normalized_path
|
428
|
+
.file_name()
|
429
|
+
.and_then(|f| f.to_str())
|
430
|
+
.unwrap_or("")
|
431
|
+
.starts_with('.')
|
432
|
+
{
|
433
|
+
return Err(self.config.not_found_behavior.clone());
|
434
|
+
}
|
435
|
+
|
294
436
|
let mut full_path = self.config.root_dir.clone();
|
295
437
|
full_path.push(normalized_path);
|
296
438
|
// Check if path exists and is a file
|
@@ -368,14 +510,17 @@ impl StaticFileServer {
|
|
368
510
|
});
|
369
511
|
}
|
370
512
|
|
371
|
-
// No index.html, check if auto_index is enabled
|
372
513
|
if self.config.auto_index {
|
373
|
-
|
374
|
-
|
514
|
+
let virtual_path = if matches!(accept, ResponseFormat::JSON) {
|
515
|
+
full_path.join(".directory_listing.dir_list_json")
|
516
|
+
} else {
|
517
|
+
full_path.join(".directory_listing.dir_list")
|
518
|
+
};
|
375
519
|
|
376
520
|
let cache_entry = CacheEntry::new_virtual_listing(
|
377
521
|
virtual_path.clone(),
|
378
|
-
&self.config
|
522
|
+
&self.config,
|
523
|
+
accept,
|
379
524
|
)
|
380
525
|
.await;
|
381
526
|
self.key_to_path
|
@@ -584,6 +729,8 @@ impl StaticFileServer {
|
|
584
729
|
|
585
730
|
build_file_response(
|
586
731
|
status,
|
732
|
+
None,
|
733
|
+
None,
|
587
734
|
get_mime_type(&file),
|
588
735
|
(end_idx - start) as usize,
|
589
736
|
last_modified,
|
@@ -593,6 +740,8 @@ impl StaticFileServer {
|
|
593
740
|
} else {
|
594
741
|
build_file_response(
|
595
742
|
status,
|
743
|
+
None,
|
744
|
+
None,
|
596
745
|
get_mime_type(&file),
|
597
746
|
content_length as usize,
|
598
747
|
last_modified,
|
@@ -614,11 +763,11 @@ impl StaticFileServer {
|
|
614
763
|
if_modified_since,
|
615
764
|
is_head_request,
|
616
765
|
path,
|
766
|
+
supported_encodings,
|
617
767
|
) = serve_cache_args;
|
618
768
|
|
619
769
|
let content_length = cache_entry.content.len() as u64;
|
620
770
|
|
621
|
-
// Handle If-Modified-Since header
|
622
771
|
if is_not_modified(cache_entry.last_modified, if_modified_since) {
|
623
772
|
return build_not_modified_response();
|
624
773
|
}
|
@@ -677,15 +826,20 @@ impl StaticFileServer {
|
|
677
826
|
return builder.body(BoxBody::new(Full::new(Bytes::new()))).unwrap();
|
678
827
|
}
|
679
828
|
|
680
|
-
// For GET requests, prepare the actual content
|
681
829
|
if is_range_request {
|
682
|
-
// Extract the requested range from the cached content
|
683
830
|
let start_idx = start as usize;
|
684
831
|
let end_idx = std::cmp::min((adjusted_end + 1) as usize, cache_entry.content.len());
|
685
832
|
let range_bytes = cache_entry.content.slice(start_idx..end_idx);
|
686
|
-
|
833
|
+
let etag = {
|
834
|
+
let mut hasher = Sha256::new();
|
835
|
+
hasher.update(&range_bytes);
|
836
|
+
let result = hasher.finalize();
|
837
|
+
general_purpose::STANDARD.encode(result)
|
838
|
+
};
|
687
839
|
build_file_response(
|
688
840
|
status,
|
841
|
+
None,
|
842
|
+
Some(&etag),
|
689
843
|
get_mime_type(path),
|
690
844
|
range_bytes.len(),
|
691
845
|
cache_entry.last_modified,
|
@@ -694,10 +848,12 @@ impl StaticFileServer {
|
|
694
848
|
)
|
695
849
|
} else {
|
696
850
|
// Return the full content
|
697
|
-
let
|
698
|
-
let body = build_ok_body(
|
851
|
+
let (content, encoding) = cache_entry.suggest_content_for(supported_encodings);
|
852
|
+
let body = build_ok_body(content);
|
699
853
|
build_file_response(
|
700
854
|
status,
|
855
|
+
Some(encoding),
|
856
|
+
Some(&cache_entry.etag),
|
701
857
|
get_mime_type(path),
|
702
858
|
content_length as usize,
|
703
859
|
cache_entry.last_modified,
|
@@ -728,38 +884,31 @@ async fn read_entire_file(path: &Path) -> std::io::Result<(Bytes, SystemTime)> {
|
|
728
884
|
Ok((Bytes::from(buf), last_modified))
|
729
885
|
}
|
730
886
|
|
731
|
-
fn
|
732
|
-
|
887
|
+
fn with_added_extension(path: &Path, ext: &str) -> PathBuf {
|
888
|
+
let mut new_path = path.to_path_buf();
|
889
|
+
if new_path.file_name().is_some() {
|
890
|
+
// Append the dot and extension in place.
|
891
|
+
new_path.as_mut_os_string().push(".");
|
892
|
+
new_path.as_mut_os_string().push(ext);
|
893
|
+
}
|
894
|
+
new_path
|
733
895
|
}
|
734
896
|
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
Some("jpg") | Some("jpeg") => "image/jpeg",
|
745
|
-
Some("gif") => "image/gif",
|
746
|
-
Some("svg") => "image/svg+xml",
|
747
|
-
Some("ico") => "image/x-icon",
|
748
|
-
Some("webp") => "image/webp",
|
749
|
-
Some("pdf") => "application/pdf",
|
750
|
-
Some("xml") => "application/xml",
|
751
|
-
Some("zip") => "application/zip",
|
752
|
-
Some("gz") => "application/gzip",
|
753
|
-
Some("mp3") => "audio/mpeg",
|
754
|
-
Some("mp4") => "video/mp4",
|
755
|
-
Some("webm") => "video/webm",
|
756
|
-
Some("woff") => "font/woff",
|
757
|
-
Some("woff2") => "font/woff2",
|
758
|
-
Some("ttf") => "font/ttf",
|
759
|
-
Some("otf") => "font/otf",
|
760
|
-
Some("dir_list") => "text/html",
|
761
|
-
_ => "application/octet-stream",
|
897
|
+
async fn read_variant(path: &Path, ext: &str) -> Option<Bytes> {
|
898
|
+
let variant = with_added_extension(path, ext);
|
899
|
+
if let Ok(metadata) = tokio::fs::metadata(&variant).await {
|
900
|
+
if let Ok(mut file) = File::open(&variant).await {
|
901
|
+
let mut buf = Vec::with_capacity(metadata.len().try_into().unwrap_or(4096));
|
902
|
+
if file.read_to_end(&mut buf).await.is_ok() {
|
903
|
+
return Some(Bytes::from(buf));
|
904
|
+
}
|
905
|
+
}
|
762
906
|
}
|
907
|
+
None
|
908
|
+
}
|
909
|
+
|
910
|
+
fn build_ok_body(bytes: Arc<Bytes>) -> BoxBody<Bytes, Infallible> {
|
911
|
+
BoxBody::new(Full::new(bytes.as_ref().clone()))
|
763
912
|
}
|
764
913
|
|
765
914
|
// Helper function to handle not modified responses
|
@@ -770,9 +919,11 @@ fn build_not_modified_response() -> http::Response<BoxBody<Bytes, Infallible>> {
|
|
770
919
|
.unwrap()
|
771
920
|
}
|
772
921
|
|
773
|
-
|
922
|
+
#[allow(clippy::too_many_arguments)]
|
774
923
|
fn build_file_response(
|
775
924
|
status: StatusCode,
|
925
|
+
content_encoding: Option<&str>,
|
926
|
+
etag: Option<&str>,
|
776
927
|
content_type: &str,
|
777
928
|
content_length: usize,
|
778
929
|
last_modified: SystemTime,
|
@@ -781,12 +932,20 @@ fn build_file_response(
|
|
781
932
|
) -> http::Response<BoxBody<Bytes, Infallible>> {
|
782
933
|
let mut builder = Response::builder()
|
783
934
|
.status(status)
|
784
|
-
.header(
|
785
|
-
.header(
|
786
|
-
.header(
|
935
|
+
.header(CONTENT_TYPE, content_type)
|
936
|
+
.header(CONTENT_LENGTH, content_length)
|
937
|
+
.header(LAST_MODIFIED, format_http_date(last_modified));
|
938
|
+
|
939
|
+
if let Some(etag) = etag {
|
940
|
+
builder = builder.header(ETAG, etag);
|
941
|
+
}
|
942
|
+
|
943
|
+
if let Some(content_encoding) = content_encoding {
|
944
|
+
builder = builder.header(CONTENT_ENCODING, content_encoding);
|
945
|
+
}
|
787
946
|
|
788
947
|
if let Some(range) = range_header {
|
789
|
-
builder = builder.header(
|
948
|
+
builder = builder.header(CONTENT_RANGE, range);
|
790
949
|
}
|
791
950
|
|
792
951
|
builder.body(body).unwrap()
|
@@ -802,7 +961,7 @@ fn is_not_modified(last_modified: SystemTime, if_modified_since: Option<SystemTi
|
|
802
961
|
false
|
803
962
|
}
|
804
963
|
|
805
|
-
fn normalize_path(path:
|
964
|
+
fn normalize_path(path: Cow<'_, str>) -> Option<PathBuf> {
|
806
965
|
let mut normalized = PathBuf::new();
|
807
966
|
let path = path.trim_start_matches('/');
|
808
967
|
|
@@ -849,136 +1008,317 @@ impl Default for StaticFileServer {
|
|
849
1008
|
recheck_interval: Duration::from_secs(60),
|
850
1009
|
try_html_extension: true,
|
851
1010
|
auto_index: true,
|
852
|
-
not_found_behavior: NotFoundBehavior::Error(ErrorResponse::
|
1011
|
+
not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
|
1012
|
+
serve_hidden_files: false,
|
1013
|
+
allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
|
853
1014
|
};
|
854
1015
|
Self::new(config)
|
855
1016
|
}
|
856
1017
|
}
|
857
1018
|
|
858
|
-
async fn generate_directory_listing(
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
html.push_str("</h1>\n");
|
890
|
-
|
891
|
-
html.push_str("<table>\n");
|
892
|
-
html.push_str("<tr><th>Name</th><th>Size</th><th>Last Modified</th></tr>\n");
|
893
|
-
|
894
|
-
// Add parent directory link if not in root
|
895
|
-
|
896
|
-
if dir_path != root_dir {
|
897
|
-
info!("{} != {}", dir_path.display(), root_dir.display());
|
898
|
-
html.push_str("<tr><td><a href=\"..\">..</a></td><td class=\"size\">-</td><td class=\"date\">-</td></tr>\n");
|
899
|
-
}
|
1019
|
+
async fn generate_directory_listing(
|
1020
|
+
dir_path: &Path,
|
1021
|
+
config: &StaticFileServerConfig,
|
1022
|
+
accept: ResponseFormat,
|
1023
|
+
) -> std::io::Result<String> {
|
1024
|
+
match accept {
|
1025
|
+
ResponseFormat::JSON => {
|
1026
|
+
let directory_display = {
|
1027
|
+
let display = dir_path
|
1028
|
+
.strip_prefix(&config.root_dir)
|
1029
|
+
.unwrap_or(Path::new(""))
|
1030
|
+
.to_string_lossy();
|
1031
|
+
if display.is_empty() {
|
1032
|
+
Cow::Borrowed(".")
|
1033
|
+
} else {
|
1034
|
+
display
|
1035
|
+
}
|
1036
|
+
};
|
1037
|
+
|
1038
|
+
let mut items = Vec::new();
|
1039
|
+
|
1040
|
+
// Add a parent directory entry if not at the root.
|
1041
|
+
if dir_path != config.root_dir {
|
1042
|
+
items.push(json!({
|
1043
|
+
"name": "..",
|
1044
|
+
"path": "..",
|
1045
|
+
"is_dir": true,
|
1046
|
+
"size": null,
|
1047
|
+
"modified": null,
|
1048
|
+
}));
|
1049
|
+
}
|
900
1050
|
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
1051
|
+
// Read directory entries.
|
1052
|
+
let mut entries = tokio::fs::read_dir(dir_path).await?;
|
1053
|
+
let mut dirs = Vec::new();
|
1054
|
+
let mut files = Vec::new();
|
1055
|
+
|
1056
|
+
while let Some(entry) = entries.next_entry().await? {
|
1057
|
+
let entry_path = entry.path();
|
1058
|
+
let metadata = entry.metadata().await?;
|
1059
|
+
let name = entry_path
|
1060
|
+
.file_name()
|
1061
|
+
.unwrap()
|
1062
|
+
.to_string_lossy()
|
1063
|
+
.into_owned();
|
1064
|
+
|
1065
|
+
if !config.serve_hidden_files && name.starts_with('.') {
|
1066
|
+
continue;
|
1067
|
+
}
|
905
1068
|
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
1069
|
+
let ext = entry_path
|
1070
|
+
.extension()
|
1071
|
+
.and_then(|s| s.to_str())
|
1072
|
+
.unwrap_or("");
|
1073
|
+
|
1074
|
+
if metadata.is_dir() {
|
1075
|
+
dirs.push((name, metadata));
|
1076
|
+
} else if config.allowed_extensions.is_empty()
|
1077
|
+
|| config.allowed_extensions.iter().any(|e| e == ext)
|
1078
|
+
{
|
1079
|
+
files.push((name, metadata));
|
1080
|
+
}
|
1081
|
+
}
|
910
1082
|
|
911
|
-
|
912
|
-
dirs.
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
1083
|
+
// Sort directories alphabetically with dot directories pushed to the bottom.
|
1084
|
+
dirs.sort_by(|(name_a, _), (name_b, _)| {
|
1085
|
+
let a_is_dot = name_a.starts_with('.');
|
1086
|
+
let b_is_dot = name_b.starts_with('.');
|
1087
|
+
if a_is_dot != b_is_dot {
|
1088
|
+
if a_is_dot {
|
1089
|
+
Ordering::Greater
|
1090
|
+
} else {
|
1091
|
+
Ordering::Less
|
1092
|
+
}
|
1093
|
+
} else {
|
1094
|
+
name_a.cmp(name_b)
|
1095
|
+
}
|
1096
|
+
});
|
917
1097
|
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
if let Ok(modified) = metadata.modified() {
|
933
|
-
let formatted_time = DateTime::<Utc>::from(modified)
|
934
|
-
.format("%Y-%m-%d %H:%M:%S")
|
935
|
-
.to_string();
|
936
|
-
html.push_str(&format!("<td class=\"date\">{}</td>", formatted_time));
|
937
|
-
} else {
|
938
|
-
html.push_str("<td class=\"date\">-</td>");
|
939
|
-
}
|
1098
|
+
// Sort files so that dot files appear last.
|
1099
|
+
files.sort_by(|(name_a, _), (name_b, _)| {
|
1100
|
+
let a_is_dot = name_a.starts_with('.');
|
1101
|
+
let b_is_dot = name_b.starts_with('.');
|
1102
|
+
if a_is_dot != b_is_dot {
|
1103
|
+
if a_is_dot {
|
1104
|
+
Ordering::Greater
|
1105
|
+
} else {
|
1106
|
+
Ordering::Less
|
1107
|
+
}
|
1108
|
+
} else {
|
1109
|
+
name_a.cmp(name_b)
|
1110
|
+
}
|
1111
|
+
});
|
940
1112
|
|
941
|
-
|
942
|
-
|
1113
|
+
// Generate JSON entries for directories.
|
1114
|
+
for (name, metadata) in dirs {
|
1115
|
+
let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
|
1116
|
+
let modified = metadata
|
1117
|
+
.modified()
|
1118
|
+
.ok()
|
1119
|
+
.map(|m| {
|
1120
|
+
DateTime::<Utc>::from(m)
|
1121
|
+
.format("%Y-%m-%d %H:%M:%S")
|
1122
|
+
.to_string()
|
1123
|
+
})
|
1124
|
+
.unwrap_or_else(|| "-".to_string());
|
1125
|
+
|
1126
|
+
items.push(json!({
|
1127
|
+
"name": format!("{}/", name),
|
1128
|
+
"path": format!("{}/", encoded),
|
1129
|
+
"is_dir": true,
|
1130
|
+
"size": null,
|
1131
|
+
"modified": modified,
|
1132
|
+
}));
|
1133
|
+
}
|
943
1134
|
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
} else if file_size < 1024 * 1024 {
|
958
|
-
format!("{:.1} KB", file_size as f64 / 1024.0)
|
959
|
-
} else if file_size < 1024 * 1024 * 1024 {
|
960
|
-
format!("{:.1} MB", file_size as f64 / (1024.0 * 1024.0))
|
961
|
-
} else {
|
962
|
-
format!("{:.1} GB", file_size as f64 / (1024.0 * 1024.0 * 1024.0))
|
963
|
-
};
|
1135
|
+
// Generate JSON entries for files.
|
1136
|
+
for (name, metadata) in files {
|
1137
|
+
let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
|
1138
|
+
let file_size = metadata.len();
|
1139
|
+
let formatted_size = if file_size < 1024 {
|
1140
|
+
format!("{} B", file_size)
|
1141
|
+
} else if file_size < 1024 * 1024 {
|
1142
|
+
format!("{:.1} KB", file_size as f64 / 1024.0)
|
1143
|
+
} else if file_size < 1024 * 1024 * 1024 {
|
1144
|
+
format!("{:.1} MB", file_size as f64 / (1024.0 * 1024.0))
|
1145
|
+
} else {
|
1146
|
+
format!("{:.1} GB", file_size as f64 / (1024.0 * 1024.0 * 1024.0))
|
1147
|
+
};
|
964
1148
|
|
965
|
-
|
1149
|
+
let modified_str = metadata
|
1150
|
+
.modified()
|
1151
|
+
.ok()
|
1152
|
+
.map(|m| {
|
1153
|
+
DateTime::<Utc>::from(m)
|
1154
|
+
.format("%Y-%m-%d %H:%M:%S")
|
1155
|
+
.to_string()
|
1156
|
+
})
|
1157
|
+
.unwrap_or_else(|| "-".to_string());
|
1158
|
+
|
1159
|
+
items.push(json!({
|
1160
|
+
"name": name,
|
1161
|
+
"path": encoded,
|
1162
|
+
"is_dir": false,
|
1163
|
+
"size": formatted_size,
|
1164
|
+
"modified": modified_str,
|
1165
|
+
}));
|
1166
|
+
}
|
966
1167
|
|
967
|
-
|
968
|
-
let
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
1168
|
+
// Build the final JSON object.
|
1169
|
+
let json_obj = json!({
|
1170
|
+
"title": format!("Directory listing for {}", directory_display),
|
1171
|
+
"directory": directory_display,
|
1172
|
+
"items": items,
|
1173
|
+
});
|
1174
|
+
|
1175
|
+
// Serialize the JSON object to a pretty-printed string.
|
1176
|
+
let json_string = serde_json::to_string_pretty(&json_obj)
|
1177
|
+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
1178
|
+
|
1179
|
+
Ok(json_string)
|
974
1180
|
}
|
1181
|
+
ResponseFormat::HTML | ResponseFormat::TEXT | ResponseFormat::UNKNOWN => {
|
1182
|
+
let template = include_str!("../default_responses/html/index.html");
|
1183
|
+
|
1184
|
+
let directory_display = {
|
1185
|
+
let display = dir_path
|
1186
|
+
.strip_prefix(&config.root_dir)
|
1187
|
+
.unwrap_or(Path::new(""))
|
1188
|
+
.to_string_lossy();
|
1189
|
+
if display.is_empty() {
|
1190
|
+
Cow::Borrowed(".")
|
1191
|
+
} else {
|
1192
|
+
display
|
1193
|
+
}
|
1194
|
+
};
|
1195
|
+
|
1196
|
+
let mut rows = String::new();
|
1197
|
+
if dir_path != config.root_dir {
|
1198
|
+
rows.push_str(
|
1199
|
+
r#"<tr><td><a href="..">..</a></td><td class="size">-</td><td class="date">-</td></tr>"#,
|
1200
|
+
);
|
1201
|
+
rows.push('\n');
|
1202
|
+
}
|
975
1203
|
|
976
|
-
|
977
|
-
|
1204
|
+
// Read directory entries.
|
1205
|
+
let mut entries = tokio::fs::read_dir(dir_path).await?;
|
1206
|
+
let mut dirs = Vec::new();
|
1207
|
+
let mut files = Vec::new();
|
1208
|
+
|
1209
|
+
while let Some(entry) = entries.next_entry().await? {
|
1210
|
+
let entry_path = entry.path();
|
1211
|
+
let metadata = entry.metadata().await?;
|
1212
|
+
let name = entry_path
|
1213
|
+
.file_name()
|
1214
|
+
.unwrap()
|
1215
|
+
.to_string_lossy()
|
1216
|
+
.into_owned();
|
1217
|
+
|
1218
|
+
if !config.serve_hidden_files && name.starts_with('.') {
|
1219
|
+
continue;
|
1220
|
+
}
|
1221
|
+
|
1222
|
+
let ext = entry_path
|
1223
|
+
.extension()
|
1224
|
+
.and_then(|s| s.to_str())
|
1225
|
+
.unwrap_or("");
|
1226
|
+
|
1227
|
+
if metadata.is_dir() {
|
1228
|
+
dirs.push((name, metadata));
|
1229
|
+
} else if config.allowed_extensions.is_empty()
|
1230
|
+
|| config.allowed_extensions.iter().any(|e| e == ext)
|
1231
|
+
{
|
1232
|
+
files.push((name, metadata));
|
1233
|
+
}
|
1234
|
+
}
|
978
1235
|
|
979
|
-
|
980
|
-
|
981
|
-
|
1236
|
+
// Sort directories and files alphabetically.
|
1237
|
+
dirs.sort_by(|(name_a, _), (name_b, _)| {
|
1238
|
+
let a_is_dot = name_a.starts_with('.');
|
1239
|
+
let b_is_dot = name_b.starts_with('.');
|
1240
|
+
if a_is_dot != b_is_dot {
|
1241
|
+
if a_is_dot {
|
1242
|
+
Ordering::Greater
|
1243
|
+
} else {
|
1244
|
+
Ordering::Less
|
1245
|
+
}
|
1246
|
+
} else {
|
1247
|
+
name_a.cmp(name_b)
|
1248
|
+
}
|
1249
|
+
});
|
982
1250
|
|
983
|
-
|
1251
|
+
// Sort files so that dot files are at the bottom.
|
1252
|
+
files.sort_by(|(name_a, _), (name_b, _)| {
|
1253
|
+
let a_is_dot = name_a.starts_with('.');
|
1254
|
+
let b_is_dot = name_b.starts_with('.');
|
1255
|
+
if a_is_dot != b_is_dot {
|
1256
|
+
if a_is_dot {
|
1257
|
+
Ordering::Greater
|
1258
|
+
} else {
|
1259
|
+
Ordering::Less
|
1260
|
+
}
|
1261
|
+
} else {
|
1262
|
+
name_a.cmp(name_b)
|
1263
|
+
}
|
1264
|
+
});
|
1265
|
+
|
1266
|
+
// Generate rows for directories.
|
1267
|
+
for (name, metadata) in dirs {
|
1268
|
+
let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
|
1269
|
+
|
1270
|
+
rows.push_str(&format!(
|
1271
|
+
r#"<tr><td><a href="{0}/">{1}/</a></td><td class="size">-</td><td class="date">{2}</td></tr>"#,
|
1272
|
+
encoded,
|
1273
|
+
name,
|
1274
|
+
metadata.modified().ok().map(|m| DateTime::<Utc>::from(m).format("%Y-%m-%d %H:%M:%S").to_string())
|
1275
|
+
.unwrap_or_else(|| "-".to_string())
|
1276
|
+
));
|
1277
|
+
rows.push('\n');
|
1278
|
+
}
|
1279
|
+
|
1280
|
+
// Generate rows for files.
|
1281
|
+
for (name, metadata) in files {
|
1282
|
+
let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
|
1283
|
+
|
1284
|
+
let file_size = metadata.len();
|
1285
|
+
let formatted_size = if file_size < 1024 {
|
1286
|
+
format!("{} B", file_size)
|
1287
|
+
} else if file_size < 1024 * 1024 {
|
1288
|
+
format!("{:.1} KB", file_size as f64 / 1024.0)
|
1289
|
+
} else if file_size < 1024 * 1024 * 1024 {
|
1290
|
+
format!("{:.1} MB", file_size as f64 / (1024.0 * 1024.0))
|
1291
|
+
} else {
|
1292
|
+
format!("{:.1} GB", file_size as f64 / (1024.0 * 1024.0 * 1024.0))
|
1293
|
+
};
|
1294
|
+
|
1295
|
+
let modified_str = metadata
|
1296
|
+
.modified()
|
1297
|
+
.ok()
|
1298
|
+
.map(|m| {
|
1299
|
+
DateTime::<Utc>::from(m)
|
1300
|
+
.format("%Y-%m-%d %H:%M:%S")
|
1301
|
+
.to_string()
|
1302
|
+
})
|
1303
|
+
.unwrap_or_else(|| "-".to_string());
|
1304
|
+
|
1305
|
+
rows.push_str(&format!(
|
1306
|
+
r#"<tr><td><a href="{0}">{1}</a></td><td class="size">{2}</td><td class="date">{3}</td></tr>"#,
|
1307
|
+
encoded, name, formatted_size, modified_str
|
1308
|
+
));
|
1309
|
+
rows.push('\n');
|
1310
|
+
}
|
1311
|
+
|
1312
|
+
// Replace the placeholders in our template.
|
1313
|
+
let html = template
|
1314
|
+
.replace(
|
1315
|
+
"{{title}}",
|
1316
|
+
&format!("Directory listing for {}", directory_display),
|
1317
|
+
)
|
1318
|
+
.replace("{{directory}}", &directory_display)
|
1319
|
+
.replace("{{rows}}", &rows);
|
1320
|
+
|
1321
|
+
Ok(html)
|
1322
|
+
}
|
1323
|
+
}
|
984
1324
|
}
|