Commit
+303 -96 +/-3 browse
1 | diff --git a/src/lib.rs b/src/lib.rs |
2 | new file mode 100644 |
3 | index 0000000..df3ea7f |
4 | --- /dev/null |
5 | +++ b/src/lib.rs |
6 | @@ -0,0 +1,265 @@ |
7 | + // Copyright (c) 2021 Jason White |
8 | + // |
9 | + // Permission is hereby granted, free of charge, to any person obtaining a copy |
10 | + // of this software and associated documentation files (the "Software"), to deal |
11 | + // in the Software without restriction, including without limitation the rights |
12 | + // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
13 | + // copies of the Software, and to permit persons to whom the Software is |
14 | + // furnished to do so, subject to the following conditions: |
15 | + // |
16 | + // The above copyright notice and this permission notice shall be included in |
17 | + // all copies or substantial portions of the Software. |
18 | + // |
19 | + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
20 | + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
21 | + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
22 | + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
23 | + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
24 | + // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
25 | + // SOFTWARE. |
26 | + mod app; |
27 | + mod error; |
28 | + mod hyperext; |
29 | + mod lfs; |
30 | + mod logger; |
31 | + mod lru; |
32 | + mod sha256; |
33 | + mod storage; |
34 | + mod util; |
35 | + |
36 | + use std::convert::Infallible; |
37 | + use std::net::SocketAddr; |
38 | + use std::path::PathBuf; |
39 | + use std::sync::Arc; |
40 | + |
41 | + use futures::future::{self, Future, TryFutureExt}; |
42 | + use hyper::{ |
43 | + self, |
44 | + server::conn::{AddrIncoming, AddrStream}, |
45 | + service::make_service_fn, |
46 | + }; |
47 | + |
48 | + use crate::app::App; |
49 | + use crate::error::Error; |
50 | + use crate::logger::Logger; |
51 | + use crate::storage::{Cached, Disk, Encrypted, Retrying, Storage, Verify, S3}; |
52 | + |
53 | + #[cfg(feature = "faulty")] |
54 | + use crate::storage::Faulty; |
55 | + |
56 | + /// Represents a running LFS server. |
57 | + pub trait Server: Future<Output = hyper::Result<()>> { |
58 | + /// Returns the local address this server is bound to. |
59 | + fn addr(&self) -> SocketAddr; |
60 | + } |
61 | + |
62 | + impl<S, E> Server for hyper::Server<AddrIncoming, S, E> |
63 | + where |
64 | + hyper::Server<AddrIncoming, S, E>: Future<Output = hyper::Result<()>>, |
65 | + { |
66 | + fn addr(&self) -> SocketAddr { |
67 | + self.local_addr() |
68 | + } |
69 | + } |
70 | + |
71 | + #[derive(Debug)] |
72 | + pub struct Cache { |
73 | + /// Path to the cache. |
74 | + dir: PathBuf, |
75 | + |
76 | + /// Maximum size of the cache, in bytes. |
77 | + max_size: u64, |
78 | + } |
79 | + |
80 | + impl Cache { |
81 | + pub fn new(dir: PathBuf, max_size: u64) -> Self { |
82 | + Self { dir, max_size } |
83 | + } |
84 | + } |
85 | + |
86 | + #[derive(Debug)] |
87 | + pub struct S3ServerBuilder { |
88 | + bucket: String, |
89 | + key: [u8; 32], |
90 | + prefix: Option<String>, |
91 | + cdn: Option<String>, |
92 | + cache: Option<Cache>, |
93 | + } |
94 | + |
95 | + impl S3ServerBuilder { |
96 | + pub fn new(bucket: String, key: [u8; 32]) -> Self { |
97 | + Self { |
98 | + bucket, |
99 | + prefix: None, |
100 | + cdn: None, |
101 | + key, |
102 | + cache: None, |
103 | + } |
104 | + } |
105 | + |
106 | + /// Sets the bucket to use. |
107 | + pub fn bucket(&mut self, bucket: String) -> &mut Self { |
108 | + self.bucket = bucket; |
109 | + self |
110 | + } |
111 | + |
112 | + /// Sets the encryption key to use. |
113 | + pub fn key(&mut self, key: [u8; 32]) -> &mut Self { |
114 | + self.key = key; |
115 | + self |
116 | + } |
117 | + |
118 | + /// Sets the prefix to use. |
119 | + pub fn prefix(&mut self, prefix: String) -> &mut Self { |
120 | + self.prefix = Some(prefix); |
121 | + self |
122 | + } |
123 | + |
124 | + /// Sets the base URL of the CDN to use. This is incompatible with |
125 | + /// encryption since the LFS object is not sent to Rudolfs. |
126 | + pub fn cdn(&mut self, url: String) -> &mut Self { |
127 | + self.cdn = Some(url); |
128 | + self |
129 | + } |
130 | + |
131 | + /// Sets the cache to use. If not specified, then no local disk cache is |
132 | + /// used. All objects will get sent directly to S3. |
133 | + pub fn cache(&mut self, cache: Cache) -> &mut Self { |
134 | + self.cache = Some(cache); |
135 | + self |
136 | + } |
137 | + |
138 | + /// Spawns the server. The server must be awaited on in order to accept |
139 | + /// incoming client connections and run. |
140 | + pub async fn spawn( |
141 | + self, |
142 | + addr: SocketAddr, |
143 | + ) -> Result<Box<dyn Server + Unpin>, Box<dyn std::error::Error>> { |
144 | + let prefix = self.prefix.unwrap_or_else(|| String::from("lfs")); |
145 | + |
146 | + let s3 = S3::new(self.bucket, prefix, self.cdn) |
147 | + .map_err(Error::from) |
148 | + .await?; |
149 | + |
150 | + // Retry certain operations to S3 to make it more reliable. |
151 | + let s3 = Retrying::new(s3); |
152 | + |
153 | + // Add a little instability for testing purposes. |
154 | + #[cfg(feature = "faulty")] |
155 | + let s3 = Faulty::new(s3); |
156 | + |
157 | + match self.cache { |
158 | + Some(cache) => { |
159 | + // Use disk storage as a cache. |
160 | + let disk = Disk::new(cache.dir).map_err(Error::from).await?; |
161 | + |
162 | + #[cfg(feature = "faulty")] |
163 | + let disk = Faulty::new(disk); |
164 | + |
165 | + let cache = Cached::new(cache.max_size, disk, s3).await?; |
166 | + let storage = Verify::new(Encrypted::new(self.key, cache)); |
167 | + Ok(Box::new(spawn_server(storage, &addr))) |
168 | + } |
169 | + None => { |
170 | + let storage = Verify::new(Encrypted::new(self.key, s3)); |
171 | + Ok(Box::new(spawn_server(storage, &addr))) |
172 | + } |
173 | + } |
174 | + } |
175 | + |
176 | + /// Spawns the server and runs it to completion. This will run forever |
177 | + /// unless there is an error or the server shuts down gracefully. |
178 | + pub async fn run( |
179 | + self, |
180 | + addr: SocketAddr, |
181 | + ) -> Result<(), Box<dyn std::error::Error>> { |
182 | + let server = self.spawn(addr).await?; |
183 | + |
184 | + log::info!("Listening on {}", server.addr()); |
185 | + |
186 | + server.await?; |
187 | + Ok(()) |
188 | + } |
189 | + } |
190 | + |
191 | + #[derive(Debug)] |
192 | + pub struct LocalServerBuilder { |
193 | + path: PathBuf, |
194 | + key: [u8; 32], |
195 | + cache: Option<Cache>, |
196 | + } |
197 | + |
198 | + impl LocalServerBuilder { |
199 | + /// Creates a local server builder. `path` is the path to the folder where |
200 | + /// all of the LFS data will be stored. |
201 | + pub fn new(path: PathBuf, key: [u8; 32]) -> Self { |
202 | + Self { |
203 | + path, |
204 | + key, |
205 | + cache: None, |
206 | + } |
207 | + } |
208 | + |
209 | + /// Sets the encryption key to use. |
210 | + pub fn key(&mut self, key: [u8; 32]) -> &mut Self { |
211 | + self.key = key; |
212 | + self |
213 | + } |
214 | + |
215 | + /// Sets the cache to use. If not specified, then no local disk cache is |
216 | + /// used. It is uncommon to want to use this when the object storage is |
217 | + /// already local. However, a cache may be useful when the data storage path |
218 | + /// is on a mounted network file system. In such a case, the network file |
219 | + /// system could be slow and the local disk storage could be fast. |
220 | + pub fn cache(&mut self, cache: Cache) -> &mut Self { |
221 | + self.cache = Some(cache); |
222 | + self |
223 | + } |
224 | + |
225 | + /// Spawns the server. The server must be awaited on in order to accept |
226 | + /// incoming client connections and run. |
227 | + pub async fn spawn( |
228 | + self, |
229 | + addr: SocketAddr, |
230 | + ) -> Result<impl Server, Box<dyn std::error::Error>> { |
231 | + let storage = Disk::new(self.path).map_err(Error::from).await?; |
232 | + let storage = Verify::new(Encrypted::new(self.key, storage)); |
233 | + |
234 | + log::info!("Local disk storage initialized."); |
235 | + |
236 | + Ok(spawn_server(storage, &addr)) |
237 | + } |
238 | + |
239 | + /// Spawns the server and runs it to completion. This will run forever |
240 | + /// unless there is an error or the server shuts down gracefully. |
241 | + pub async fn run( |
242 | + self, |
243 | + addr: SocketAddr, |
244 | + ) -> Result<(), Box<dyn std::error::Error>> { |
245 | + let server = self.spawn(addr).await?; |
246 | + |
247 | + log::info!("Listening on {}", server.addr()); |
248 | + |
249 | + server.await?; |
250 | + Ok(()) |
251 | + } |
252 | + } |
253 | + |
254 | + fn spawn_server<S>(storage: S, addr: &SocketAddr) -> impl Server |
255 | + where |
256 | + S: Storage + Send + Sync + 'static, |
257 | + S::Error: Into<Error>, |
258 | + Error: From<S::Error>, |
259 | + { |
260 | + let storage = Arc::new(storage); |
261 | + |
262 | + let new_service = make_service_fn(move |socket: &AddrStream| { |
263 | + // Create our app. |
264 | + let service = App::new(storage.clone()); |
265 | + |
266 | + // Add logging middleware |
267 | + future::ok::<_, Infallible>(Logger::new(socket.remote_addr(), service)) |
268 | + }); |
269 | + |
270 | + hyper::Server::bind(&addr).serve(new_service) |
271 | + } |
272 | diff --git a/src/main.rs b/src/main.rs |
273 | index 1975421..f511920 100644 |
274 | --- a/src/main.rs |
275 | +++ b/src/main.rs |
276 | @@ -1,4 +1,4 @@ |
277 | - // Copyright (c) 2019 Jason White |
278 | + // Copyright (c) 2021 Jason White |
279 | // |
280 | // Permission is hereby granted, free of charge, to any person obtaining a copy |
281 | // of this software and associated documentation files (the "Software"), to deal |
282 | @@ -17,30 +17,13 @@ |
283 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
284 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
285 | // SOFTWARE. |
286 | - mod app; |
287 | - mod error; |
288 | - mod hyperext; |
289 | - mod lfs; |
290 | - mod logger; |
291 | - mod lru; |
292 | - mod sha256; |
293 | - mod storage; |
294 | - mod util; |
295 | - |
296 | - use std::convert::Infallible; |
297 | use std::net::{SocketAddr, ToSocketAddrs}; |
298 | use std::path::PathBuf; |
299 | - use std::sync::Arc; |
300 | |
301 | - use futures::future::{self, TryFutureExt}; |
302 | use hex::FromHex; |
303 | - use hyper::{self, server::conn::AddrStream, service::make_service_fn, Server}; |
304 | use structopt::StructOpt; |
305 | |
306 | - use crate::app::App; |
307 | - use crate::error::Error; |
308 | - use crate::logger::Logger; |
309 | - use crate::storage::{Cached, Disk, Encrypted, Retrying, Storage, Verify, S3}; |
310 | + use rudolfs::{Cache, LocalServerBuilder, S3ServerBuilder}; |
311 | |
312 | #[cfg(feature = "faulty")] |
313 | use crate::storage::Faulty; |
314 | @@ -176,43 +159,22 @@ impl S3Args { |
315 | addr: SocketAddr, |
316 | global_args: GlobalArgs, |
317 | ) -> Result<(), Box<dyn std::error::Error>> { |
318 | - let s3 = S3::new(self.bucket, self.prefix, self.cdn) |
319 | - .map_err(Error::from) |
320 | - .await?; |
321 | - |
322 | - // Retry certain operations to S3 to make it more reliable. |
323 | - let s3 = Retrying::new(s3); |
324 | - |
325 | - // Add a little instability for testing purposes. |
326 | - #[cfg(feature = "faulty")] |
327 | - let s3 = Faulty::new(s3); |
328 | - |
329 | - match global_args.cache_dir { |
330 | - Some(cache_dir) => { |
331 | - // Convert cache size to bytes. |
332 | - let max_cache_size = global_args |
333 | - .max_cache_size |
334 | - .into::<human_size::Byte>() |
335 | - .value() as u64; |
336 | - |
337 | - // Use disk storage as a cache. |
338 | - let disk = Disk::new(cache_dir).map_err(Error::from).await?; |
339 | - |
340 | - #[cfg(feature = "faulty")] |
341 | - let disk = Faulty::new(disk); |
342 | - |
343 | - let cache = Cached::new(max_cache_size, disk, s3).await?; |
344 | - let storage = |
345 | - Verify::new(Encrypted::new(global_args.key, cache)); |
346 | - run_server(storage, &addr).await?; |
347 | - } |
348 | - None => { |
349 | - let storage = Verify::new(Encrypted::new(global_args.key, s3)); |
350 | - run_server(storage, &addr).await?; |
351 | - } |
352 | + let mut builder = S3ServerBuilder::new(self.bucket, global_args.key); |
353 | + builder.prefix(self.prefix); |
354 | + |
355 | + if let Some(cdn) = self.cdn { |
356 | + builder.cdn(cdn); |
357 | } |
358 | |
359 | - Ok(()) |
360 | + if let Some(cache_dir) = global_args.cache_dir { |
361 | + let max_cache_size = global_args |
362 | + .max_cache_size |
363 | + .into::<human_size::Byte>() |
364 | + .value() as u64; |
365 | + builder.cache(Cache::new(cache_dir, max_cache_size)); |
366 | + } |
367 | + |
368 | + builder.run(addr).await |
369 | } |
370 | } |
371 | |
372 | @@ -222,40 +184,20 @@ impl LocalArgs { |
373 | addr: SocketAddr, |
374 | global_args: GlobalArgs, |
375 | ) -> Result<(), Box<dyn std::error::Error>> { |
376 | - let storage = Disk::new(self.path).map_err(Error::from).await?; |
377 | - let storage = Verify::new(Encrypted::new(global_args.key, storage)); |
378 | - |
379 | - log::info!("Local disk storage initialized."); |
380 | + let mut builder = LocalServerBuilder::new(self.path, global_args.key); |
381 | + |
382 | + if let Some(cache_dir) = global_args.cache_dir { |
383 | + let max_cache_size = global_args |
384 | + .max_cache_size |
385 | + .into::<human_size::Byte>() |
386 | + .value() as u64; |
387 | + builder.cache(Cache::new(cache_dir, max_cache_size)); |
388 | + } |
389 | |
390 | - run_server(storage, &addr).await?; |
391 | - Ok(()) |
392 | + builder.run(addr).await |
393 | } |
394 | } |
395 | |
396 | - async fn run_server<S>(storage: S, addr: &SocketAddr) -> hyper::Result<()> |
397 | - where |
398 | - S: Storage + Send + Sync + 'static, |
399 | - S::Error: Into<Error>, |
400 | - Error: From<S::Error>, |
401 | - { |
402 | - let storage = Arc::new(storage); |
403 | - |
404 | - let new_service = make_service_fn(move |socket: &AddrStream| { |
405 | - // Create our app. |
406 | - let service = App::new(storage.clone()); |
407 | - |
408 | - // Add logging middleware |
409 | - future::ok::<_, Infallible>(Logger::new(socket.remote_addr(), service)) |
410 | - }); |
411 | - |
412 | - let server = Server::bind(&addr).serve(new_service); |
413 | - |
414 | - log::info!("Listening on {}", server.local_addr()); |
415 | - |
416 | - server.await?; |
417 | - Ok(()) |
418 | - } |
419 | - |
420 | #[tokio::main] |
421 | async fn main() { |
422 | let exit_code = if let Err(err) = Args::from_args().main().await { |
423 | diff --git a/src/storage/disk.rs b/src/storage/disk.rs |
424 | index 86dd977..4b5a147 100644 |
425 | --- a/src/storage/disk.rs |
426 | +++ b/src/storage/disk.rs |
427 | @@ -156,18 +156,18 @@ impl Storage for Backend { |
428 | /// |
429 | /// The directory structure is assumed to be like this: |
430 | /// |
431 | - /// objects/{org}/{project}/ |
432 | - /// ├── 00 |
433 | - /// │ ├── 07 |
434 | - /// │ │ └── 0007941906960... |
435 | - /// │ └── ff |
436 | - /// │ └── 00ff9e9c69224... |
437 | - /// ├── 01 |
438 | - /// │ ├── 89 |
439 | - /// │ │ └── 0189e5fd19477... |
440 | - /// │ └── f5 |
441 | - /// │ └── 01f5c45c65e62... |
442 | - /// ^^^^ |
443 | + /// objects/{org}/{project}/ |
444 | + /// ├── 00 |
445 | + /// │ ├── 07 |
446 | + /// │ │ └── 0007941906960... |
447 | + /// │ └── ff |
448 | + /// │ └── 00ff9e9c69224... |
449 | + /// ├── 01 |
450 | + /// │ ├── 89 |
451 | + /// │ │ └── 0189e5fd19477... |
452 | + /// │ └── f5 |
453 | + /// │ └── 01f5c45c65e62... |
454 | + /// ^^^^ |
455 | /// |
456 | /// Note that the first four characters are repeated in the file name so |
457 | /// that transforming the file name into an object ID is simpler. |