1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use axum::{middleware, routing::get, Router};
use axum_extra::routing::RouterExt;
use axum_server::tls_rustls::RustlsConfig;
use tokio::net::TcpListener;
use tracing::{info, level_filters::LevelFilter};
use crate::{
acl::init_acl,
auth::init_auth,
context::ServerRuntimeContext,
error::{AppResult, ErrorKind},
handlers::{
file_config::{add_config, delete_config, get_config, has_config},
file_exchange::{add_file, delete_file, get_file},
file_length::file_length,
files_list::list_files,
health::{init_start_time, live_check},
repository::{create_repository, delete_repository},
},
log::print_request_response,
storage::{init_storage, Storage},
typed_path::{RepositoryConfigPath, RepositoryPath, RepositoryTpeNamePath, RepositoryTpePath},
};
/// Start the web server
///
/// # Arguments
///
/// * `runtime_ctx` - The server runtime context
pub async fn start_web_server<S>(runtime_ctx: ServerRuntimeContext<S>) -> AppResult<()>
where
S: Storage + Clone + std::fmt::Debug,
{
let ServerRuntimeContext {
socket_address,
acl,
auth,
storage,
tls,
..
} = runtime_ctx;
init_start_time();
init_acl(acl)?;
init_auth(auth)?;
init_storage(storage)?;
let mut app = Router::new();
// /health/live
//
// Liveness probe. This is used to check if the server is running.
// Returns “200 OK” if the server is running.
app = app.route("/health/live", get(live_check));
// /health/ready
//
// Readiness probe. This is used to check if the server is ready to accept requests.
// Returns “200 OK” if the server is ready to accept requests.
// app = app.route("/health/ready", get(ready_check));
// /:repo/:tpe/:name
app = app
// Returns “200 OK” if the blob with the given name and type is stored in the repository,
// “404 not found” otherwise. If the blob exists, the HTTP header Content-Length
// is set to the file size.
.typed_head(file_length::<RepositoryTpeNamePath>)
// Returns the content of the blob with the given name and type if it is stored
// in the repository, “404 not found” otherwise.
// If the request specifies a partial read with a Range header field, then the
// status code of the response is 206 instead of 200 and the response only contains
// the specified range.
//
// Response format: binary/octet-stream
.typed_get(get_file::<RepositoryTpeNamePath>)
// Saves the content of the request body as a blob with the given name and type,
// an HTTP error otherwise.
//
// Request format: binary/octet-stream
.typed_post(add_file::<RepositoryTpeNamePath>)
// Returns “200 OK” if the blob with the given name and type has been deleted from
// the repository, an HTTP error otherwise.
.typed_delete(delete_file::<RepositoryTpeNamePath>);
// /:repo/config
app = app
// Returns “200 OK” if the repository has a configuration, an HTTP error otherwise.
.typed_head(has_config)
// Returns the content of the configuration file if the repository has a configuration,
// an HTTP error otherwise.
//
// Response format: binary/octet-stream
.typed_get(get_config::<RepositoryConfigPath>)
// Returns “200 OK” if the configuration of the request body has been saved,
// an HTTP error otherwise.
.typed_post(add_config::<RepositoryConfigPath>)
// Returns “200 OK” if the configuration of the repository has been deleted,
// an HTTP error otherwise.
// Note: This is not part of the API documentation, but it is implemented
// to allow for the deletion of the configuration file during testing.
.typed_delete(delete_config::<RepositoryConfigPath>);
// /:repo/:tpe/
// # API version 1
//
// Returns a JSON array containing the names of all the blobs stored for a given type, example:
//
// ```json
// [
// "245bc4c430d393f74fbe7b13325e30dbde9fb0745e50caad57c446c93d20096b",
// "85b420239efa1132c41cea0065452a40ebc20c6f8e0b132a5b2f5848360973ec",
// "8e2006bb5931a520f3c7009fe278d1ebb87eb72c3ff92a50c30e90f1b8cf3e60",
// "e75c8c407ea31ba399ab4109f28dd18c4c68303d8d86cc275432820c42ce3649"
// ]
// ```
//
// # API version 2
//
// Returns a JSON array containing an object for each file of the given type.
// The objects have two keys: name for the file name, and size for the size in bytes.
//
// [
// {
// "name": "245bc4c430d393f74fbe7b13325e30dbde9fb0745e50caad57c446c93d20096b",
// "size": 2341058
// },
// {
// "name": "85b420239efa1132c41cea0065452a40ebc20c6f8e0b132a5b2f5848360973ec",
// "size": 2908900
// },
// {
// "name": "8e2006bb5931a520f3c7009fe278d1ebb87eb72c3ff92a50c30e90f1b8cf3e60",
// "size": 3030712
// },
// {
// "name": "e75c8c407ea31ba399ab4109f28dd18c4c68303d8d86cc275432820c42ce3649",
// "size": 2804
// }
// ]
app = app.typed_get(list_files::<RepositoryTpePath>);
// /:repo/ --> note: trailing slash
app = app
// This request is used to initially create a new repository.
// The server responds with “200 OK” if the repository structure was created
// successfully or already exists, otherwise an error is returned.
.typed_post(create_repository::<RepositoryPath>)
// Deletes the repository on the server side. The server responds with “200 OK”
// if the repository was successfully removed. If this function is not implemented
// the server returns “501 Not Implemented”, if this it is denied by the server it
// returns “403 Forbidden”.
.typed_delete(delete_repository::<RepositoryPath>);
// TODO: This is not reflected in the API documentation?
// TODO: Decide if we want to keep this or not!
// // /:tpe/:name
// // we loop here over explicit types, to prevent conflict with paths "/:repo/:tpe"
// for tpe in constants::TYPES.into_iter() {
// let path = format!("/{}:name", &tpe);
// app = app
// .route(path.as_str(), head(file_length::<TpeNamePath>))
// .route(path.as_str(), get(get_file::<TpeNamePath>))
// .route(path.as_str(), post(add_file::<TpeNamePath>))
// .route(path.as_str(), delete(delete_file::<TpeNamePath>));
// }
//
// /:tpe --> note: NO trailing slash
// we loop here over explicit types, to prevent the conflict with paths "/:repo/"
// for tpe in constants::TYPES.into_iter() {
// let path = format!("/{}", &tpe);
// app = app.route(path.as_str(), get(list_files::<TpePath>));
// }
// Extra logging requested. Handlers will log too
// TODO: Use LogSettings here, this should be set from the cli by `--log`
// TODO: and then needs to go to a file
// e.g. log_opts.is_disabled() or other checks
match LevelFilter::current() {
LevelFilter::TRACE | LevelFilter::DEBUG | LevelFilter::INFO => {
app = app.layer(middleware::from_fn(print_request_response));
}
_ => {}
};
info!("Starting web server ...");
if let Some(tls) = tls {
// Start server with or without TLS
let config = RustlsConfig::from_pem_file(tls.tls_cert, tls.tls_key)
.await
.map_err(|err|
ErrorKind::Io.context(
format!("Failed to load TLS certificate/key. Please make sure the paths are correct. `{err}`")
)
)?;
info!("Listening on: `https://{socket_address}`");
axum_server::bind_rustls(socket_address, config)
.serve(app.into_make_service())
.await
.expect("Failed to start server. Is the address already in use?");
} else {
info!("Listening on: `http://{socket_address}`");
axum::serve(
TcpListener::bind(socket_address)
.await
.expect("Failed to bind to socket. Please make sure the address is correct."),
app.into_make_service(),
)
.await
.expect("Failed to start server. Is the address already in use?");
};
Ok(())
}