Author: Jason White [jwhite@esri.com]
Hash: 8db98d676c7a4f4e826d235dfbdd8f96740797a9
Timestamp: Sat, 02 Feb 2019 10:10:53 +0000 (5 years ago)

+4971 -0 +/-25 browse
Initial commit
Initial commit

The pre-release history has been squashed to protect the innocent.
1diff --git a/.cirrus.yml b/.cirrus.yml
2new file mode 100644
3index 0000000..31dc9d0
4--- /dev/null
5+++ b/.cirrus.yml
6 @@ -0,0 +1,32 @@
7+ # Check that formatting is correct using nightly rust.
8+ fmt_task:
9+ container:
10+ image: rustlang/rust:nightly
11+ install_script: rustup component add rustfmt-preview
12+ check_script: cargo fmt -- --check
13+
14+ # Run clippy.
15+ clippy_task:
16+ container:
17+ image: rustlang/rust:nightly
18+ cargo_cache:
19+ folder: $CARGO_HOME/registry
20+ fingerprint_script: cat Cargo.lock
21+ install_script: rustup component add clippy-preview
22+ check_script: cargo clippy
23+ before_cache_script: rm -rf $CARGO_HOME/registry/index
24+
25+ # Build and test.
26+ test_task:
27+ matrix:
28+ - container:
29+ image: rust:latest
30+ - allow_failures: true
31+ container:
32+ image: rustlang/rust:nightly
33+ cargo_cache:
34+ folder: $CARGO_HOME/registry
35+ fingerprint_script: cat Cargo.lock
36+ build_script: cargo build
37+ test_script: cargo test
38+ before_cache_script: rm -rf $CARGO_HOME/registry/index
39 diff --git a/.dockerignore b/.dockerignore
40new file mode 100644
41index 0000000..8d42d79
42--- /dev/null
43+++ b/.dockerignore
44 @@ -0,0 +1,7 @@
45+ # Ignore everything
46+ **
47+
48+ # Only include these files.
49+ !/src/**
50+ !/Cargo.toml
51+ !/Cargo.lock
52 diff --git a/.gitignore b/.gitignore
53new file mode 100644
54index 0000000..9340db5
55--- /dev/null
56+++ b/.gitignore
57 @@ -0,0 +1,5 @@
58+ /target
59+ **/*.rs.bk
60+ /test.sh
61+ /cache/
62+ .env
63 diff --git a/.license_template b/.license_template
64new file mode 100644
65index 0000000..65079b2
66--- /dev/null
67+++ b/.license_template
68 @@ -0,0 +1,19 @@
69+ // Copyright (c) {\d+} Jason White
70+ //
71+ // Permission is hereby granted, free of charge, to any person obtaining a copy
72+ // of this software and associated documentation files (the "Software"), to deal
73+ // in the Software without restriction, including without limitation the rights
74+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
75+ // copies of the Software, and to permit persons to whom the Software is
76+ // furnished to do so, subject to the following conditions:
77+ //
78+ // The above copyright notice and this permission notice shall be included in
79+ // all copies or substantial portions of the Software.
80+ //
81+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
82+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
83+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
84+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
85+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
86+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
87+ // SOFTWARE.
88 diff --git a/Cargo.lock b/Cargo.lock
89new file mode 100644
90index 0000000..5872c48
91--- /dev/null
92+++ b/Cargo.lock
93 @@ -0,0 +1,1783 @@
94+ # This file is automatically @generated by Cargo.
95+ # It is not intended for manual editing.
96+ [[package]]
97+ name = "aho-corasick"
98+ version = "0.6.10"
99+ source = "registry+https://github.com/rust-lang/crates.io-index"
100+ dependencies = [
101+ "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
102+ ]
103+
104+ [[package]]
105+ name = "ansi_term"
106+ version = "0.11.0"
107+ source = "registry+https://github.com/rust-lang/crates.io-index"
108+ dependencies = [
109+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
110+ ]
111+
112+ [[package]]
113+ name = "argon2rs"
114+ version = "0.2.5"
115+ source = "registry+https://github.com/rust-lang/crates.io-index"
116+ dependencies = [
117+ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
118+ "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
119+ ]
120+
121+ [[package]]
122+ name = "arrayref"
123+ version = "0.3.5"
124+ source = "registry+https://github.com/rust-lang/crates.io-index"
125+
126+ [[package]]
127+ name = "arrayvec"
128+ version = "0.4.10"
129+ source = "registry+https://github.com/rust-lang/crates.io-index"
130+ dependencies = [
131+ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
132+ ]
133+
134+ [[package]]
135+ name = "atty"
136+ version = "0.2.11"
137+ source = "registry+https://github.com/rust-lang/crates.io-index"
138+ dependencies = [
139+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
140+ "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
141+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
142+ ]
143+
144+ [[package]]
145+ name = "autocfg"
146+ version = "0.1.2"
147+ source = "registry+https://github.com/rust-lang/crates.io-index"
148+
149+ [[package]]
150+ name = "backtrace"
151+ version = "0.3.14"
152+ source = "registry+https://github.com/rust-lang/crates.io-index"
153+ dependencies = [
154+ "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
155+ "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
156+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
157+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
158+ "rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
159+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
160+ ]
161+
162+ [[package]]
163+ name = "backtrace-sys"
164+ version = "0.1.28"
165+ source = "registry+https://github.com/rust-lang/crates.io-index"
166+ dependencies = [
167+ "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
168+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
169+ ]
170+
171+ [[package]]
172+ name = "base64"
173+ version = "0.9.3"
174+ source = "registry+https://github.com/rust-lang/crates.io-index"
175+ dependencies = [
176+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
177+ "safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
178+ ]
179+
180+ [[package]]
181+ name = "bitflags"
182+ version = "1.0.4"
183+ source = "registry+https://github.com/rust-lang/crates.io-index"
184+
185+ [[package]]
186+ name = "blake2-rfc"
187+ version = "0.2.18"
188+ source = "registry+https://github.com/rust-lang/crates.io-index"
189+ dependencies = [
190+ "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
191+ "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
192+ ]
193+
194+ [[package]]
195+ name = "block-buffer"
196+ version = "0.3.3"
197+ source = "registry+https://github.com/rust-lang/crates.io-index"
198+ dependencies = [
199+ "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
200+ "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
201+ ]
202+
203+ [[package]]
204+ name = "block-buffer"
205+ version = "0.7.0"
206+ source = "registry+https://github.com/rust-lang/crates.io-index"
207+ dependencies = [
208+ "block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
209+ "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
210+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
211+ "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
212+ ]
213+
214+ [[package]]
215+ name = "block-padding"
216+ version = "0.1.3"
217+ source = "registry+https://github.com/rust-lang/crates.io-index"
218+ dependencies = [
219+ "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
220+ ]
221+
222+ [[package]]
223+ name = "byte-tools"
224+ version = "0.2.0"
225+ source = "registry+https://github.com/rust-lang/crates.io-index"
226+
227+ [[package]]
228+ name = "byte-tools"
229+ version = "0.3.1"
230+ source = "registry+https://github.com/rust-lang/crates.io-index"
231+
232+ [[package]]
233+ name = "byteorder"
234+ version = "1.3.1"
235+ source = "registry+https://github.com/rust-lang/crates.io-index"
236+
237+ [[package]]
238+ name = "bytes"
239+ version = "0.4.12"
240+ source = "registry+https://github.com/rust-lang/crates.io-index"
241+ dependencies = [
242+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
243+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
244+ ]
245+
246+ [[package]]
247+ name = "cc"
248+ version = "1.0.31"
249+ source = "registry+https://github.com/rust-lang/crates.io-index"
250+
251+ [[package]]
252+ name = "cfg-if"
253+ version = "0.1.7"
254+ source = "registry+https://github.com/rust-lang/crates.io-index"
255+
256+ [[package]]
257+ name = "chacha"
258+ version = "0.3.0"
259+ source = "registry+https://github.com/rust-lang/crates.io-index"
260+ dependencies = [
261+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
262+ "keystream 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
263+ ]
264+
265+ [[package]]
266+ name = "chrono"
267+ version = "0.4.6"
268+ source = "registry+https://github.com/rust-lang/crates.io-index"
269+ dependencies = [
270+ "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
271+ "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
272+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
273+ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
274+ ]
275+
276+ [[package]]
277+ name = "clap"
278+ version = "2.32.0"
279+ source = "registry+https://github.com/rust-lang/crates.io-index"
280+ dependencies = [
281+ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
282+ "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
283+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
284+ "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
285+ "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
286+ "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
287+ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
288+ ]
289+
290+ [[package]]
291+ name = "cloudabi"
292+ version = "0.0.3"
293+ source = "registry+https://github.com/rust-lang/crates.io-index"
294+ dependencies = [
295+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
296+ ]
297+
298+ [[package]]
299+ name = "constant_time_eq"
300+ version = "0.1.3"
301+ source = "registry+https://github.com/rust-lang/crates.io-index"
302+
303+ [[package]]
304+ name = "crossbeam-deque"
305+ version = "0.7.1"
306+ source = "registry+https://github.com/rust-lang/crates.io-index"
307+ dependencies = [
308+ "crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
309+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
310+ ]
311+
312+ [[package]]
313+ name = "crossbeam-epoch"
314+ version = "0.7.1"
315+ source = "registry+https://github.com/rust-lang/crates.io-index"
316+ dependencies = [
317+ "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
318+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
319+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
320+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
321+ "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
322+ "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
323+ ]
324+
325+ [[package]]
326+ name = "crossbeam-queue"
327+ version = "0.1.2"
328+ source = "registry+https://github.com/rust-lang/crates.io-index"
329+ dependencies = [
330+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
331+ ]
332+
333+ [[package]]
334+ name = "crossbeam-utils"
335+ version = "0.6.5"
336+ source = "registry+https://github.com/rust-lang/crates.io-index"
337+ dependencies = [
338+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
339+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
340+ ]
341+
342+ [[package]]
343+ name = "crypto-mac"
344+ version = "0.5.2"
345+ source = "registry+https://github.com/rust-lang/crates.io-index"
346+ dependencies = [
347+ "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
348+ "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
349+ ]
350+
351+ [[package]]
352+ name = "ct-logs"
353+ version = "0.4.0"
354+ source = "registry+https://github.com/rust-lang/crates.io-index"
355+ dependencies = [
356+ "sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
357+ ]
358+
359+ [[package]]
360+ name = "derive_more"
361+ version = "0.14.0"
362+ source = "registry+https://github.com/rust-lang/crates.io-index"
363+ dependencies = [
364+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
365+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
366+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
367+ "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
368+ ]
369+
370+ [[package]]
371+ name = "digest"
372+ version = "0.7.6"
373+ source = "registry+https://github.com/rust-lang/crates.io-index"
374+ dependencies = [
375+ "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
376+ ]
377+
378+ [[package]]
379+ name = "digest"
380+ version = "0.8.0"
381+ source = "registry+https://github.com/rust-lang/crates.io-index"
382+ dependencies = [
383+ "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
384+ ]
385+
386+ [[package]]
387+ name = "dirs"
388+ version = "1.0.5"
389+ source = "registry+https://github.com/rust-lang/crates.io-index"
390+ dependencies = [
391+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
392+ "redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
393+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
394+ ]
395+
396+ [[package]]
397+ name = "env_logger"
398+ version = "0.6.1"
399+ source = "registry+https://github.com/rust-lang/crates.io-index"
400+ dependencies = [
401+ "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
402+ "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
403+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
404+ "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
405+ "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
406+ ]
407+
408+ [[package]]
409+ name = "failure"
410+ version = "0.1.5"
411+ source = "registry+https://github.com/rust-lang/crates.io-index"
412+ dependencies = [
413+ "backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
414+ "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
415+ ]
416+
417+ [[package]]
418+ name = "failure_derive"
419+ version = "0.1.5"
420+ source = "registry+https://github.com/rust-lang/crates.io-index"
421+ dependencies = [
422+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
423+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
424+ "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
425+ "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
426+ ]
427+
428+ [[package]]
429+ name = "fake-simd"
430+ version = "0.1.2"
431+ source = "registry+https://github.com/rust-lang/crates.io-index"
432+
433+ [[package]]
434+ name = "fnv"
435+ version = "1.0.6"
436+ source = "registry+https://github.com/rust-lang/crates.io-index"
437+
438+ [[package]]
439+ name = "fuchsia-cprng"
440+ version = "0.1.1"
441+ source = "registry+https://github.com/rust-lang/crates.io-index"
442+
443+ [[package]]
444+ name = "fuchsia-zircon"
445+ version = "0.3.3"
446+ source = "registry+https://github.com/rust-lang/crates.io-index"
447+ dependencies = [
448+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
449+ "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
450+ ]
451+
452+ [[package]]
453+ name = "fuchsia-zircon-sys"
454+ version = "0.3.3"
455+ source = "registry+https://github.com/rust-lang/crates.io-index"
456+
457+ [[package]]
458+ name = "futures"
459+ version = "0.1.25"
460+ source = "registry+https://github.com/rust-lang/crates.io-index"
461+
462+ [[package]]
463+ name = "futures-cpupool"
464+ version = "0.1.8"
465+ source = "registry+https://github.com/rust-lang/crates.io-index"
466+ dependencies = [
467+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
468+ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
469+ ]
470+
471+ [[package]]
472+ name = "generic-array"
473+ version = "0.9.0"
474+ source = "registry+https://github.com/rust-lang/crates.io-index"
475+ dependencies = [
476+ "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
477+ ]
478+
479+ [[package]]
480+ name = "generic-array"
481+ version = "0.12.0"
482+ source = "registry+https://github.com/rust-lang/crates.io-index"
483+ dependencies = [
484+ "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
485+ ]
486+
487+ [[package]]
488+ name = "h2"
489+ version = "0.1.16"
490+ source = "registry+https://github.com/rust-lang/crates.io-index"
491+ dependencies = [
492+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
493+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
494+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
495+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
496+ "http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
497+ "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
498+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
499+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
500+ "string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
501+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
502+ ]
503+
504+ [[package]]
505+ name = "heck"
506+ version = "0.3.1"
507+ source = "registry+https://github.com/rust-lang/crates.io-index"
508+ dependencies = [
509+ "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
510+ ]
511+
512+ [[package]]
513+ name = "hex"
514+ version = "0.3.2"
515+ source = "registry+https://github.com/rust-lang/crates.io-index"
516+
517+ [[package]]
518+ name = "hmac"
519+ version = "0.5.0"
520+ source = "registry+https://github.com/rust-lang/crates.io-index"
521+ dependencies = [
522+ "crypto-mac 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
523+ "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
524+ ]
525+
526+ [[package]]
527+ name = "http"
528+ version = "0.1.16"
529+ source = "registry+https://github.com/rust-lang/crates.io-index"
530+ dependencies = [
531+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
532+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
533+ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
534+ ]
535+
536+ [[package]]
537+ name = "httparse"
538+ version = "1.3.3"
539+ source = "registry+https://github.com/rust-lang/crates.io-index"
540+
541+ [[package]]
542+ name = "human-size"
543+ version = "0.4.0"
544+ source = "registry+https://github.com/rust-lang/crates.io-index"
545+
546+ [[package]]
547+ name = "humantime"
548+ version = "1.2.0"
549+ source = "registry+https://github.com/rust-lang/crates.io-index"
550+ dependencies = [
551+ "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
552+ ]
553+
554+ [[package]]
555+ name = "hyper"
556+ version = "0.12.25"
557+ source = "registry+https://github.com/rust-lang/crates.io-index"
558+ dependencies = [
559+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
560+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
561+ "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
562+ "h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
563+ "http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
564+ "httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
565+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
566+ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
567+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
568+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
569+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
570+ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
571+ "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
572+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
573+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
574+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
575+ "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
576+ "tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
577+ "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
578+ "want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
579+ ]
580+
581+ [[package]]
582+ name = "hyper-rustls"
583+ version = "0.14.0"
584+ source = "registry+https://github.com/rust-lang/crates.io-index"
585+ dependencies = [
586+ "ct-logs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
587+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
588+ "http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
589+ "hyper 0.12.25 (registry+https://github.com/rust-lang/crates.io-index)",
590+ "rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
591+ "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
592+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
593+ "tokio-rustls 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
594+ "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
595+ "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
596+ "webpki-roots 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
597+ ]
598+
599+ [[package]]
600+ name = "idna"
601+ version = "0.1.5"
602+ source = "registry+https://github.com/rust-lang/crates.io-index"
603+ dependencies = [
604+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
605+ "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
606+ "unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
607+ ]
608+
609+ [[package]]
610+ name = "indexmap"
611+ version = "1.0.2"
612+ source = "registry+https://github.com/rust-lang/crates.io-index"
613+
614+ [[package]]
615+ name = "iovec"
616+ version = "0.1.2"
617+ source = "registry+https://github.com/rust-lang/crates.io-index"
618+ dependencies = [
619+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
620+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
621+ ]
622+
623+ [[package]]
624+ name = "itoa"
625+ version = "0.4.3"
626+ source = "registry+https://github.com/rust-lang/crates.io-index"
627+
628+ [[package]]
629+ name = "kernel32-sys"
630+ version = "0.2.2"
631+ source = "registry+https://github.com/rust-lang/crates.io-index"
632+ dependencies = [
633+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
634+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
635+ ]
636+
637+ [[package]]
638+ name = "keystream"
639+ version = "1.0.0"
640+ source = "registry+https://github.com/rust-lang/crates.io-index"
641+
642+ [[package]]
643+ name = "lazy_static"
644+ version = "1.3.0"
645+ source = "registry+https://github.com/rust-lang/crates.io-index"
646+
647+ [[package]]
648+ name = "lazycell"
649+ version = "1.2.1"
650+ source = "registry+https://github.com/rust-lang/crates.io-index"
651+
652+ [[package]]
653+ name = "libc"
654+ version = "0.2.50"
655+ source = "registry+https://github.com/rust-lang/crates.io-index"
656+
657+ [[package]]
658+ name = "linked-hash-map"
659+ version = "0.5.1"
660+ source = "registry+https://github.com/rust-lang/crates.io-index"
661+ dependencies = [
662+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
663+ "serde_test 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
664+ ]
665+
666+ [[package]]
667+ name = "lock_api"
668+ version = "0.1.5"
669+ source = "registry+https://github.com/rust-lang/crates.io-index"
670+ dependencies = [
671+ "owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
672+ "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
673+ ]
674+
675+ [[package]]
676+ name = "log"
677+ version = "0.4.6"
678+ source = "registry+https://github.com/rust-lang/crates.io-index"
679+ dependencies = [
680+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
681+ ]
682+
683+ [[package]]
684+ name = "matches"
685+ version = "0.1.8"
686+ source = "registry+https://github.com/rust-lang/crates.io-index"
687+
688+ [[package]]
689+ name = "md5"
690+ version = "0.3.8"
691+ source = "registry+https://github.com/rust-lang/crates.io-index"
692+
693+ [[package]]
694+ name = "memchr"
695+ version = "2.2.0"
696+ source = "registry+https://github.com/rust-lang/crates.io-index"
697+
698+ [[package]]
699+ name = "memoffset"
700+ version = "0.2.1"
701+ source = "registry+https://github.com/rust-lang/crates.io-index"
702+
703+ [[package]]
704+ name = "mio"
705+ version = "0.6.16"
706+ source = "registry+https://github.com/rust-lang/crates.io-index"
707+ dependencies = [
708+ "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
709+ "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
710+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
711+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
712+ "lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
713+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
714+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
715+ "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
716+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
717+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
718+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
719+ ]
720+
721+ [[package]]
722+ name = "mio-uds"
723+ version = "0.6.7"
724+ source = "registry+https://github.com/rust-lang/crates.io-index"
725+ dependencies = [
726+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
727+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
728+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
729+ ]
730+
731+ [[package]]
732+ name = "miow"
733+ version = "0.2.1"
734+ source = "registry+https://github.com/rust-lang/crates.io-index"
735+ dependencies = [
736+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
737+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
738+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
739+ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
740+ ]
741+
742+ [[package]]
743+ name = "net2"
744+ version = "0.2.33"
745+ source = "registry+https://github.com/rust-lang/crates.io-index"
746+ dependencies = [
747+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
748+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
749+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
750+ ]
751+
752+ [[package]]
753+ name = "nodrop"
754+ version = "0.1.13"
755+ source = "registry+https://github.com/rust-lang/crates.io-index"
756+
757+ [[package]]
758+ name = "num-integer"
759+ version = "0.1.39"
760+ source = "registry+https://github.com/rust-lang/crates.io-index"
761+ dependencies = [
762+ "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
763+ ]
764+
765+ [[package]]
766+ name = "num-traits"
767+ version = "0.2.6"
768+ source = "registry+https://github.com/rust-lang/crates.io-index"
769+
770+ [[package]]
771+ name = "num_cpus"
772+ version = "1.10.0"
773+ source = "registry+https://github.com/rust-lang/crates.io-index"
774+ dependencies = [
775+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
776+ ]
777+
778+ [[package]]
779+ name = "opaque-debug"
780+ version = "0.2.2"
781+ source = "registry+https://github.com/rust-lang/crates.io-index"
782+
783+ [[package]]
784+ name = "owning_ref"
785+ version = "0.4.0"
786+ source = "registry+https://github.com/rust-lang/crates.io-index"
787+ dependencies = [
788+ "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
789+ ]
790+
791+ [[package]]
792+ name = "parking_lot"
793+ version = "0.7.1"
794+ source = "registry+https://github.com/rust-lang/crates.io-index"
795+ dependencies = [
796+ "lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
797+ "parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
798+ ]
799+
800+ [[package]]
801+ name = "parking_lot_core"
802+ version = "0.4.0"
803+ source = "registry+https://github.com/rust-lang/crates.io-index"
804+ dependencies = [
805+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
806+ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
807+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
808+ "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
809+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
810+ ]
811+
812+ [[package]]
813+ name = "percent-encoding"
814+ version = "1.0.1"
815+ source = "registry+https://github.com/rust-lang/crates.io-index"
816+
817+ [[package]]
818+ name = "pretty_env_logger"
819+ version = "0.3.0"
820+ source = "registry+https://github.com/rust-lang/crates.io-index"
821+ dependencies = [
822+ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
823+ "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
824+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
825+ ]
826+
827+ [[package]]
828+ name = "proc-macro2"
829+ version = "0.4.27"
830+ source = "registry+https://github.com/rust-lang/crates.io-index"
831+ dependencies = [
832+ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
833+ ]
834+
835+ [[package]]
836+ name = "quick-error"
837+ version = "1.2.2"
838+ source = "registry+https://github.com/rust-lang/crates.io-index"
839+
840+ [[package]]
841+ name = "quote"
842+ version = "0.6.11"
843+ source = "registry+https://github.com/rust-lang/crates.io-index"
844+ dependencies = [
845+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
846+ ]
847+
848+ [[package]]
849+ name = "rand"
850+ version = "0.6.5"
851+ source = "registry+https://github.com/rust-lang/crates.io-index"
852+ dependencies = [
853+ "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
854+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
855+ "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
856+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
857+ "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
858+ "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
859+ "rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
860+ "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
861+ "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
862+ "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
863+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
864+ ]
865+
866+ [[package]]
867+ name = "rand_chacha"
868+ version = "0.1.1"
869+ source = "registry+https://github.com/rust-lang/crates.io-index"
870+ dependencies = [
871+ "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
872+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
873+ ]
874+
875+ [[package]]
876+ name = "rand_core"
877+ version = "0.3.1"
878+ source = "registry+https://github.com/rust-lang/crates.io-index"
879+ dependencies = [
880+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
881+ ]
882+
883+ [[package]]
884+ name = "rand_core"
885+ version = "0.4.0"
886+ source = "registry+https://github.com/rust-lang/crates.io-index"
887+
888+ [[package]]
889+ name = "rand_hc"
890+ version = "0.1.0"
891+ source = "registry+https://github.com/rust-lang/crates.io-index"
892+ dependencies = [
893+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
894+ ]
895+
896+ [[package]]
897+ name = "rand_isaac"
898+ version = "0.1.1"
899+ source = "registry+https://github.com/rust-lang/crates.io-index"
900+ dependencies = [
901+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
902+ ]
903+
904+ [[package]]
905+ name = "rand_jitter"
906+ version = "0.1.3"
907+ source = "registry+https://github.com/rust-lang/crates.io-index"
908+ dependencies = [
909+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
910+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
911+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
912+ ]
913+
914+ [[package]]
915+ name = "rand_os"
916+ version = "0.1.3"
917+ source = "registry+https://github.com/rust-lang/crates.io-index"
918+ dependencies = [
919+ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
920+ "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
921+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
922+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
923+ "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
924+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
925+ ]
926+
927+ [[package]]
928+ name = "rand_pcg"
929+ version = "0.1.2"
930+ source = "registry+https://github.com/rust-lang/crates.io-index"
931+ dependencies = [
932+ "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
933+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
934+ ]
935+
936+ [[package]]
937+ name = "rand_xorshift"
938+ version = "0.1.1"
939+ source = "registry+https://github.com/rust-lang/crates.io-index"
940+ dependencies = [
941+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
942+ ]
943+
944+ [[package]]
945+ name = "rdrand"
946+ version = "0.4.0"
947+ source = "registry+https://github.com/rust-lang/crates.io-index"
948+ dependencies = [
949+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
950+ ]
951+
952+ [[package]]
953+ name = "redox_syscall"
954+ version = "0.1.51"
955+ source = "registry+https://github.com/rust-lang/crates.io-index"
956+
957+ [[package]]
958+ name = "redox_termios"
959+ version = "0.1.1"
960+ source = "registry+https://github.com/rust-lang/crates.io-index"
961+ dependencies = [
962+ "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
963+ ]
964+
965+ [[package]]
966+ name = "redox_users"
967+ version = "0.3.0"
968+ source = "registry+https://github.com/rust-lang/crates.io-index"
969+ dependencies = [
970+ "argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
971+ "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
972+ "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
973+ "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
974+ ]
975+
976+ [[package]]
977+ name = "regex"
978+ version = "1.1.2"
979+ source = "registry+https://github.com/rust-lang/crates.io-index"
980+ dependencies = [
981+ "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
982+ "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
983+ "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
984+ "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
985+ "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
986+ ]
987+
988+ [[package]]
989+ name = "regex-syntax"
990+ version = "0.6.5"
991+ source = "registry+https://github.com/rust-lang/crates.io-index"
992+ dependencies = [
993+ "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
994+ ]
995+
996+ [[package]]
997+ name = "ring"
998+ version = "0.13.5"
999+ source = "registry+https://github.com/rust-lang/crates.io-index"
1000+ dependencies = [
1001+ "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
1002+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
1003+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
1004+ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1005+ ]
1006+
1007+ [[package]]
1008+ name = "rudolfs"
1009+ version = "0.1.0"
1010+ dependencies = [
1011+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1012+ "chacha 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
1013+ "derive_more 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
1014+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1015+ "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
1016+ "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
1017+ "http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
1018+ "human-size 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
1019+ "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
1020+ "hyper 0.12.25 (registry+https://github.com/rust-lang/crates.io-index)",
1021+ "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
1022+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1023+ "pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
1024+ "rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)",
1025+ "rusoto_s3 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)",
1026+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1027+ "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
1028+ "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
1029+ "structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)",
1030+ "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
1031+ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
1032+ "uuid 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
1033+ ]
1034+
1035+ [[package]]
1036+ name = "rusoto_core"
1037+ version = "0.36.0"
1038+ source = "registry+https://github.com/rust-lang/crates.io-index"
1039+ dependencies = [
1040+ "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
1041+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1042+ "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
1043+ "hmac 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
1044+ "hyper 0.12.25 (registry+https://github.com/rust-lang/crates.io-index)",
1045+ "hyper-rustls 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
1046+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
1047+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1048+ "md5 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
1049+ "rusoto_credential 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
1050+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
1051+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1052+ "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
1053+ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
1054+ "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
1055+ "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1056+ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
1057+ "xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
1058+ ]
1059+
1060+ [[package]]
1061+ name = "rusoto_credential"
1062+ version = "0.15.0"
1063+ source = "registry+https://github.com/rust-lang/crates.io-index"
1064+ dependencies = [
1065+ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1066+ "dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
1067+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1068+ "hyper 0.12.25 (registry+https://github.com/rust-lang/crates.io-index)",
1069+ "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1070+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1071+ "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1072+ "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
1073+ "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1074+ ]
1075+
1076+ [[package]]
1077+ name = "rusoto_s3"
1078+ version = "0.36.0"
1079+ source = "registry+https://github.com/rust-lang/crates.io-index"
1080+ dependencies = [
1081+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1082+ "rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)",
1083+ "xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
1084+ ]
1085+
1086+ [[package]]
1087+ name = "rustc-demangle"
1088+ version = "0.1.13"
1089+ source = "registry+https://github.com/rust-lang/crates.io-index"
1090+
1091+ [[package]]
1092+ name = "rustc_version"
1093+ version = "0.2.3"
1094+ source = "registry+https://github.com/rust-lang/crates.io-index"
1095+ dependencies = [
1096+ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
1097+ ]
1098+
1099+ [[package]]
1100+ name = "rustls"
1101+ version = "0.13.1"
1102+ source = "registry+https://github.com/rust-lang/crates.io-index"
1103+ dependencies = [
1104+ "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
1105+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1106+ "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
1107+ "sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
1108+ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1109+ "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
1110+ ]
1111+
1112+ [[package]]
1113+ name = "ryu"
1114+ version = "0.2.7"
1115+ source = "registry+https://github.com/rust-lang/crates.io-index"
1116+
1117+ [[package]]
1118+ name = "safemem"
1119+ version = "0.3.0"
1120+ source = "registry+https://github.com/rust-lang/crates.io-index"
1121+
1122+ [[package]]
1123+ name = "scoped-tls"
1124+ version = "0.1.2"
1125+ source = "registry+https://github.com/rust-lang/crates.io-index"
1126+
1127+ [[package]]
1128+ name = "scoped_threadpool"
1129+ version = "0.1.9"
1130+ source = "registry+https://github.com/rust-lang/crates.io-index"
1131+
1132+ [[package]]
1133+ name = "scopeguard"
1134+ version = "0.3.3"
1135+ source = "registry+https://github.com/rust-lang/crates.io-index"
1136+
1137+ [[package]]
1138+ name = "sct"
1139+ version = "0.4.0"
1140+ source = "registry+https://github.com/rust-lang/crates.io-index"
1141+ dependencies = [
1142+ "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
1143+ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1144+ ]
1145+
1146+ [[package]]
1147+ name = "semver"
1148+ version = "0.9.0"
1149+ source = "registry+https://github.com/rust-lang/crates.io-index"
1150+ dependencies = [
1151+ "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
1152+ ]
1153+
1154+ [[package]]
1155+ name = "semver-parser"
1156+ version = "0.7.0"
1157+ source = "registry+https://github.com/rust-lang/crates.io-index"
1158+
1159+ [[package]]
1160+ name = "serde"
1161+ version = "1.0.89"
1162+ source = "registry+https://github.com/rust-lang/crates.io-index"
1163+ dependencies = [
1164+ "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1165+ ]
1166+
1167+ [[package]]
1168+ name = "serde_derive"
1169+ version = "1.0.89"
1170+ source = "registry+https://github.com/rust-lang/crates.io-index"
1171+ dependencies = [
1172+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
1173+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
1174+ "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
1175+ ]
1176+
1177+ [[package]]
1178+ name = "serde_json"
1179+ version = "1.0.39"
1180+ source = "registry+https://github.com/rust-lang/crates.io-index"
1181+ dependencies = [
1182+ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
1183+ "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
1184+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1185+ ]
1186+
1187+ [[package]]
1188+ name = "serde_test"
1189+ version = "1.0.89"
1190+ source = "registry+https://github.com/rust-lang/crates.io-index"
1191+ dependencies = [
1192+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
1193+ ]
1194+
1195+ [[package]]
1196+ name = "sha2"
1197+ version = "0.7.1"
1198+ source = "registry+https://github.com/rust-lang/crates.io-index"
1199+ dependencies = [
1200+ "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
1201+ "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
1202+ "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
1203+ "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1204+ ]
1205+
1206+ [[package]]
1207+ name = "sha2"
1208+ version = "0.8.0"
1209+ source = "registry+https://github.com/rust-lang/crates.io-index"
1210+ dependencies = [
1211+ "block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
1212+ "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
1213+ "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1214+ "opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
1215+ ]
1216+
1217+ [[package]]
1218+ name = "slab"
1219+ version = "0.4.2"
1220+ source = "registry+https://github.com/rust-lang/crates.io-index"
1221+
1222+ [[package]]
1223+ name = "smallvec"
1224+ version = "0.6.9"
1225+ source = "registry+https://github.com/rust-lang/crates.io-index"
1226+
1227+ [[package]]
1228+ name = "stable_deref_trait"
1229+ version = "1.1.1"
1230+ source = "registry+https://github.com/rust-lang/crates.io-index"
1231+
1232+ [[package]]
1233+ name = "string"
1234+ version = "0.1.3"
1235+ source = "registry+https://github.com/rust-lang/crates.io-index"
1236+
1237+ [[package]]
1238+ name = "strsim"
1239+ version = "0.7.0"
1240+ source = "registry+https://github.com/rust-lang/crates.io-index"
1241+
1242+ [[package]]
1243+ name = "structopt"
1244+ version = "0.2.15"
1245+ source = "registry+https://github.com/rust-lang/crates.io-index"
1246+ dependencies = [
1247+ "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
1248+ "structopt-derive 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)",
1249+ ]
1250+
1251+ [[package]]
1252+ name = "structopt-derive"
1253+ version = "0.2.15"
1254+ source = "registry+https://github.com/rust-lang/crates.io-index"
1255+ dependencies = [
1256+ "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
1257+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
1258+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
1259+ "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
1260+ ]
1261+
1262+ [[package]]
1263+ name = "syn"
1264+ version = "0.15.29"
1265+ source = "registry+https://github.com/rust-lang/crates.io-index"
1266+ dependencies = [
1267+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
1268+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
1269+ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
1270+ ]
1271+
1272+ [[package]]
1273+ name = "synstructure"
1274+ version = "0.10.1"
1275+ source = "registry+https://github.com/rust-lang/crates.io-index"
1276+ dependencies = [
1277+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
1278+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
1279+ "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
1280+ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
1281+ ]
1282+
1283+ [[package]]
1284+ name = "termcolor"
1285+ version = "1.0.4"
1286+ source = "registry+https://github.com/rust-lang/crates.io-index"
1287+ dependencies = [
1288+ "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
1289+ ]
1290+
1291+ [[package]]
1292+ name = "termion"
1293+ version = "1.5.1"
1294+ source = "registry+https://github.com/rust-lang/crates.io-index"
1295+ dependencies = [
1296+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
1297+ "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
1298+ "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
1299+ ]
1300+
1301+ [[package]]
1302+ name = "textwrap"
1303+ version = "0.10.0"
1304+ source = "registry+https://github.com/rust-lang/crates.io-index"
1305+ dependencies = [
1306+ "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
1307+ ]
1308+
1309+ [[package]]
1310+ name = "thread_local"
1311+ version = "0.3.6"
1312+ source = "registry+https://github.com/rust-lang/crates.io-index"
1313+ dependencies = [
1314+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
1315+ ]
1316+
1317+ [[package]]
1318+ name = "time"
1319+ version = "0.1.42"
1320+ source = "registry+https://github.com/rust-lang/crates.io-index"
1321+ dependencies = [
1322+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
1323+ "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
1324+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
1325+ ]
1326+
1327+ [[package]]
1328+ name = "tokio"
1329+ version = "0.1.16"
1330+ source = "registry+https://github.com/rust-lang/crates.io-index"
1331+ dependencies = [
1332+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1333+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1334+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
1335+ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
1336+ "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
1337+ "tokio-current-thread 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
1338+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1339+ "tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1340+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1341+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
1342+ "tokio-sync 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
1343+ "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
1344+ "tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1345+ "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1346+ "tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
1347+ "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
1348+ ]
1349+
1350+ [[package]]
1351+ name = "tokio-codec"
1352+ version = "0.1.1"
1353+ source = "registry+https://github.com/rust-lang/crates.io-index"
1354+ dependencies = [
1355+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1356+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1357+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1358+ ]
1359+
1360+ [[package]]
1361+ name = "tokio-core"
1362+ version = "0.1.17"
1363+ source = "registry+https://github.com/rust-lang/crates.io-index"
1364+ dependencies = [
1365+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1366+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1367+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1368+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1369+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
1370+ "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1371+ "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
1372+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1373+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1374+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
1375+ "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1376+ ]
1377+
1378+ [[package]]
1379+ name = "tokio-current-thread"
1380+ version = "0.1.5"
1381+ source = "registry+https://github.com/rust-lang/crates.io-index"
1382+ dependencies = [
1383+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1384+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1385+ ]
1386+
1387+ [[package]]
1388+ name = "tokio-executor"
1389+ version = "0.1.6"
1390+ source = "registry+https://github.com/rust-lang/crates.io-index"
1391+ dependencies = [
1392+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
1393+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1394+ ]
1395+
1396+ [[package]]
1397+ name = "tokio-fs"
1398+ version = "0.1.6"
1399+ source = "registry+https://github.com/rust-lang/crates.io-index"
1400+ dependencies = [
1401+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1402+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1403+ "tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1404+ ]
1405+
1406+ [[package]]
1407+ name = "tokio-io"
1408+ version = "0.1.12"
1409+ source = "registry+https://github.com/rust-lang/crates.io-index"
1410+ dependencies = [
1411+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1412+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1413+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1414+ ]
1415+
1416+ [[package]]
1417+ name = "tokio-reactor"
1418+ version = "0.1.9"
1419+ source = "registry+https://github.com/rust-lang/crates.io-index"
1420+ dependencies = [
1421+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
1422+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1423+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
1424+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1425+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
1426+ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
1427+ "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
1428+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
1429+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1430+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1431+ "tokio-sync 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
1432+ ]
1433+
1434+ [[package]]
1435+ name = "tokio-rustls"
1436+ version = "0.7.2"
1437+ source = "registry+https://github.com/rust-lang/crates.io-index"
1438+ dependencies = [
1439+ "rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
1440+ "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
1441+ "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
1442+ ]
1443+
1444+ [[package]]
1445+ name = "tokio-sync"
1446+ version = "0.1.3"
1447+ source = "registry+https://github.com/rust-lang/crates.io-index"
1448+ dependencies = [
1449+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
1450+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1451+ ]
1452+
1453+ [[package]]
1454+ name = "tokio-tcp"
1455+ version = "0.1.3"
1456+ source = "registry+https://github.com/rust-lang/crates.io-index"
1457+ dependencies = [
1458+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1459+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1460+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1461+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
1462+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1463+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
1464+ ]
1465+
1466+ [[package]]
1467+ name = "tokio-threadpool"
1468+ version = "0.1.12"
1469+ source = "registry+https://github.com/rust-lang/crates.io-index"
1470+ dependencies = [
1471+ "crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
1472+ "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1473+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
1474+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1475+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1476+ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
1477+ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
1478+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
1479+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1480+ ]
1481+
1482+ [[package]]
1483+ name = "tokio-timer"
1484+ version = "0.2.10"
1485+ source = "registry+https://github.com/rust-lang/crates.io-index"
1486+ dependencies = [
1487+ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
1488+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1489+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
1490+ "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
1491+ ]
1492+
1493+ [[package]]
1494+ name = "tokio-udp"
1495+ version = "0.1.3"
1496+ source = "registry+https://github.com/rust-lang/crates.io-index"
1497+ dependencies = [
1498+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1499+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1500+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1501+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
1502+ "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
1503+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1504+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
1505+ ]
1506+
1507+ [[package]]
1508+ name = "tokio-uds"
1509+ version = "0.2.5"
1510+ source = "registry+https://github.com/rust-lang/crates.io-index"
1511+ dependencies = [
1512+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
1513+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1514+ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1515+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
1516+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1517+ "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
1518+ "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
1519+ "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
1520+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
1521+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
1522+ ]
1523+
1524+ [[package]]
1525+ name = "try-lock"
1526+ version = "0.2.2"
1527+ source = "registry+https://github.com/rust-lang/crates.io-index"
1528+
1529+ [[package]]
1530+ name = "typenum"
1531+ version = "1.10.0"
1532+ source = "registry+https://github.com/rust-lang/crates.io-index"
1533+
1534+ [[package]]
1535+ name = "ucd-util"
1536+ version = "0.1.3"
1537+ source = "registry+https://github.com/rust-lang/crates.io-index"
1538+
1539+ [[package]]
1540+ name = "unicode-bidi"
1541+ version = "0.3.4"
1542+ source = "registry+https://github.com/rust-lang/crates.io-index"
1543+ dependencies = [
1544+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
1545+ ]
1546+
1547+ [[package]]
1548+ name = "unicode-normalization"
1549+ version = "0.1.8"
1550+ source = "registry+https://github.com/rust-lang/crates.io-index"
1551+ dependencies = [
1552+ "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
1553+ ]
1554+
1555+ [[package]]
1556+ name = "unicode-segmentation"
1557+ version = "1.2.1"
1558+ source = "registry+https://github.com/rust-lang/crates.io-index"
1559+
1560+ [[package]]
1561+ name = "unicode-width"
1562+ version = "0.1.5"
1563+ source = "registry+https://github.com/rust-lang/crates.io-index"
1564+
1565+ [[package]]
1566+ name = "unicode-xid"
1567+ version = "0.1.0"
1568+ source = "registry+https://github.com/rust-lang/crates.io-index"
1569+
1570+ [[package]]
1571+ name = "untrusted"
1572+ version = "0.6.2"
1573+ source = "registry+https://github.com/rust-lang/crates.io-index"
1574+
1575+ [[package]]
1576+ name = "url"
1577+ version = "1.7.2"
1578+ source = "registry+https://github.com/rust-lang/crates.io-index"
1579+ dependencies = [
1580+ "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
1581+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
1582+ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
1583+ ]
1584+
1585+ [[package]]
1586+ name = "utf8-ranges"
1587+ version = "1.0.2"
1588+ source = "registry+https://github.com/rust-lang/crates.io-index"
1589+
1590+ [[package]]
1591+ name = "uuid"
1592+ version = "0.7.2"
1593+ source = "registry+https://github.com/rust-lang/crates.io-index"
1594+ dependencies = [
1595+ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
1596+ ]
1597+
1598+ [[package]]
1599+ name = "vec_map"
1600+ version = "0.8.1"
1601+ source = "registry+https://github.com/rust-lang/crates.io-index"
1602+
1603+ [[package]]
1604+ name = "want"
1605+ version = "0.0.6"
1606+ source = "registry+https://github.com/rust-lang/crates.io-index"
1607+ dependencies = [
1608+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
1609+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
1610+ "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
1611+ ]
1612+
1613+ [[package]]
1614+ name = "webpki"
1615+ version = "0.18.1"
1616+ source = "registry+https://github.com/rust-lang/crates.io-index"
1617+ dependencies = [
1618+ "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
1619+ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1620+ ]
1621+
1622+ [[package]]
1623+ name = "webpki-roots"
1624+ version = "0.15.0"
1625+ source = "registry+https://github.com/rust-lang/crates.io-index"
1626+ dependencies = [
1627+ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1628+ "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
1629+ ]
1630+
1631+ [[package]]
1632+ name = "winapi"
1633+ version = "0.2.8"
1634+ source = "registry+https://github.com/rust-lang/crates.io-index"
1635+
1636+ [[package]]
1637+ name = "winapi"
1638+ version = "0.3.6"
1639+ source = "registry+https://github.com/rust-lang/crates.io-index"
1640+ dependencies = [
1641+ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
1642+ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
1643+ ]
1644+
1645+ [[package]]
1646+ name = "winapi-build"
1647+ version = "0.1.1"
1648+ source = "registry+https://github.com/rust-lang/crates.io-index"
1649+
1650+ [[package]]
1651+ name = "winapi-i686-pc-windows-gnu"
1652+ version = "0.4.0"
1653+ source = "registry+https://github.com/rust-lang/crates.io-index"
1654+
1655+ [[package]]
1656+ name = "winapi-util"
1657+ version = "0.1.2"
1658+ source = "registry+https://github.com/rust-lang/crates.io-index"
1659+ dependencies = [
1660+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
1661+ ]
1662+
1663+ [[package]]
1664+ name = "winapi-x86_64-pc-windows-gnu"
1665+ version = "0.4.0"
1666+ source = "registry+https://github.com/rust-lang/crates.io-index"
1667+
1668+ [[package]]
1669+ name = "wincolor"
1670+ version = "1.0.1"
1671+ source = "registry+https://github.com/rust-lang/crates.io-index"
1672+ dependencies = [
1673+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
1674+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
1675+ ]
1676+
1677+ [[package]]
1678+ name = "ws2_32-sys"
1679+ version = "0.2.1"
1680+ source = "registry+https://github.com/rust-lang/crates.io-index"
1681+ dependencies = [
1682+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
1683+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
1684+ ]
1685+
1686+ [[package]]
1687+ name = "xml-rs"
1688+ version = "0.7.0"
1689+ source = "registry+https://github.com/rust-lang/crates.io-index"
1690+ dependencies = [
1691+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
1692+ ]
1693+
1694+ [metadata]
1695+ "checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
1696+ "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
1697+ "checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392"
1698+ "checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee"
1699+ "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71"
1700+ "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
1701+ "checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
1702+ "checksum backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5a90e2b463010cd0e0ce9a11d4a9d5d58d9f41d4a6ba3dcaf9e68b466e88b4"
1703+ "checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
1704+ "checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
1705+ "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
1706+ "checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400"
1707+ "checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab"
1708+ "checksum block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49665c62e0e700857531fa5d3763e91b539ff1abeebd56808d378b495870d60d"
1709+ "checksum block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d75255892aeb580d3c566f213a2b6fdc1c66667839f45719ee1d30ebf2aea591"
1710+ "checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40"
1711+ "checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
1712+ "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
1713+ "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
1714+ "checksum cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "c9ce8bb087aacff865633f0bd5aeaed910fe2fe55b55f4739527f2e023a2e53d"
1715+ "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
1716+ "checksum chacha 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
1717+ "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878"
1718+ "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
1719+ "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
1720+ "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e"
1721+ "checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71"
1722+ "checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4"
1723+ "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
1724+ "checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c"
1725+ "checksum crypto-mac 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0999b4ff4d3446d4ddb19a63e9e00c1876e75cd7000d20e57a693b4b3f08d958"
1726+ "checksum ct-logs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "95a4bf5107667e12bf6ce31a3a5066d67acc88942b6742117a41198734aaccaa"
1727+ "checksum derive_more 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fbe9f11be34f800b3ecaaed0ec9ec2e015d1d0ba0c8644c1310f73d6e8994615"
1728+ "checksum digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90"
1729+ "checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c"
1730+ "checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
1731+ "checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a"
1732+ "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
1733+ "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
1734+ "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
1735+ "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
1736+ "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
1737+ "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
1738+ "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
1739+ "checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
1740+ "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
1741+ "checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592"
1742+ "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d"
1743+ "checksum h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "ddb2b25a33e231484694267af28fec74ac63b5ccf51ee2065a5e313b834d836e"
1744+ "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
1745+ "checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77"
1746+ "checksum hmac 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44f3bdb08579d99d7dc761c0e266f13b5f2ab8c8c703b9fc9ef333cd8f48f55e"
1747+ "checksum http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "fe67e3678f2827030e89cc4b9e7ecd16d52f132c0b940ab5005f88e821500f6a"
1748+ "checksum httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e8734b0cfd3bc3e101ec59100e101c2eecd19282202e87808b3037b442777a83"
1749+ "checksum human-size 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5bec6e801ef7367625bd94ad7e2965e6027189f3e9deef422388d993af2814a0"
1750+ "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
1751+ "checksum hyper 0.12.25 (registry+https://github.com/rust-lang/crates.io-index)" = "7d5b6658b016965ae301fa995306db965c93677880ea70765a84235a96eae896"
1752+ "checksum hyper-rustls 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "68f2aa6b1681795bf4da8063f718cd23145aa0c9a5143d9787b345aa60d38ee4"
1753+ "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
1754+ "checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d"
1755+ "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
1756+ "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
1757+ "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
1758+ "checksum keystream 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
1759+ "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
1760+ "checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
1761+ "checksum libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "aab692d7759f5cd8c859e169db98ae5b52c924add2af5fbbca11d12fefb567c1"
1762+ "checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e"
1763+ "checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
1764+ "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
1765+ "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
1766+ "checksum md5 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
1767+ "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
1768+ "checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
1769+ "checksum mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "71646331f2619b1026cc302f87a2b8b648d5c6dd6937846a16cc8ce0f347f432"
1770+ "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125"
1771+ "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
1772+ "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
1773+ "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
1774+ "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
1775+ "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
1776+ "checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba"
1777+ "checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409"
1778+ "checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
1779+ "checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
1780+ "checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
1781+ "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
1782+ "checksum pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8b3f4e0475def7d9c2e5de8e5a1306949849761e107b360d03e98eafaffd61"
1783+ "checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
1784+ "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
1785+ "checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
1786+ "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
1787+ "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
1788+ "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
1789+ "checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0"
1790+ "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
1791+ "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
1792+ "checksum rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9ea758282efe12823e0d952ddb269d2e1897227e464919a554f2a03ef1b832"
1793+ "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
1794+ "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
1795+ "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
1796+ "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
1797+ "checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85"
1798+ "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
1799+ "checksum redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe5204c3a17e97dde73f285d49be585df59ed84b50a872baf416e73b62c3828"
1800+ "checksum regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53ee8cfdddb2e0291adfb9f13d31d3bbe0a03c9a402c01b1e24188d86c35b24f"
1801+ "checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
1802+ "checksum ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a"
1803+ "checksum rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18a699355ef3189e3bbf34b64ff5a31f06456b689b09d05cdb4a901dcf4406a8"
1804+ "checksum rusoto_credential 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc8dd0f0a7e8b62f31aa23fa12fa0a7ac0e1eb52f6f4d4279d8a2ae51d8f099"
1805+ "checksum rusoto_s3 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1c7ed5248f4d330412be31522ef242de61a55d4c80f298dc7377121d52610ddb"
1806+ "checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
1807+ "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
1808+ "checksum rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "942b71057b31981152970d57399c25f72e27a6ee0d207a669d8304cabf44705b"
1809+ "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
1810+ "checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9"
1811+ "checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
1812+ "checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
1813+ "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
1814+ "checksum sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cb8f61f9e6eadd062a71c380043d28036304a4706b3c4dd001ff3387ed00745a"
1815+ "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
1816+ "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
1817+ "checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560"
1818+ "checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c"
1819+ "checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
1820+ "checksum serde_test 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "70807e147558b5253cb70f55d343db1d07204d773087c96d0f35fced295dba82"
1821+ "checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0"
1822+ "checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d"
1823+ "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
1824+ "checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be"
1825+ "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
1826+ "checksum string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b639411d0b9c738748b5397d5ceba08e648f4f1992231aa859af1a017f31f60b"
1827+ "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
1828+ "checksum structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)" = "3d0760c312538987d363c36c42339b55f5ee176ea8808bbe4543d484a291c8d1"
1829+ "checksum structopt-derive 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)" = "528aeb7351d042e6ffbc2a6fb76a86f9b622fdf7c25932798e7a82cb03bc94c6"
1830+ "checksum syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1825685f977249735d510a242a6727b46efe914bb67e38d30c071b1b72b1d5c2"
1831+ "checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
1832+ "checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
1833+ "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
1834+ "checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6"
1835+ "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
1836+ "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
1837+ "checksum tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "fcaabb3cec70485d0df6e9454fe514393ad1c4070dee8915f11041e95630b230"
1838+ "checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f"
1839+ "checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71"
1840+ "checksum tokio-current-thread 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c756b04680eea21902a46fca4e9f410a2332c04995af590e07ff262e2193a9a3"
1841+ "checksum tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "30c6dbf2d1ad1de300b393910e8a3aa272b724a400b6531da03eed99e329fbf0"
1842+ "checksum tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe6dc22b08d6993916647d108a1a7d15b9cd29c4f4496c62b92c45b5041b7af"
1843+ "checksum tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926"
1844+ "checksum tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6af16bfac7e112bea8b0442542161bfc41cbfa4466b580bdda7d18cb88b911ce"
1845+ "checksum tokio-rustls 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "208d62fa3e015426e3c64039d9d20adf054a3c9b4d9445560f1c41c75bef3eab"
1846+ "checksum tokio-sync 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1bf2b9dac2a0509b5cfd1df5aa25eafacb616a42a491a13604d6bbeab4486363"
1847+ "checksum tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119"
1848+ "checksum tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "742e511f6ce2298aeb86fc9ea0d8df81c2388c6ebae3dc8a7316e8c9df0df801"
1849+ "checksum tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2910970404ba6fa78c5539126a9ae2045d62e3713041e447f695f41405a120c6"
1850+ "checksum tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "66268575b80f4a4a710ef83d087fdfeeabdce9b74c797535fbac18a2cb906e92"
1851+ "checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445"
1852+ "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
1853+ "checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169"
1854+ "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
1855+ "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
1856+ "checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426"
1857+ "checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1"
1858+ "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
1859+ "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
1860+ "checksum untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f"
1861+ "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
1862+ "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
1863+ "checksum uuid 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0238db0c5b605dd1cf51de0f21766f97fba2645897024461d6a00c036819a768"
1864+ "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
1865+ "checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3"
1866+ "checksum webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "17d7967316d8411ca3b01821ee6c332bde138ba4363becdb492f12e514daa17f"
1867+ "checksum webpki-roots 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "85d1f408918fd590908a70d36b7ac388db2edc221470333e4d6e5b598e44cabf"
1868+ "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
1869+ "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
1870+ "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
1871+ "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1872+ "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
1873+ "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1874+ "checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
1875+ "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
1876+ "checksum xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c1cb601d29fe2c2ac60a2b2e5e293994d87a1f6fa9687a31a15270f909be9c2"
1877 diff --git a/Cargo.toml b/Cargo.toml
1878new file mode 100644
1879index 0000000..aec1174
1880--- /dev/null
1881+++ b/Cargo.toml
1882 @@ -0,0 +1,37 @@
1883+ [package]
1884+ name = "rudolfs"
1885+ version = "0.1.0"
1886+ authors = ["Jason White"]
1887+ edition = "2018"
1888+
1889+ [dependencies]
1890+ bytes = "0.4"
1891+ chacha = "0.3"
1892+ derive_more = "0.14"
1893+ futures = "0.1"
1894+ generic-array = "0.12"
1895+ hex = "0.3"
1896+ http = "0.1"
1897+ human-size = "0.4"
1898+ humantime = "1"
1899+ hyper = "0.12"
1900+ linked-hash-map = { version = "0.5", features = ["serde_impl"] }
1901+ log = "0.4"
1902+ pretty_env_logger = "0.3"
1903+ serde = { version = "1", features = ["derive"] }
1904+ serde_json = "1"
1905+ sha2 = "0.8"
1906+ structopt = "0.2"
1907+ tokio = "0.1"
1908+ url = "1"
1909+ uuid = { version = "0.7", features = ["v4"] }
1910+
1911+ [dependencies.rusoto_core]
1912+ version = "0.36"
1913+ default_features = false
1914+ features = ["rustls"]
1915+
1916+ [dependencies.rusoto_s3]
1917+ version = "0.36"
1918+ default_features = false
1919+ features = ["rustls"]
1920 diff --git a/Dockerfile b/Dockerfile
1921new file mode 100644
1922index 0000000..a8aa95b
1923--- /dev/null
1924+++ b/Dockerfile
1925 @@ -0,0 +1,50 @@
1926+ FROM rust:1.33 as build
1927+
1928+ ENV CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
1929+
1930+ ENV DEBIAN_FRONTEND=noninteractive
1931+ RUN \
1932+ apt-get update && \
1933+ apt-get -y install ca-certificates musl-tools && \
1934+ rustup target add ${CARGO_BUILD_TARGET}
1935+
1936+ # Use Tini as our PID 1. This will enable signals to be handled more correctly.
1937+ #
1938+ # Note that this can't be downloaded inside the scratch container as we have no
1939+ # chmod command.
1940+ #
1941+ # TODO: Use `--init` instead when it is more well-supported (this should be the
1942+ # case by Jan 1, 2020).
1943+ ENV TINI_VERSION v0.18.0
1944+ ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini
1945+ RUN chmod +x /tini
1946+
1947+ WORKDIR /source
1948+
1949+ ENV PKG_CONFIG_ALLOW_CROSS=1
1950+
1951+ # Build the real project.
1952+ COPY ./ ./
1953+
1954+ RUN cargo build --release
1955+
1956+ RUN \
1957+ mkdir -p /build && \
1958+ cp target/${CARGO_BUILD_TARGET}/release/rudolfs /build/ && \
1959+ strip /build/rudolfs
1960+
1961+ # Use scratch so we can get an itty-bitty-teeny-tiny image. This requires us to
1962+ # use musl when building the application.
1963+ FROM scratch
1964+
1965+ EXPOSE 8080
1966+ VOLUME ["/data"]
1967+
1968+ COPY --from=build /tini /tini
1969+
1970+ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
1971+
1972+ COPY --from=build /build/ /
1973+
1974+ ENTRYPOINT ["/tini", "--", "/rudolfs"]
1975+ CMD ["--cache-dir", "/data"]
1976 diff --git a/LICENSE b/LICENSE
1977new file mode 100644
1978index 0000000..ddfd2cb
1979--- /dev/null
1980+++ b/LICENSE
1981 @@ -0,0 +1,18 @@
1982+ Copyright (c) 2019 Jason White
1983+
1984+ Permission is hereby granted, free of charge, to any person obtaining a copy of
1985+ this software and associated documentation files (the "Software"), to deal in
1986+ the Software without restriction, including without limitation the rights to
1987+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
1988+ the Software, and to permit persons to whom the Software is furnished to do so,
1989+ subject to the following conditions:
1990+
1991+ The above copyright notice and this permission notice shall be included in all
1992+ copies or substantial portions of the Software.
1993+
1994+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1995+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
1996+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
1997+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
1998+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
1999+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2000 diff --git a/README.md b/README.md
2001new file mode 100644
2002index 0000000..524a0a4
2003--- /dev/null
2004+++ b/README.md
2005 @@ -0,0 +1,142 @@
2006+ # Rudo-LFS (aka "rudolfs")
2007+
2008+ A high-performance, caching Git LFS server with an AWS S3 back-end.
2009+
2010+ ## Features
2011+
2012+ - AWS S3 permanent storage back-end.
2013+
2014+ - A configurable local disk cache to speed up downloads (and reduce your
2015+ S3 bill).
2016+
2017+ - Corruption-resilient local disk cache. Even if the disk is getting
2018+ blasted by cosmic rays, it'll find corrupted LFS objects and purge them from
2019+ the cache transparently. The client should never notice this happening.
2020+
2021+ - Encryption of LFS objects in both the cache and in permanent storage.
2022+
2023+ ## Running It
2024+
2025+ ### Generate an encryption key
2026+
2027+ All LFS objects are encrypted with the xchacha20 symmetric stream cipher. You
2028+ must generate a 32-byte encryption key before starting the server.
2029+
2030+ Generating a random key is easy:
2031+
2032+ openssl rand -hex 32
2033+
2034+ Keep this secret and save it in a password manager so you don't lose it. We will
2035+ pass this to the server below.
2036+
2037+ **Note**:
2038+ - If the key ever changes, all existing LFS objects will become garbage.
2039+ When the Git LFS client attempts to download them, the SHA256 verification
2040+ step will fail.
2041+ - LFS objects in both the cache and in permanent storage are encrypted.
2042+ However, objects are decrypted before being sent to the LFS client, so take
2043+ any necessary precautions to keep your intellectual property safe.
2044+
2045+ ### Development
2046+
2047+ For testing during development, it is easiest to run it with Cargo. Create
2048+ a file called `test.sh` (this path is already ignored by `.gitignore`):
2049+
2050+ ```bash
2051+ # Your AWS credentials.
2052+ export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
2053+ export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2054+ export AWS_DEFAULT_REGION=us-west-1
2055+
2056+ # Change this to the output of `openssl rand -hex 32`.
2057+ KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2058+
2059+ cargo run -- \
2060+ --cache-dir cache \
2061+ --host localhost:8080 \
2062+ --s3-bucket foobar \
2063+ --max-cache-size 10GiB \
2064+ --key $KEY
2065+ ```
2066+
2067+ **Note**: Always use a different S3 bucket, cache directory, and encryption key
2068+ than what you use in your production environment.
2069+
2070+ ### Production
2071+
2072+ To run in a production environment, it is easiest to use `docker-compose`:
2073+
2074+ 1. Create a `.env` file next to `docker-compose.yml` with the configuration
2075+ variables:
2076+
2077+ ```
2078+ AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXQ
2079+ AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2080+ AWS_DEFAULT_REGION=us-west-1
2081+ LFS_ENCRYPTION_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2082+ LFS_S3_BUCKET=my-bucket
2083+ LFS_MAX_CACHE_SIZE=10GB
2084+ ```
2085+
2086+ 2. Use the provided `docker-compose.yml` file to run a production environment:
2087+
2088+ ```bash
2089+ docker-compose up -d
2090+ ```
2091+
2092+ 3. **[Optional]**: It is best to use nginx as a reverse proxy for this server.
2093+ Use it to enable TLS. How to configure this is better covered by other
2094+ tutorials on the internet.
2095+
2096+ **Note**:
2097+ - A bigger cache is (almost) always better. Try to use ~85% of the available
2098+ disk space.
2099+ - The cache data is stored in a Docker volume named `rudolfs_data`. If you
2100+ want to delete it, run `docker volume rm rudolfs_data`.
2101+
2102+ ## AWS Credentials
2103+
2104+ AWS credentials must be provided to the server so that it can make requests to
2105+ the S3 bucket specified on the command line (with `--s3-bucket`).
2106+
2107+ Your AWS credentials will be searched for in the following order:
2108+
2109+ 1. Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
2110+ 2. AWS credentials file. Usually located at `~/.aws/credentials`.
2111+ 3. IAM instance profile. Will only work if running on an EC2 instance with an
2112+ instance profile/role.
2113+
2114+ The AWS region is read from the `AWS_DEFAULT_REGION` or `AWS_REGION` environment
2115+ variable. If it is malformed, it will fall back to `us-east-1`. If it is not
2116+ present it will fall back on the value associated with the current profile in
2117+ `~/.aws/config` or the file specified by the `AWS_CONFIG_FILE` environment
2118+ variable. If that is malformed or absent it will fall back to `us-east-1`.
2119+
2120+ ## Client Configuration
2121+
2122+ Add `.lfsconfig` to the root of your Git repository:
2123+
2124+ ```
2125+ [lfs]
2126+ url = "http://gitlfs.example.com:8080/"
2127+ ```
2128+
2129+ Optionally, I also recommend changing these global settings to speed things up:
2130+
2131+ ``` bash
2132+ # Increase the number of worker threads
2133+ git config --global lfs.concurrenttransfers 64
2134+
2135+ # Use a global LFS cache to make re-cloning faster
2136+ git config --global lfs.storage ~/.cache/lfs
2137+ ```
2138+
2139+ ## License
2140+
2141+ [MIT License](/LICENSE)
2142+
2143+ ## Thanks
2144+
2145+ This was developed at [Environmental Systems Research
2146+ Institute](http://www.esri.com/) (Esri) who have graciously allowed me to retain
2147+ the copyright and publish it as open source software.
2148 diff --git a/docker-compose.yml b/docker-compose.yml
2149new file mode 100644
2150index 0000000..84e4037
2151--- /dev/null
2152+++ b/docker-compose.yml
2153 @@ -0,0 +1,48 @@
2154+ # Copy this file to another location and modify as necessary.
2155+ version: '3'
2156+ services:
2157+ app:
2158+ #image: rudolfs:latest
2159+ build:
2160+ context: .
2161+ dockerfile: Dockerfile
2162+ ports:
2163+ - "8081:8080"
2164+ volumes:
2165+ - data:/data
2166+ restart: always
2167+ environment:
2168+ - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
2169+ - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
2170+ - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
2171+ - LFS_ENCRYPTION_KEY=${LFS_ENCRYPTION_KEY}
2172+ - LFS_S3_BUCKET=${LFS_S3_BUCKET}
2173+ - LFS_MAX_CACHE_SIZE=${LFS_MAX_CACHE_SIZE}
2174+ entrypoint:
2175+ - /tini
2176+ - --
2177+ - /rudolfs
2178+ - --cache-dir
2179+ - /data
2180+ - --key
2181+ - ${LFS_ENCRYPTION_KEY}
2182+ - --s3-bucket
2183+ - ${LFS_S3_BUCKET}
2184+ - --max-cache-size
2185+ - ${LFS_MAX_CACHE_SIZE}
2186+
2187+ # A real production server should use nginx. How to configure this depends on
2188+ # your needs. Use your Google-search skills to configure this correctly.
2189+ #
2190+ # nginx:
2191+ # image: nginx:stable
2192+ # ports:
2193+ # - 80:80
2194+ # - 443:443
2195+ # volumes:
2196+ # - ./nginx.conf:/etc/nginx/nginx.conf
2197+ # - ./nginx/errors.log:/etc/nginx/errors.log
2198+
2199+
2200+ volumes:
2201+ data:
2202 diff --git a/rustfmt.toml b/rustfmt.toml
2203new file mode 100644
2204index 0000000..2451809
2205--- /dev/null
2206+++ b/rustfmt.toml
2207 @@ -0,0 +1,6 @@
2208+ max_width = 80
2209+ error_on_line_overflow = true
2210+ error_on_unformatted = true
2211+ normalize_comments = true
2212+ wrap_comments = true
2213+ license_template_path = ".license_template"
2214 diff --git a/src/app.rs b/src/app.rs
2215new file mode 100644
2216index 0000000..4dfa7ed
2217--- /dev/null
2218+++ b/src/app.rs
2219 @@ -0,0 +1,429 @@
2220+ // Copyright (c) 2019 Jason White
2221+ //
2222+ // Permission is hereby granted, free of charge, to any person obtaining a copy
2223+ // of this software and associated documentation files (the "Software"), to deal
2224+ // in the Software without restriction, including without limitation the rights
2225+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2226+ // copies of the Software, and to permit persons to whom the Software is
2227+ // furnished to do so, subject to the following conditions:
2228+ //
2229+ // The above copyright notice and this permission notice shall be included in
2230+ // all copies or substantial portions of the Software.
2231+ //
2232+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2233+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2234+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2235+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2236+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2237+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2238+ // SOFTWARE.
2239+ use std::io;
2240+ use std::sync::Arc;
2241+
2242+ use futures::{
2243+ future::{self, Either},
2244+ Future, IntoFuture, Stream,
2245+ };
2246+ use http::{
2247+ self, header,
2248+ uri::{Authority, Scheme},
2249+ StatusCode, Uri,
2250+ };
2251+ use hyper::{self, service::Service, Method, Request, Response, Chunk};
2252+
2253+ use crate::error::Error;
2254+ use crate::hyperext::{into_request, Body, IntoResponse, RequestExt};
2255+ use crate::lfs;
2256+ use crate::storage::{LFSObject, Storage};
2257+
2258+ /// Shared state for all instances of the `App` service.
2259+ pub struct State<S> {
2260+ // Storage backend.
2261+ storage: S,
2262+ }
2263+
2264+ impl<S> State<S> {
2265+ pub fn new(storage: S) -> Self {
2266+ State { storage }
2267+ }
2268+ }
2269+
2270+ #[derive(Clone)]
2271+ pub struct App<S> {
2272+ state: Arc<State<S>>,
2273+ }
2274+
2275+ impl<S> App<S>
2276+ where
2277+ S: Storage + Send + Sync + 'static,
2278+ S::Error: Into<Error> + 'static,
2279+ Error: From<S::Error>,
2280+ {
2281+ pub fn new(state: Arc<State<S>>) -> Self {
2282+ App { state }
2283+ }
2284+
2285+ /// Downloads a single LFS object.
2286+ fn download(
2287+ &mut self,
2288+ _req: Request<Body>,
2289+ oid: lfs::Oid,
2290+ ) -> impl Future<Item = Response<Body>, Error = Error> {
2291+ self.state.storage.get(&oid).from_err::<Error>().and_then(
2292+ move |object| -> Result<_, Error> {
2293+ if let Some(object) = object {
2294+ return Response::builder()
2295+ .status(StatusCode::OK)
2296+ .header(
2297+ header::CONTENT_TYPE,
2298+ "application/octet-stream",
2299+ )
2300+ .header(header::CONTENT_LENGTH, object.len())
2301+ .body(Body::wrap_stream(object.stream()))
2302+ .map_err(Into::into);
2303+ } else {
2304+ return Response::builder()
2305+ .status(StatusCode::NOT_FOUND)
2306+ .body(Body::empty())
2307+ .map_err(Into::into);
2308+ }
2309+ },
2310+ )
2311+ }
2312+
2313+ /// Uploads a single LFS object.
2314+ fn upload(
2315+ &mut self,
2316+ req: Request<Body>,
2317+ oid: lfs::Oid,
2318+ ) -> <Self as Service>::Future {
2319+ let len = req
2320+ .headers()
2321+ .get("Content-Length")
2322+ .and_then(|v| v.to_str().ok())
2323+ .and_then(|s| s.parse::<u64>().ok());
2324+
2325+ let len = match len {
2326+ Some(len) => len,
2327+ None => {
2328+ return Box::new(
2329+ Response::builder()
2330+ .status(StatusCode::BAD_REQUEST)
2331+ .body(Body::from("Invalid Content-Length header."))
2332+ .map_err(Into::into)
2333+ .into_future(),
2334+ );
2335+ }
2336+ };
2337+
2338+ // Verify the SHA256 of the uploaded object as it is being uploaded.
2339+ let stream = req
2340+ .into_body()
2341+ .map(Chunk::into_bytes)
2342+ .from_err::<Error>()
2343+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e));
2344+
2345+ let object = LFSObject::new(len, Box::new(stream));
2346+
2347+ Box::new(
2348+ self.state
2349+ .storage
2350+ .put(&oid, object)
2351+ .from_err::<Error>()
2352+ .and_then(|_| {
2353+ Response::builder()
2354+ .status(StatusCode::OK)
2355+ .body(Body::empty())
2356+ .map_err(Into::into)
2357+ }),
2358+ )
2359+ }
2360+
2361+ /// Verifies that an LFS object exists on the server.
2362+ fn verify(
2363+ &mut self,
2364+ req: Request<Body>,
2365+ ) -> impl Future<Item = Response<Body>, Error = Error> {
2366+ let state = self.state.clone();
2367+
2368+ req.into_body()
2369+ .into_json()
2370+ .and_then(move |val: lfs::VerifyRequest| {
2371+ state.storage.size(&val.oid).from_err::<Error>().and_then(
2372+ move |size| {
2373+ if let Some(size) = size {
2374+ if size == val.size {
2375+ return Response::builder()
2376+ .status(StatusCode::OK)
2377+ .body(Body::empty())
2378+ .map_err(Into::into);
2379+ }
2380+ }
2381+
2382+ // Object doesn't exist or the size is incorrect.
2383+ Response::builder()
2384+ .status(StatusCode::NOT_FOUND)
2385+ .body(Body::empty())
2386+ .map_err(Into::into)
2387+ },
2388+ )
2389+ })
2390+ }
2391+
2392+ /// Generates a "404 not found" response.
2393+ fn not_found(
2394+ &mut self,
2395+ _req: Request<Body>,
2396+ ) -> Result<Response<Body>, Error> {
2397+ Ok(Response::builder()
2398+ .status(StatusCode::NOT_FOUND)
2399+ .body("Not found".into())?)
2400+ }
2401+
2402+ /// Batch API endpoint for the Git LFS server spec.
2403+ ///
2404+ /// See also:
2405+ /// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
2406+ fn batch(
2407+ &mut self,
2408+ req: Request<Body>,
2409+ ) -> impl Future<Item = Response<Body>, Error = Error> {
2410+ // Get the host name and scheme.
2411+ let uri = Uri::builder()
2412+ .scheme(req.scheme().unwrap_or(Scheme::HTTP))
2413+ .authority(
2414+ req.authority()
2415+ .unwrap_or_else(|| Authority::from_static("localhost")),
2416+ )
2417+ .path_and_query("/")
2418+ .build()
2419+ .unwrap();
2420+
2421+ let state = self.state.clone();
2422+
2423+ req.into_body().into_json().then(
2424+ move |result: Result<lfs::BatchRequest, _>| {
2425+ match result {
2426+ Ok(val) => {
2427+ let operation = val.operation;
2428+
2429+ // For each object, check if it exists in the storage
2430+ // backend.
2431+ let objects =
2432+ val.objects.into_iter().map(move |object| {
2433+ let uri = uri.clone();
2434+
2435+ state.storage.size(&object.oid).map(
2436+ move |size| {
2437+ basic_response(
2438+ uri, object, operation, size,
2439+ )
2440+ },
2441+ )
2442+ });
2443+
2444+ Either::A(
2445+ future::join_all(objects)
2446+ .from_err::<Error>()
2447+ .and_then(|objects| {
2448+ let response = lfs::BatchResponse {
2449+ transfer: Some(lfs::Transfer::Basic),
2450+ objects,
2451+ };
2452+
2453+ Response::builder()
2454+ .status(StatusCode::OK)
2455+ .header(
2456+ header::CONTENT_TYPE,
2457+ "application/json",
2458+ )
2459+ .body(Body::json(&response)?)
2460+ .map_err(Into::into)
2461+ }),
2462+ )
2463+ }
2464+ Err(err) => {
2465+ let response = lfs::BatchResponseError {
2466+ message: err.to_string(),
2467+ documentation_url: None,
2468+ request_id: None,
2469+ };
2470+
2471+ Either::B(
2472+ Response::builder()
2473+ .status(StatusCode::BAD_REQUEST)
2474+ .body(Body::json(&response).unwrap())
2475+ .map_err(Into::into)
2476+ .into_future(),
2477+ )
2478+ }
2479+ }
2480+ },
2481+ )
2482+ }
2483+ }
2484+
2485+ fn basic_response(
2486+ uri: Uri,
2487+ object: lfs::RequestObject,
2488+ op: lfs::Operation,
2489+ size: Option<u64>,
2490+ ) -> lfs::ResponseObject {
2491+ if let Some(size) = size {
2492+ // Ensure that the client and server agree on the size of the object.
2493+ if object.size != size {
2494+ return lfs::ResponseObject {
2495+ oid: object.oid,
2496+ size,
2497+ error: Some(lfs::ObjectError {
2498+ code: 400,
2499+ message: format!(
2500+ "bad object size: requested={}, actual={}",
2501+ object.size, size
2502+ ),
2503+ }),
2504+ authenticated: None,
2505+ actions: None,
2506+ };
2507+ }
2508+ }
2509+
2510+ let href = format!("{}object/{}", uri, object.oid);
2511+
2512+ let action = lfs::Action {
2513+ href,
2514+ header: None,
2515+ expires_in: None,
2516+ expires_at: None,
2517+ };
2518+
2519+ match op {
2520+ lfs::Operation::Upload => {
2521+ // If the object does exist, then we should not return any action.
2522+ //
2523+ // If the object does not exist, then we should return an upload
2524+ // action.
2525+ match size {
2526+ Some(size) => lfs::ResponseObject {
2527+ oid: object.oid,
2528+ size,
2529+ error: None,
2530+ authenticated: Some(true),
2531+ actions: None,
2532+ },
2533+ None => lfs::ResponseObject {
2534+ oid: object.oid,
2535+ size: object.size,
2536+ error: None,
2537+ authenticated: Some(true),
2538+ actions: Some(lfs::Actions {
2539+ download: None,
2540+ upload: Some(action.clone()),
2541+ verify: Some(lfs::Action {
2542+ href: format!("{}objects/verify", uri),
2543+ header: None,
2544+ expires_in: None,
2545+ expires_at: None,
2546+ }),
2547+ }),
2548+ },
2549+ }
2550+ }
2551+ lfs::Operation::Download => {
2552+ // If the object does not exist, then we should return a 404 error
2553+ // for this object.
2554+ match size {
2555+ Some(size) => lfs::ResponseObject {
2556+ oid: object.oid,
2557+ size,
2558+ error: None,
2559+ authenticated: Some(true),
2560+ actions: Some(lfs::Actions {
2561+ download: Some(action),
2562+ upload: None,
2563+ verify: None,
2564+ }),
2565+ },
2566+ None => lfs::ResponseObject {
2567+ oid: object.oid,
2568+ size: object.size,
2569+ error: Some(lfs::ObjectError {
2570+ code: 404,
2571+ message: "not found".into(),
2572+ }),
2573+ authenticated: Some(true),
2574+ actions: None,
2575+ },
2576+ }
2577+ }
2578+ }
2579+ }
2580+
2581+ impl<S> Service for App<S>
2582+ where
2583+ S: Storage + Send + Sync + 'static,
2584+ S::Error: Into<Error> + 'static,
2585+ Error: From<S::Error>,
2586+ {
2587+ type ReqBody = hyper::Body;
2588+ type ResBody = Body;
2589+ type Error = Error;
2590+ type Future = Box<dyn Future<Item = Response<Body>, Error = Error> + Send>;
2591+
2592+ fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
2593+ let req = into_request(req);
2594+
2595+ if req.uri().path().starts_with("/object/") {
2596+ // Extract the OID from the request.
2597+ let mut parts =
2598+ req.uri().path().split('/').filter(|s| !s.is_empty());
2599+
2600+ // Skip over the 'object/' part.
2601+ parts.next();
2602+
2603+ let oid = match parts.next() {
2604+ Some(oid) => oid,
2605+ None => {
2606+ return Response::builder()
2607+ .status(StatusCode::BAD_REQUEST)
2608+ .body(Body::from("Missing OID parameter."))
2609+ .map_err(Into::into)
2610+ .response();
2611+ }
2612+ };
2613+
2614+ let oid = match oid.parse::<lfs::Oid>() {
2615+ Ok(oid) => oid,
2616+ Err(err) => {
2617+ return Response::builder()
2618+ .status(StatusCode::BAD_REQUEST)
2619+ .body(Body::from(format!("Invalid OID: {}", err)))
2620+ .map_err(Into::into)
2621+ .response();
2622+ }
2623+ };
2624+
2625+ match *req.method() {
2626+ Method::GET => return self.download(req, oid).response(),
2627+ Method::PUT => return self.upload(req, oid),
2628+ _ => return self.not_found(req).response(),
2629+ }
2630+ }
2631+
2632+ match (req.method(), req.uri().path()) {
2633+ (&Method::POST, "/objects/batch") => {
2634+ return self.batch(req).response();
2635+ }
2636+ (&Method::POST, "/objects/verify") => {
2637+ return self.verify(req).response();
2638+ }
2639+ _ => {}
2640+ }
2641+
2642+ Response::builder()
2643+ .status(StatusCode::NOT_FOUND)
2644+ .body(Body::empty())
2645+ .map_err(Into::into)
2646+ .response()
2647+ }
2648+ }
2649 diff --git a/src/error.rs b/src/error.rs
2650new file mode 100644
2651index 0000000..419087d
2652--- /dev/null
2653+++ b/src/error.rs
2654 @@ -0,0 +1,43 @@
2655+ // Copyright (c) 2019 Jason White
2656+ //
2657+ // Permission is hereby granted, free of charge, to any person obtaining a copy
2658+ // of this software and associated documentation files (the "Software"), to deal
2659+ // in the Software without restriction, including without limitation the rights
2660+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2661+ // copies of the Software, and to permit persons to whom the Software is
2662+ // furnished to do so, subject to the following conditions:
2663+ //
2664+ // The above copyright notice and this permission notice shall be included in
2665+ // all copies or substantial portions of the Software.
2666+ //
2667+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2668+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2669+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2670+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2671+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2672+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2673+ // SOFTWARE.
2674+ use std::io;
2675+
2676+ use derive_more::{Display, From};
2677+ use http;
2678+ use hyper;
2679+ use serde_json;
2680+
2681+ use crate::sha256::{Sha256Error, Sha256VerifyError};
2682+ use crate::storage::{self, Storage};
2683+
2684+ // Define a type so we can return multiple types of errors
2685+ #[derive(Debug, From, Display)]
2686+ pub enum Error {
2687+ Io(io::Error),
2688+ Http(http::Error),
2689+ Hyper(hyper::Error),
2690+ Json(serde_json::Error),
2691+ Sha256Error(Sha256Error),
2692+ Sha256VerifyError(Sha256VerifyError),
2693+ S3(storage::S3Error),
2694+ S3DiskCache(<storage::S3DiskCache as Storage>::Error),
2695+ }
2696+
2697+ impl std::error::Error for Error {}
2698 diff --git a/src/hyperext.rs b/src/hyperext.rs
2699new file mode 100644
2700index 0000000..c6cdc80
2701--- /dev/null
2702+++ b/src/hyperext.rs
2703 @@ -0,0 +1,218 @@
2704+ // Copyright (c) 2019 Jason White
2705+ //
2706+ // Permission is hereby granted, free of charge, to any person obtaining a copy
2707+ // of this software and associated documentation files (the "Software"), to deal
2708+ // in the Software without restriction, including without limitation the rights
2709+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2710+ // copies of the Software, and to permit persons to whom the Software is
2711+ // furnished to do so, subject to the following conditions:
2712+ //
2713+ // The above copyright notice and this permission notice shall be included in
2714+ // all copies or substantial portions of the Software.
2715+ //
2716+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2717+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2718+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2719+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2720+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2721+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2722+ // SOFTWARE.
2723+ use std::ops::Deref;
2724+
2725+ use derive_more::From;
2726+ use futures::{Future, IntoFuture, Poll, Stream};
2727+ use http::uri::{Authority, Scheme};
2728+ use hyper::{
2729+ body::{Payload, Sender},
2730+ Body as HBody, Chunk, HeaderMap, Request, Response,
2731+ };
2732+ use serde::{Deserialize, Serialize};
2733+
2734+ use crate::error::Error;
2735+
2736+ #[derive(Debug, Default, From)]
2737+ pub struct Body(HBody);
2738+
2739+ impl Body {
2740+ pub fn empty() -> Self {
2741+ HBody::empty().into()
2742+ }
2743+
2744+ #[allow(unused)]
2745+ pub fn channel() -> (Sender, Self) {
2746+ let (sender, body) = HBody::channel();
2747+ (sender, body.into())
2748+ }
2749+
2750+ /// Deserializes the body as json. Returns a future of the deserialized
2751+ /// json.
2752+ pub fn into_json<T>(self) -> impl Future<Item = T, Error = Error>
2753+ where
2754+ T: for<'de> Deserialize<'de>,
2755+ {
2756+ self.concat2()
2757+ .from_err::<Error>()
2758+ .and_then(|body| Ok(serde_json::from_slice(&body)?))
2759+ .from_err()
2760+ }
2761+
2762+ /// Serialize a value to JSON.
2763+ pub fn json<T>(value: &T) -> Result<Self, Error>
2764+ where
2765+ T: Serialize,
2766+ {
2767+ let body: HBody = serde_json::to_vec_pretty(&value)?.into();
2768+ Ok(body.into())
2769+ }
2770+
2771+ pub fn wrap_stream<S>(stream: S) -> Self
2772+ where
2773+ S: Stream + Send + 'static,
2774+ S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
2775+ Chunk: From<S::Item>,
2776+ {
2777+ Body(HBody::wrap_stream(stream))
2778+ }
2779+ }
2780+
2781+ impl Into<HBody> for Body {
2782+ fn into(self) -> HBody {
2783+ self.0
2784+ }
2785+ }
2786+
2787+ impl From<String> for Body {
2788+ fn from(s: String) -> Self {
2789+ Body(s.into())
2790+ }
2791+ }
2792+
2793+ impl From<&'static str> for Body {
2794+ fn from(slice: &'static str) -> Self {
2795+ Body(slice.into())
2796+ }
2797+ }
2798+
2799+ type ChunkedStream = Box<
2800+ dyn Stream<
2801+ Item = Chunk,
2802+ Error = Box<dyn std::error::Error + 'static + Sync + Send>,
2803+ >
2804+ + 'static
2805+ + Send,
2806+ >;
2807+
2808+ impl From<ChunkedStream> for Body {
2809+ fn from(stream: ChunkedStream) -> Self {
2810+ Body(stream.into())
2811+ }
2812+ }
2813+
2814+ impl Deref for Body {
2815+ type Target = HBody;
2816+
2817+ fn deref(&self) -> &Self::Target {
2818+ &self.0
2819+ }
2820+ }
2821+
2822+ impl Stream for Body {
2823+ type Item = <HBody as Stream>::Item;
2824+ type Error = <HBody as Stream>::Error;
2825+
2826+ fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
2827+ self.0.poll()
2828+ }
2829+ }
2830+
2831+ impl Payload for Body {
2832+ type Data = <HBody as Payload>::Data;
2833+ type Error = <HBody as Payload>::Error;
2834+
2835+ fn poll_data(&mut self) -> Poll<Option<Self::Data>, Self::Error> {
2836+ <HBody as Payload>::poll_data(&mut self.0)
2837+ }
2838+
2839+ fn poll_trailers(&mut self) -> Poll<Option<HeaderMap>, Self::Error> {
2840+ <HBody as Payload>::poll_trailers(&mut self.0)
2841+ }
2842+
2843+ fn is_end_stream(&self) -> bool {
2844+ <HBody as Payload>::is_end_stream(&self.0)
2845+ }
2846+
2847+ fn content_length(&self) -> Option<u64> {
2848+ <HBody as Payload>::content_length(&self.0)
2849+ }
2850+ }
2851+
2852+ type BoxResponse<T> = Box<dyn Future<Item = Response<T>, Error = Error> + Send>;
2853+
2854+ pub trait IntoResponse<T> {
2855+ /// Convert something into a response.
2856+ fn response(self) -> BoxResponse<T>;
2857+ }
2858+
2859+ impl<F, T, U> IntoResponse<U> for F
2860+ where
2861+ F: IntoFuture<Item = Response<T>, Error = Error>,
2862+ F::Future: Send + 'static,
2863+ T: Into<U>,
2864+ {
2865+ fn response(self) -> BoxResponse<U> {
2866+ Box::new(
2867+ self.into_future()
2868+ .map(move |r| -> Response<U> { into_response(r) }),
2869+ )
2870+ }
2871+ }
2872+
2873+ pub fn into_request<T, U>(req: Request<T>) -> Request<U>
2874+ where
2875+ T: Into<U>,
2876+ {
2877+ let (parts, body) = req.into_parts();
2878+ Request::from_parts(parts, body.into())
2879+ }
2880+
2881+ fn into_response<T, U>(res: Response<T>) -> Response<U>
2882+ where
2883+ T: Into<U>,
2884+ {
2885+ let (parts, body) = res.into_parts();
2886+ Response::from_parts(parts, body.into())
2887+ }
2888+
2889+ pub trait RequestExt {
2890+ /// Gets the scheme based on the headers in the request.
2891+ ///
2892+ /// There's no good way to determine the scheme for HTTP/1, so we must defer
2893+ /// to other methods in that case (such as from a server configuration
2894+ /// file).
2895+ ///
2896+ /// HTTP/2 has the `:scheme` header that can tell us the scheme.
2897+ fn scheme(&self) -> Option<Scheme>;
2898+
2899+ /// Gets the authority based on the headers in the request.
2900+ ///
2901+ /// First checks the HTTP/2 header `:athority` and then falls back to the
2902+ /// `Host` header.
2903+ fn authority(&self) -> Option<Authority>;
2904+ }
2905+
2906+ impl<B> RequestExt for Request<B> {
2907+ fn scheme(&self) -> Option<Scheme> {
2908+ self.headers().get(":scheme").and_then(|scheme| {
2909+ Scheme::from_shared(scheme.as_bytes().into()).ok()
2910+ })
2911+ }
2912+
2913+ fn authority(&self) -> Option<Authority> {
2914+ self.headers()
2915+ .get(":authority")
2916+ .or_else(|| self.headers().get("Host"))
2917+ .and_then(|authority| {
2918+ Authority::from_shared(authority.as_bytes().into()).ok()
2919+ })
2920+ }
2921+ }
2922 diff --git a/src/lfs.rs b/src/lfs.rs
2923new file mode 100644
2924index 0000000..f6e7b6a
2925--- /dev/null
2926+++ b/src/lfs.rs
2927 @@ -0,0 +1,194 @@
2928+ // Copyright (c) 2019 Jason White
2929+ //
2930+ // Permission is hereby granted, free of charge, to any person obtaining a copy
2931+ // of this software and associated documentation files (the "Software"), to deal
2932+ // in the Software without restriction, including without limitation the rights
2933+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2934+ // copies of the Software, and to permit persons to whom the Software is
2935+ // furnished to do so, subject to the following conditions:
2936+ //
2937+ // The above copyright notice and this permission notice shall be included in
2938+ // all copies or substantial portions of the Software.
2939+ //
2940+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2941+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2942+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2943+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2944+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2945+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2946+ // SOFTWARE.
2947+ use std::collections::BTreeMap;
2948+
2949+ use serde::{Deserialize, Serialize};
2950+
2951+ use crate::sha256::Sha256;
2952+
2953+ pub type Oid = Sha256;
2954+
2955+ #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
2956+ #[serde(rename_all = "lowercase")]
2957+ pub enum Operation {
2958+ Upload,
2959+ Download,
2960+ }
2961+
2962+ /// A transfer adaptor.
2963+ #[derive(Clone, Debug, Serialize, Deserialize)]
2964+ #[serde(rename_all = "lowercase")]
2965+ pub enum Transfer {
2966+ /// Basic transfer adaptor.
2967+ Basic,
2968+ }
2969+
2970+ /// An LFS object in a request.
2971+ #[derive(Clone, Debug, Serialize, Deserialize)]
2972+ pub struct RequestObject {
2973+ /// String OID of the LFS object.
2974+ pub oid: Oid,
2975+
2976+ /// Integer byte size of the LFS object.
2977+ pub size: u64,
2978+ }
2979+
2980+ #[derive(Clone, Default, Debug, Serialize, Deserialize)]
2981+ pub struct Action {
2982+ /// URL to hit for the object.
2983+ pub href: String,
2984+
2985+ /// Optional hash of string HTTP header key/value pairs to apply to the
2986+ /// request.
2987+ pub header: Option<BTreeMap<String, String>>,
2988+
2989+ /// Whole number of seconds after local client time when transfer will
2990+ /// expire. Preferred over `expires_at` if both are provided.
2991+ #[serde(skip_serializing_if = "Option::is_none")]
2992+ pub expires_in: Option<i32>,
2993+
2994+ /// String ISO 8601 formatted timestamp for when the given action expires
2995+ /// (usually due to a temporary token).
2996+ #[serde(skip_serializing_if = "Option::is_none")]
2997+ pub expires_at: Option<String>,
2998+ }
2999+
3000+ #[derive(Debug, Serialize, Deserialize)]
3001+ pub struct ObjectError {
3002+ /// HTTP response code.
3003+ pub code: u32,
3004+
3005+ /// Error message.
3006+ pub message: String,
3007+ }
3008+
3009+ #[derive(Debug, Serialize, Deserialize)]
3010+ pub struct Actions {
3011+ /// A download action. The action href is to receive a GET request. The
3012+ /// response contains the LFS object data.
3013+ #[serde(skip_serializing_if = "Option::is_none")]
3014+ pub download: Option<Action>,
3015+
3016+ /// An upload action. The action href is to receive a PUT request with the
3017+ /// LFS object data.
3018+ #[serde(skip_serializing_if = "Option::is_none")]
3019+ pub upload: Option<Action>,
3020+
3021+ /// A verify action. The action href is to receive a POST request after
3022+ /// a successful upload.
3023+ #[serde(skip_serializing_if = "Option::is_none")]
3024+ pub verify: Option<Action>,
3025+ }
3026+
3027+ #[derive(Debug, Serialize, Deserialize)]
3028+ pub struct ResponseObject {
3029+ /// String OID of the LFS object.
3030+ pub oid: Oid,
3031+
3032+ /// Integer byte size of the LFS object.
3033+ pub size: u64,
3034+
3035+ /// An error.
3036+ #[serde(skip_serializing_if = "Option::is_none")]
3037+ pub error: Option<ObjectError>,
3038+
3039+ /// Optional boolean specifying whether the request for this specific
3040+ /// object is authenticated. If ommitted or `false`, Git LFS will
3041+ /// attempt to find credentials for this URL.
3042+ #[serde(skip_serializing_if = "Option::is_none")]
3043+ pub authenticated: Option<bool>,
3044+
3045+ /// Object containing the next actions for this object. Applicable actions
3046+ /// depend on which `operation` is specified in the request. How these
3047+ /// properties are interpreted depends on which transfer adapter the client
3048+ /// will be using.
3049+ ///
3050+ /// This will be `None` if there are no actions to perform. This will be
3051+ /// the case for upload operations if the server already has the
3052+ /// objects.
3053+ #[serde(skip_serializing_if = "Option::is_none")]
3054+ pub actions: Option<Actions>,
3055+ }
3056+
3057+ /// A batch request.
3058+ #[derive(Debug, Serialize, Deserialize)]
3059+ pub struct BatchRequest {
3060+ /// The operation being performed.
3061+ pub operation: Operation,
3062+
3063+ /// An optional array of string identifiers for transfer adapters that the
3064+ /// client has configured. If ommitted, the `basic` transfer adaptor *must*
3065+ /// be assumed by the server.
3066+ ///
3067+ /// Note: Git LFS currently only supports the `basic` transfer adapter.
3068+ /// This property was added for future compatibility with some experimental
3069+ /// tranfer adapters.
3070+ #[serde(skip_serializing_if = "Option::is_none")]
3071+ pub transfers: Option<Vec<Transfer>>,
3072+
3073+ /// Optional object describing the server ref that the objects belong to.
3074+ ///
3075+ /// Note: Added in v2.4.
3076+ #[serde(rename = "ref")]
3077+ #[serde(skip_serializing_if = "Option::is_none")]
3078+ pub refs: Option<BTreeMap<String, String>>,
3079+
3080+ /// An array of objects to upload/download.
3081+ pub objects: Vec<RequestObject>,
3082+ }
3083+
3084+ #[derive(Debug, Serialize, Deserialize)]
3085+ pub struct BatchResponse {
3086+ /// String identifier of the transfer adapter that the server prefers. This
3087+ /// *must* be one of the given `transfer` identifiers from the request.
3088+ /// Servers can assume the `basic` transfer adaptor if `None` was given.
3089+ #[serde(skip_serializing_if = "Option::is_none")]
3090+ pub transfer: Option<Transfer>,
3091+
3092+ /// An array of objects to download or upload.
3093+ pub objects: Vec<ResponseObject>,
3094+ }
3095+
3096+ /// An error response.
3097+ #[derive(Debug, Serialize, Deserialize)]
3098+ pub struct BatchResponseError {
3099+ /// The error message.
3100+ pub message: String,
3101+
3102+ /// Optional string to give the user a place to report errors.
3103+ #[serde(skip_serializing_if = "Option::is_none")]
3104+ pub documentation_url: Option<String>,
3105+
3106+ /// Optional string unique identifier for the request. Useful for
3107+ /// debugging.
3108+ #[serde(skip_serializing_if = "Option::is_none")]
3109+ pub request_id: Option<String>,
3110+ }
3111+
3112+ /// A request to verify an LFS object.
3113+ #[derive(Debug, Serialize, Deserialize)]
3114+ pub struct VerifyRequest {
3115+ /// Object ID.
3116+ pub oid: Oid,
3117+
3118+ /// Size of the object. If this doesn't match the size of the real object,
3119+ /// then an error code should be returned.
3120+ pub size: u64,
3121+ }
3122 diff --git a/src/logger.rs b/src/logger.rs
3123new file mode 100644
3124index 0000000..cfb85b3
3125--- /dev/null
3126+++ b/src/logger.rs
3127 @@ -0,0 +1,89 @@
3128+ // Copyright (c) 2019 Jason White
3129+ //
3130+ // Permission is hereby granted, free of charge, to any person obtaining a copy
3131+ // of this software and associated documentation files (the "Software"), to deal
3132+ // in the Software without restriction, including without limitation the rights
3133+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3134+ // copies of the Software, and to permit persons to whom the Software is
3135+ // furnished to do so, subject to the following conditions:
3136+ //
3137+ // The above copyright notice and this permission notice shall be included in
3138+ // all copies or substantial portions of the Software.
3139+ //
3140+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3141+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3142+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3143+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3144+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3145+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3146+ // SOFTWARE.
3147+ use std::fmt;
3148+ use std::net::SocketAddr;
3149+ use std::time::Instant;
3150+
3151+ use futures::Future;
3152+ use humantime::format_duration;
3153+ use hyper::{service::Service, Request, Response};
3154+ use log;
3155+
3156+ /// Wraps a service to provide logging on both the request and the response.
3157+ pub struct Logger<S> {
3158+ remote_addr: SocketAddr,
3159+ service: S,
3160+ }
3161+
3162+ impl<S> Logger<S> {
3163+ pub fn new(remote_addr: SocketAddr, service: S) -> Self {
3164+ Logger {
3165+ remote_addr,
3166+ service,
3167+ }
3168+ }
3169+ }
3170+
3171+ impl<S> Service for Logger<S>
3172+ where
3173+ S: Service,
3174+ S::Future: Send + 'static,
3175+ S::Error: fmt::Display + Send + 'static,
3176+ {
3177+ type ReqBody = S::ReqBody;
3178+ type ResBody = S::ResBody;
3179+ type Error = S::Error;
3180+ type Future = Box<
3181+ dyn Future<Item = Response<Self::ResBody>, Error = Self::Error> + Send,
3182+ >;
3183+
3184+ fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
3185+ let method = req.method().clone();
3186+ let uri = req.uri().clone();
3187+ let remote_addr = self.remote_addr;
3188+
3189+ let start = Instant::now();
3190+
3191+ Box::new(self.service.call(req).then(move |response| {
3192+ // TODO: Add a duration of how long it took to respond to the
3193+ // request.
3194+ match &response {
3195+ Ok(response) => log::info!(
3196+ "[{}] {} {} - {} ({})",
3197+ remote_addr.ip(),
3198+ method,
3199+ uri,
3200+ response.status(),
3201+ format_duration(start.elapsed()),
3202+ ),
3203+ Err(err) => log::error!(
3204+ "[{}] {} {} - {} ({})",
3205+ remote_addr.ip(),
3206+ method,
3207+ uri,
3208+ err,
3209+ format_duration(start.elapsed()),
3210+ ),
3211+ };
3212+
3213+ response
3214+ }))
3215+ }
3216+ }
3217 diff --git a/src/lru.rs b/src/lru.rs
3218new file mode 100644
3219index 0000000..00364ae
3220--- /dev/null
3221+++ b/src/lru.rs
3222 @@ -0,0 +1,148 @@
3223+ // Copyright (c) 2019 Jason White
3224+ //
3225+ // Permission is hereby granted, free of charge, to any person obtaining a copy
3226+ // of this software and associated documentation files (the "Software"), to deal
3227+ // in the Software without restriction, including without limitation the rights
3228+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3229+ // copies of the Software, and to permit persons to whom the Software is
3230+ // furnished to do so, subject to the following conditions:
3231+ //
3232+ // The above copyright notice and this permission notice shall be included in
3233+ // all copies or substantial portions of the Software.
3234+ //
3235+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3236+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3237+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3238+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3239+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3240+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3241+ // SOFTWARE.
3242+ use futures::{Future, Stream};
3243+ use linked_hash_map::LinkedHashMap;
3244+
3245+ use crate::lfs::Oid;
3246+
3247+ /// A least recently used (LRU) cache.
3248+ pub struct Cache {
3249+ // A linked hash map is used to implement an efficient LRU cache.
3250+ map: LinkedHashMap<Oid, u64>,
3251+
3252+ // Total size of the cache. This is equal to the sum of the values in the
3253+ // map. We use this to determine if the cache has grown too large and must
3254+ // be pruned.
3255+ size: u64,
3256+ }
3257+
3258+ impl Cache {
3259+ /// Creates a new, empty cache.
3260+ pub fn new() -> Self {
3261+ Cache {
3262+ map: LinkedHashMap::new(),
3263+ size: 0,
3264+ }
3265+ }
3266+
3267+ /// Returns the size (in bytes) of the cache.
3268+ pub fn size(&self) -> u64 {
3269+ self.size
3270+ }
3271+
3272+ /// Loads the cache from a stream of entries. Note that this throws away any
3273+ /// LRU information. Since the server shouldn't be restarted very often,
3274+ /// this shouldn't be a problem in practice. Frequently used entries will
3275+ /// naturally bubble back up to the top.
3276+ pub fn from_stream<S>(
3277+ stream: S,
3278+ ) -> impl Future<Item = Self, Error = S::Error>
3279+ where
3280+ S: Stream<Item = (Oid, u64)>,
3281+ {
3282+ stream
3283+ .fold(Cache::new(), move |mut cache, (oid, len)| {
3284+ cache.push(oid, len);
3285+ Ok(cache)
3286+ })
3287+ .or_else(move |_err| Ok(Cache::new()))
3288+ }
3289+
3290+ /// Removes the least recently used item. Returns `None` if the cache is
3291+ /// empty. When the cache gets too large, this should be called in a loop in
3292+ /// order to bring it below the threshold.
3293+ pub fn pop(&mut self) -> Option<(Oid, u64)> {
3294+ if let Some((k, v)) = self.map.pop_front() {
3295+ self.size -= v;
3296+ Some((k, v))
3297+ } else {
3298+ None
3299+ }
3300+ }
3301+
3302+ /// Removes an entry from the cache. Returns the size of the object if it
3303+ /// exists, or `None` if it didn't exist in the cache.
3304+ pub fn remove(&mut self, key: &Oid) -> Option<u64> {
3305+ self.map.remove(key).map(|size| {
3306+ self.size -= size;
3307+ size
3308+ })
3309+ }
3310+
3311+ /// Gets an entry by key without perturbing the LRU ordering.
3312+ pub fn get(&self, key: &Oid) -> Option<u64> {
3313+ self.map.get(key).cloned()
3314+ }
3315+
3316+ /// Gets an entry by key. If the entry exists, it will be touched so that it
3317+ /// becomes the most recently used item.
3318+ pub fn get_refresh(&mut self, key: &Oid) -> Option<u64> {
3319+ self.map.get_refresh(key).cloned()
3320+ }
3321+
3322+ /// Adds an entry to the cache. Returns the previous value value of the
3323+ /// cache item if it already existed. Returns `None` if the entry did not
3324+ /// previously exist. In either case, the entry is always touched so that it
3325+ /// becomes the most recently used item.
3326+ pub fn push(&mut self, key: Oid, value: u64) -> Option<u64> {
3327+ self.size += value;
3328+
3329+ if let Some(old_value) = self.map.insert(key, value) {
3330+ self.size -= old_value;
3331+ Some(old_value)
3332+ } else {
3333+ None
3334+ }
3335+ }
3336+
3337+ /// Returns a future that prunes the least recently used entries that cause
3338+ /// the cache to exceed the given maximum size.
3339+ ///
3340+ /// If the stream is thrown away, then the items will not be deleted.
3341+ #[cfg(none)]
3342+ pub fn prune(
3343+ &mut self,
3344+ root: PathBuf,
3345+ max_size: u64,
3346+ ) -> impl Future<Item = usize, Error = io::Error> {
3347+ if max_size == 0 {
3348+ return Either::A(future::ok(0));
3349+ }
3350+
3351+ let mut to_delete = Vec::new();
3352+
3353+ while self.size() > max_size {
3354+ if let Some((oid, _)) = self.pop() {
3355+ to_delete.push(oid);
3356+ }
3357+ }
3358+
3359+ Either::B(stream::iter_ok(to_delete).fold(0, move |acc, oid| {
3360+ let path = root.join(format!("objects/{}", oid.path()));
3361+
3362+ fs::remove_file(path)
3363+ .map_err(move |e| {
3364+ log::error!("Failed to delete '{}' ({})", oid, e);
3365+ e
3366+ })
3367+ .map(move |()| acc + 1)
3368+ }))
3369+ }
3370+ }
3371 diff --git a/src/main.rs b/src/main.rs
3372new file mode 100644
3373index 0000000..f1b4754
3374--- /dev/null
3375+++ b/src/main.rs
3376 @@ -0,0 +1,149 @@
3377+ // Copyright (c) 2019 Jason White
3378+ //
3379+ // Permission is hereby granted, free of charge, to any person obtaining a copy
3380+ // of this software and associated documentation files (the "Software"), to deal
3381+ // in the Software without restriction, including without limitation the rights
3382+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3383+ // copies of the Software, and to permit persons to whom the Software is
3384+ // furnished to do so, subject to the following conditions:
3385+ //
3386+ // The above copyright notice and this permission notice shall be included in
3387+ // all copies or substantial portions of the Software.
3388+ //
3389+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3390+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3391+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3392+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3393+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3394+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3395+ // SOFTWARE.
3396+ mod app;
3397+ mod error;
3398+ mod hyperext;
3399+ mod lfs;
3400+ mod logger;
3401+ mod lru;
3402+ mod sha256;
3403+ mod storage;
3404+
3405+ use std::net::{SocketAddr, ToSocketAddrs};
3406+ use std::path::PathBuf;
3407+ use std::process::exit;
3408+ use std::sync::Arc;
3409+
3410+ use futures::Future;
3411+ use hex::FromHex;
3412+ use hyper::{self, server::conn::AddrStream, service::make_service_fn, Server};
3413+ use log;
3414+ use pretty_env_logger;
3415+ use structopt::StructOpt;
3416+
3417+ use crate::app::{App, State};
3418+ use crate::error::Error;
3419+ use crate::logger::Logger;
3420+ use crate::storage::{Cached, Disk, Encrypted, Verify, S3};
3421+
3422+ #[derive(StructOpt)]
3423+ struct Args {
3424+ /// Host or address to listen on.
3425+ #[structopt(long = "host", default_value = "0.0.0.0:8080")]
3426+ host: String,
3427+
3428+ /// Root directory of the object cache.
3429+ #[structopt(long = "cache-dir")]
3430+ cache_dir: PathBuf,
3431+
3432+ /// Logging level to use. By default, uses `info`.
3433+ #[structopt(long = "log-level", default_value = "info")]
3434+ log_level: log::LevelFilter,
3435+
3436+ /// Amazon S3 bucket to use.
3437+ #[structopt(long = "s3-bucket")]
3438+ s3_bucket: String,
3439+
3440+ /// Encryption key to use.
3441+ #[structopt(long = "key", parse(try_from_str = "FromHex::from_hex"))]
3442+ key: [u8; 32],
3443+
3444+ /// Maximum size of the cache, in bytes. Set to 0 for an unlimited cache
3445+ /// size.
3446+ #[structopt(long = "max-cache-size", default_value = "50 GiB")]
3447+ max_cache_size: human_size::Size,
3448+ }
3449+
3450+ impl Args {
3451+ fn main(self) -> Result<(), Box<dyn std::error::Error>> {
3452+ // Initialize logging.
3453+ pretty_env_logger::formatted_timed_builder()
3454+ .filter_module("rudolfs", self.log_level)
3455+ .init();
3456+
3457+ // Find a socket address to bind to. This will resolve domain names.
3458+ let addr = self
3459+ .host
3460+ .to_socket_addrs()?
3461+ .next()
3462+ .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 8080)));
3463+
3464+ // Convert cache size to bytes.
3465+ let max_cache_size =
3466+ self.max_cache_size.into::<human_size::Byte>().value() as u64;
3467+ let key = self.key;
3468+
3469+ let mut rt = tokio::runtime::Runtime::new()?;
3470+
3471+ // Initialize our storage backends.
3472+ let disk = Disk::new(self.cache_dir).map_err(Error::from);
3473+ let s3 = S3::new(self.s3_bucket, "lfs".into()).map_err(Error::from);
3474+ let storage = disk
3475+ .join(s3)
3476+ .and_then(move |(disk, s3)| {
3477+ // Use the disk as a cache.
3478+ Cached::new(max_cache_size, disk, s3).from_err()
3479+ })
3480+ .map(move |storage| {
3481+ // Verify object SHA256s as they are uploaded and downloaded.
3482+ Verify::new(Encrypted::new(key, storage))
3483+ });
3484+
3485+ log::info!("Initializing storage...");
3486+ let storage = rt.block_on(storage)?;
3487+ log::info!("Successfully initialized storage.");
3488+
3489+ // Initialize the shared state.
3490+ let state = Arc::new(State::new(storage));
3491+
3492+ // Create our service factory.
3493+ let new_service =
3494+ make_service_fn(move |socket: &AddrStream| -> Result<_, Error> {
3495+ // Create our app.
3496+ let service = App::new(state.clone());
3497+
3498+ // Add logging middleware
3499+ let service = Logger::new(socket.remote_addr(), service);
3500+
3501+ Ok(service)
3502+ });
3503+
3504+ // Create the server.
3505+ let server = Server::bind(&addr).serve(new_service);
3506+
3507+ log::info!("Listening on {}", server.local_addr());
3508+
3509+ // Run the server.
3510+ rt.block_on_all(server)?;
3511+
3512+ Ok(())
3513+ }
3514+ }
3515+
3516+ fn main() {
3517+ let exit_code = if let Err(err) = Args::from_args().main() {
3518+ log::error!("{}", err);
3519+ 1
3520+ } else {
3521+ 0
3522+ };
3523+
3524+ exit(exit_code);
3525+ }
3526 diff --git a/src/sha256.rs b/src/sha256.rs
3527new file mode 100644
3528index 0000000..3572828
3529--- /dev/null
3530+++ b/src/sha256.rs
3531 @@ -0,0 +1,341 @@
3532+ // Copyright (c) 2019 Jason White
3533+ //
3534+ // Permission is hereby granted, free of charge, to any person obtaining a copy
3535+ // of this software and associated documentation files (the "Software"), to deal
3536+ // in the Software without restriction, including without limitation the rights
3537+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3538+ // copies of the Software, and to permit persons to whom the Software is
3539+ // furnished to do so, subject to the following conditions:
3540+ //
3541+ // The above copyright notice and this permission notice shall be included in
3542+ // all copies or substantial portions of the Software.
3543+ //
3544+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3545+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3546+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3547+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3548+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3549+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3550+ // SOFTWARE.
3551+ use std::fmt;
3552+ use std::ops;
3553+ use std::str::FromStr;
3554+
3555+ use futures::{try_ready, Async, Poll, Stream};
3556+ use hex::{FromHex, FromHexError, ToHex};
3557+ use serde::{
3558+ de::{self, Deserializer, Visitor},
3559+ ser::{self, Serializer},
3560+ Deserialize, Serialize,
3561+ };
3562+
3563+ use generic_array::{typenum, GenericArray};
3564+ use sha2::{self, Digest};
3565+
3566+ /// An error associated with parsing a SHA256.
3567+ #[derive(Debug, Clone, Copy, PartialEq)]
3568+ pub struct Sha256Error(FromHexError);
3569+
3570+ impl From<FromHexError> for Sha256Error {
3571+ fn from(error: FromHexError) -> Self {
3572+ Sha256Error(error)
3573+ }
3574+ }
3575+
3576+ impl fmt::Display for Sha256Error {
3577+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3578+ self.0.fmt(f)
3579+ }
3580+ }
3581+
3582+ impl std::error::Error for Sha256Error {}
3583+
3584+ /// A Git LFS object ID (i.e., a SHA256).
3585+ #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
3586+ pub struct Sha256(GenericArray<u8, typenum::U32>);
3587+
3588+ impl Sha256 {
3589+ pub fn bytes(&self) -> &[u8] {
3590+ &self.0
3591+ }
3592+
3593+ /// Returns an object that can be formatted as a path.
3594+ pub fn path(&self) -> Sha256Path {
3595+ Sha256Path(self)
3596+ }
3597+ }
3598+
3599+ pub struct Sha256Path<'a>(&'a Sha256);
3600+
3601+ impl<'a> fmt::Display for Sha256Path<'a> {
3602+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3603+ write!(
3604+ f,
3605+ "{:02x}/{:02x}/{}",
3606+ self.0.bytes()[0],
3607+ self.0.bytes()[1],
3608+ self.0
3609+ )
3610+ }
3611+ }
3612+
3613+ impl AsRef<[u8]> for Sha256 {
3614+ fn as_ref(&self) -> &[u8] {
3615+ &self.0
3616+ }
3617+ }
3618+
3619+ impl From<[u8; 32]> for Sha256 {
3620+ fn from(arr: [u8; 32]) -> Self {
3621+ Sha256(arr.into())
3622+ }
3623+ }
3624+
3625+ impl From<GenericArray<u8, typenum::U32>> for Sha256 {
3626+ fn from(arr: GenericArray<u8, typenum::U32>) -> Self {
3627+ Sha256(arr)
3628+ }
3629+ }
3630+
3631+ impl FromHex for Sha256 {
3632+ type Error = Sha256Error;
3633+
3634+ fn from_hex<T>(hex: T) -> Result<Self, Self::Error>
3635+ where
3636+ T: AsRef<[u8]>,
3637+ {
3638+ Ok(Sha256::from(<[u8; 32]>::from_hex(hex)?))
3639+ }
3640+ }
3641+
3642+ impl FromStr for Sha256 {
3643+ type Err = Sha256Error;
3644+
3645+ fn from_str(s: &str) -> Result<Self, Self::Err> {
3646+ Self::from_hex(s)
3647+ }
3648+ }
3649+
3650+ impl fmt::UpperHex for Sha256 {
3651+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3652+ self.write_hex_upper(f)
3653+ }
3654+ }
3655+
3656+ impl fmt::LowerHex for Sha256 {
3657+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3658+ self.write_hex(f)
3659+ }
3660+ }
3661+
3662+ impl fmt::Display for Sha256 {
3663+ #[inline]
3664+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3665+ <Self as fmt::LowerHex>::fmt(self, f)
3666+ }
3667+ }
3668+
3669+ impl fmt::Debug for Sha256 {
3670+ #[inline]
3671+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3672+ <Self as fmt::Display>::fmt(self, f)
3673+ }
3674+ }
3675+
3676+ impl ops::Deref for Sha256 {
3677+ type Target = [u8];
3678+
3679+ fn deref(&self) -> &Self::Target {
3680+ &self.0
3681+ }
3682+ }
3683+
3684+ impl Serialize for Sha256 {
3685+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3686+ where
3687+ S: Serializer,
3688+ {
3689+ if serializer.is_human_readable() {
3690+ // Serialize as a hex string.
3691+ let mut hex = String::new();
3692+ self.0
3693+ .as_ref()
3694+ .write_hex(&mut hex)
3695+ .map_err(ser::Error::custom)?;
3696+ serializer.serialize_str(&hex)
3697+ } else {
3698+ // Serialize as a byte array with known length.
3699+ serializer.serialize_bytes(self.0.as_ref())
3700+ }
3701+ }
3702+ }
3703+
3704+ impl<'de> Deserialize<'de> for Sha256 {
3705+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3706+ where
3707+ D: Deserializer<'de>,
3708+ {
3709+ struct Sha256Visitor;
3710+
3711+ impl<'de> Visitor<'de> for Sha256Visitor {
3712+ type Value = Sha256;
3713+
3714+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
3715+ write!(f, "hex string or 32 bytes")
3716+ }
3717+
3718+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
3719+ where
3720+ E: de::Error,
3721+ {
3722+ let v = Sha256::from_hex(v).map_err(|e| match e.0 {
3723+ FromHexError::InvalidHexCharacter { c, .. } => {
3724+ E::invalid_value(
3725+ de::Unexpected::Char(c),
3726+ &"string with only hexadecimal characters",
3727+ )
3728+ }
3729+ FromHexError::InvalidStringLength => E::invalid_length(
3730+ v.len(),
3731+ &"hex string with a valid length",
3732+ ),
3733+ FromHexError::OddLength => E::invalid_length(
3734+ v.len(),
3735+ &"hex string with an even length",
3736+ ),
3737+ })?;
3738+
3739+ Ok(v)
3740+ }
3741+
3742+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
3743+ where
3744+ E: de::Error,
3745+ {
3746+ if v.len() != 32 {
3747+ return Err(E::invalid_length(v.len(), &"32 bytes"));
3748+ }
3749+
3750+ let mut inner = <[u8; 32]>::default();
3751+ inner.copy_from_slice(v);
3752+
3753+ Ok(Sha256::from(inner))
3754+ }
3755+ }
3756+
3757+ if deserializer.is_human_readable() {
3758+ deserializer.deserialize_str(Sha256Visitor)
3759+ } else {
3760+ deserializer.deserialize_bytes(Sha256Visitor)
3761+ }
3762+ }
3763+ }
3764+
3765+ #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
3766+ pub struct Sha256VerifyError {
3767+ pub expected: Sha256,
3768+ pub found: Sha256,
3769+ }
3770+
3771+ impl fmt::Display for Sha256VerifyError {
3772+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3773+ write!(
3774+ f,
3775+ "expected SHA256 '{}', but found '{}'",
3776+ self.expected, self.found
3777+ )
3778+ }
3779+ }
3780+
3781+ impl std::error::Error for Sha256VerifyError {}
3782+
3783+ /// A stream adaptor that verifies the SHA256 of a byte stream.
3784+ pub struct VerifyStream<S> {
3785+ /// The underlying stream.
3786+ stream: S,
3787+
3788+ /// The total size of the stream.
3789+ total: u64,
3790+
3791+ /// The size so far.
3792+ len: u64,
3793+
3794+ /// The expected SHA256.
3795+ expected: Sha256,
3796+
3797+ /// The current state of the hasher.
3798+ hasher: sha2::Sha256,
3799+ }
3800+
3801+ impl<S> VerifyStream<S>
3802+ where
3803+ S: Stream,
3804+ S::Item: AsRef<[u8]>,
3805+ S::Error: From<Sha256VerifyError>,
3806+ {
3807+ pub fn new(stream: S, total: u64, expected: Sha256) -> Self {
3808+ VerifyStream {
3809+ stream,
3810+ total,
3811+ len: 0,
3812+ expected,
3813+ hasher: sha2::Sha256::default(),
3814+ }
3815+ }
3816+ }
3817+
3818+ impl<S> Stream for VerifyStream<S>
3819+ where
3820+ S: Stream,
3821+ S::Item: AsRef<[u8]>,
3822+ S::Error: From<Sha256VerifyError>,
3823+ {
3824+ type Item = S::Item;
3825+ type Error = S::Error;
3826+
3827+ fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
3828+ let bytes = try_ready!(self.stream.poll());
3829+
3830+ match bytes {
3831+ Some(bytes) => {
3832+ self.len += bytes.as_ref().len() as u64;
3833+
3834+ // Continuously hash the bytes as we receive them.
3835+ self.hasher.input(bytes.as_ref());
3836+
3837+ if self.len >= self.total {
3838+ // This is the last chunk in the stream. Verify that the
3839+ // digest matches.
3840+ let found = Sha256::from(self.hasher.result_reset());
3841+
3842+ if found == self.expected {
3843+ Ok(Async::Ready(Some(bytes)))
3844+ } else {
3845+ Err(Self::Error::from(Sha256VerifyError {
3846+ found,
3847+ expected: self.expected,
3848+ }))
3849+ }
3850+ } else {
3851+ Ok(Async::Ready(Some(bytes)))
3852+ }
3853+ }
3854+ None => {
3855+ // End of stream.
3856+ Ok(Async::Ready(None))
3857+ }
3858+ }
3859+ }
3860+ }
3861+
3862+ #[cfg(test)]
3863+ mod tests {
3864+ use super::*;
3865+
3866+ #[test]
3867+ fn sha256() {
3868+ let s =
3869+ "b1fbeefc23e6a149a6f7d0c2fb635bfc78f7ddc2da963ea9c6a63eb324260e6d";
3870+ assert_eq!(Sha256::from_str(s).unwrap().to_string(), s);
3871+ }
3872+ }
3873 diff --git a/src/storage/cached.rs b/src/storage/cached.rs
3874new file mode 100644
3875index 0000000..81012b6
3876--- /dev/null
3877+++ b/src/storage/cached.rs
3878 @@ -0,0 +1,332 @@
3879+ // Copyright (c) 2019 Jason White
3880+ //
3881+ // Permission is hereby granted, free of charge, to any person obtaining a copy
3882+ // of this software and associated documentation files (the "Software"), to deal
3883+ // in the Software without restriction, including without limitation the rights
3884+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3885+ // copies of the Software, and to permit persons to whom the Software is
3886+ // furnished to do so, subject to the following conditions:
3887+ //
3888+ // The above copyright notice and this permission notice shall be included in
3889+ // all copies or substantial portions of the Software.
3890+ //
3891+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3892+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3893+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3894+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3895+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3896+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3897+ // SOFTWARE.
3898+ use std::fmt;
3899+ use std::sync::{Arc, Mutex};
3900+
3901+ use futures::{
3902+ future::{self, Either},
3903+ stream, Future, Stream,
3904+ };
3905+ use tokio;
3906+
3907+ use crate::lfs::Oid;
3908+ use crate::lru::Cache;
3909+
3910+ use super::{LFSObject, Storage, StorageFuture, StorageStream};
3911+
3912+ #[derive(Debug)]
3913+ pub enum Error<C, S> {
3914+ /// An error that occurred in the cache.
3915+ Cache(C),
3916+
3917+ /// An error that occurred in the storage backend.
3918+ Storage(S),
3919+ }
3920+
3921+ impl<C, S> fmt::Display for Error<C, S>
3922+ where
3923+ C: fmt::Display,
3924+ S: fmt::Display,
3925+ {
3926+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3927+ match self {
3928+ Error::Cache(x) => fmt::Display::fmt(&x, f),
3929+ Error::Storage(x) => fmt::Display::fmt(&x, f),
3930+ }
3931+ }
3932+ }
3933+
3934+ impl<C, S> Error<C, S> {
3935+ pub fn from_cache(error: C) -> Self {
3936+ Error::Cache(error)
3937+ }
3938+
3939+ pub fn from_storage(error: S) -> Self {
3940+ Error::Storage(error)
3941+ }
3942+ }
3943+
3944+ impl<C, S> std::error::Error for Error<C, S>
3945+ where
3946+ C: fmt::Debug + fmt::Display,
3947+ S: fmt::Debug + fmt::Display,
3948+ {
3949+ }
3950+
3951+ /// Combines a cache with a permanent storage backend such that if a query to
3952+ /// the cache fails, it falls back to a permanent storage backend.
3953+ pub struct Backend<C, S> {
3954+ lru: Arc<Mutex<Cache>>,
3955+ max_size: u64,
3956+ cache: Arc<C>,
3957+ storage: Arc<S>,
3958+ }
3959+
3960+ impl<C, S> Backend<C, S>
3961+ where
3962+ C: Storage,
3963+ S: Storage,
3964+ {
3965+ pub fn new(
3966+ max_size: u64,
3967+ cache: C,
3968+ storage: S,
3969+ ) -> impl Future<Item = Self, Error = C::Error> {
3970+ Cache::from_stream(cache.list()).and_then(move |mut lru| {
3971+ let cache = Arc::new(cache);
3972+
3973+ // Prune the cache. The maximum size setting may have changed
3974+ // between server invocations. Thus, prune it down right away
3975+ // instead of waiting for a client to do an upload.
3976+ prune_cache(&mut lru, max_size, cache.clone()).map(move |count| {
3977+ if count > 0 {
3978+ log::info!("Pruned {} entries from the cache", count);
3979+ }
3980+
3981+ Backend {
3982+ lru: Arc::new(Mutex::new(lru)),
3983+ max_size,
3984+ cache,
3985+ storage: Arc::new(storage),
3986+ }
3987+ })
3988+ })
3989+ }
3990+ }
3991+
3992+ /// Returns a future that prunes the least recently used entries that cause the
3993+ /// storage to exceed the given maximum size.
3994+ ///
3995+ /// If the stream is thrown away, then the items will be removed from the
3996+ /// in-memory LRU cache, but not physically deleted.
3997+ fn prune_cache<S>(
3998+ lru: &mut Cache,
3999+ max_size: u64,
4000+ storage: Arc<S>,
4001+ ) -> impl Future<Item = usize, Error = S::Error>
4002+ where
4003+ S: Storage,
4004+ {
4005+ if max_size == 0 {
4006+ // The cache can have unlimited size.
4007+ return Either::A(future::ok(0));
4008+ }
4009+
4010+ let mut to_delete = Vec::new();
4011+
4012+ while lru.size() > max_size {
4013+ if let Some((oid, _)) = lru.pop() {
4014+ to_delete.push(oid);
4015+ }
4016+ }
4017+
4018+ Either::B(stream::iter_ok(to_delete).fold(0, move |acc, oid| {
4019+ storage.delete(&oid).map(move |()| acc + 1)
4020+ }))
4021+ }
4022+
4023+ fn cache_and_prune<C>(
4024+ cache: Arc<C>,
4025+ key: Oid,
4026+ obj: LFSObject,
4027+ lru: Arc<Mutex<Cache>>,
4028+ max_size: u64,
4029+ ) -> impl Future<Item = (), Error = ()>
4030+ where
4031+ C: Storage,
4032+ {
4033+ let len = obj.len();
4034+
4035+ cache
4036+ .put(&key, obj)
4037+ .and_then(move |()| {
4038+ // Add the object info to our LRU cache once the download from
4039+ // permanent storage is complete.
4040+ let mut lru = lru.lock().unwrap();
4041+ lru.push(key, len);
4042+
4043+ // Prune the cache.
4044+ prune_cache(&mut lru, max_size, cache).map(move |count| {
4045+ if count > 0 {
4046+ log::info!("Pruned {} entries from the cache", count);
4047+ }
4048+ })
4049+ })
4050+ .map_err(move |err| {
4051+ log::error!("Error caching {} ({})", key, err);
4052+ })
4053+ }
4054+
4055+ impl<C, S> Storage for Backend<C, S>
4056+ where
4057+ S: Storage + Send + Sync + 'static,
4058+ S::Error: 'static,
4059+ C: Storage + Send + Sync + 'static,
4060+ C::Error: 'static,
4061+ {
4062+ type Error = Error<C::Error, S::Error>;
4063+
4064+ /// Tries to query the cache first. If that fails, falls back to the
4065+ /// permanent storage backend.
4066+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error> {
4067+ // TODO: Keep stats on cache hits and misses. We can then display those
4068+ // stats on a web page or send them to another service such as
4069+ // Prometheus.
4070+ let key = *key;
4071+
4072+ if let Ok(mut lru) = self.lru.lock() {
4073+ if lru.get_refresh(&key).is_some() {
4074+ // Cache hit!
4075+ //
4076+ // TODO: Verify the stream as we send it back. If the SHA256 is
4077+ // incorrect, delete it and let the client try again.
4078+ let storage = self.storage.clone();
4079+ let lru2 = self.lru.clone();
4080+
4081+ return Box::new(
4082+ self.cache.get(&key).map_err(Error::from_cache).and_then(
4083+ move |obj| match obj {
4084+ Some(obj) => Either::A(future::ok(Some(obj))),
4085+ None => {
4086+ // If the cache doesn't actually have it, delete
4087+ // the entry from our LRU. This can happen if
4088+ // the cache is cleared out manually.
4089+ let mut lru = lru2.lock().unwrap();
4090+ lru.remove(&key);
4091+
4092+ // Fall back to permanent storage. Note that
4093+ // this won't actually cache the object. This
4094+ // will be done next time the same object is
4095+ // requested.
4096+ Either::B(
4097+ storage
4098+ .get(&key)
4099+ .map_err(Error::from_storage),
4100+ )
4101+ }
4102+ },
4103+ ),
4104+ );
4105+ }
4106+ }
4107+
4108+ // Cache miss. Get the object from permanent storage. If successful, we
4109+ // to cache the resulting byte stream.
4110+ let lru = self.lru.clone();
4111+ let max_size = self.max_size;
4112+ let cache = self.cache.clone();
4113+
4114+ Box::new(
4115+ self.storage
4116+ .get(&key)
4117+ .map_err(Error::from_storage)
4118+ .and_then(move |obj| match obj {
4119+ Some(obj) => {
4120+ // Cache the returned LFS object.
4121+ let (a, b) = obj.split();
4122+
4123+ // Cache the object in the background. Whether or not
4124+ // this succeeds shouldn't prevent the client from
4125+ // getting the LFS object. For example, even if we run
4126+ // out of disk space, the server should still continue
4127+ // operating.
4128+ tokio::spawn(cache_and_prune(
4129+ cache, key, b, lru, max_size,
4130+ ));
4131+
4132+ // Send the object from permanent-storage.
4133+ Either::A(future::ok(Some(a)))
4134+ }
4135+ None => {
4136+ // The permanent storage also doesn't have it.
4137+ //
4138+ // Note that we cannot cache the non-existence of an
4139+ // object because the storage backend might be shared by
4140+ // multiple caches.
4141+ Either::B(future::ok(None))
4142+ }
4143+ }),
4144+ )
4145+ }
4146+
4147+ fn put(
4148+ &self,
4149+ key: &Oid,
4150+ value: LFSObject,
4151+ ) -> StorageFuture<(), Self::Error> {
4152+ let key = *key;
4153+ let lru = self.lru.clone();
4154+ let max_size = self.max_size;
4155+ let cache = self.cache.clone();
4156+
4157+ let (a, b) = value.split();
4158+
4159+ // Cache the object in the background. Whether or not this succeeds
4160+ // shouldn't prevent the client from uploading the LFS object to
4161+ // permanent storage. For example, even if we run out of disk space, the
4162+ // server should still continue operating.
4163+ tokio::spawn(cache_and_prune(cache, key, b, lru, max_size));
4164+
4165+ Box::new(self.storage.put(&key, a).map_err(Error::from_storage))
4166+ }
4167+
4168+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error> {
4169+ // Get just the size of an object without perturbing the LRU ordering.
4170+ // Only downloads or uploads need to perturb the LRU ordering.
4171+ let lru = self.lru.lock().unwrap();
4172+ if let Some(size) = lru.get(key) {
4173+ // Cache hit!
4174+ Box::new(future::ok(Some(size)))
4175+ } else {
4176+ // Cache miss. Check permanent storage.
4177+ Box::new(self.storage.size(key).map_err(Error::from_storage))
4178+ }
4179+ }
4180+
4181+ /// Deletes an item from the cache (not from permanent storage).
4182+ fn delete(&self, key: &Oid) -> StorageFuture<(), Self::Error> {
4183+ // Only ever delete items from the cache. This may be called when
4184+ // a corrupted object is detected.
4185+ Box::new(self.cache.delete(key).map_err(Error::from_cache))
4186+ }
4187+
4188+ /// Returns a stream of cached items.
4189+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error> {
4190+ // TODO: Use the internal linked hash map instead to get this list.
4191+ Box::new(self.cache.list().map_err(Error::from_cache))
4192+ }
4193+
4194+ /// Returns the total size of the LRU cache (not the total size of the
4195+ /// permanent storage).
4196+ fn total_size(&self) -> Option<u64> {
4197+ let lru = self.lru.lock().unwrap();
4198+ Some(lru.size())
4199+ }
4200+
4201+ /// Returns the maximum size of the LRU cache (not the maximum size of the
4202+ /// permanent storage).
4203+ fn max_size(&self) -> Option<u64> {
4204+ if self.max_size == 0 {
4205+ None
4206+ } else {
4207+ Some(self.max_size)
4208+ }
4209+ }
4210+ }
4211 diff --git a/src/storage/disk.rs b/src/storage/disk.rs
4212new file mode 100644
4213index 0000000..7a7d47f
4214--- /dev/null
4215+++ b/src/storage/disk.rs
4216 @@ -0,0 +1,177 @@
4217+ // Copyright (c) 2019 Jason White
4218+ //
4219+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4220+ // of this software and associated documentation files (the "Software"), to deal
4221+ // in the Software without restriction, including without limitation the rights
4222+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4223+ // copies of the Software, and to permit persons to whom the Software is
4224+ // furnished to do so, subject to the following conditions:
4225+ //
4226+ // The above copyright notice and this permission notice shall be included in
4227+ // all copies or substantial portions of the Software.
4228+ //
4229+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4230+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4231+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4232+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4233+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4234+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4235+ // SOFTWARE.
4236+ use std::ffi::OsStr;
4237+ use std::io;
4238+ use std::path::PathBuf;
4239+ use std::str::FromStr;
4240+
4241+ use bytes::BytesMut;
4242+ use futures::{future, Future, Stream};
4243+ use tokio::{
4244+ self,
4245+ codec::{BytesCodec, Framed},
4246+ fs,
4247+ };
4248+ use uuid::Uuid;
4249+
4250+ use super::{LFSObject, Storage, StorageFuture, StorageStream};
4251+ use crate::lfs::Oid;
4252+
4253+ pub struct Backend {
4254+ root: PathBuf,
4255+ }
4256+
4257+ impl Backend {
4258+ pub fn new(root: PathBuf) -> impl Future<Item = Self, Error = io::Error> {
4259+ // TODO: Clean out files in the "incomplete" folder.
4260+ future::ok(Backend { root })
4261+ }
4262+
4263+ // Use sub directories in order to better utilize the file system's internal
4264+ // tree data structure.
4265+ fn key_to_path(&self, oid: &Oid) -> PathBuf {
4266+ self.root.join(format!("objects/{}", oid.path()))
4267+ }
4268+ }
4269+
4270+ impl Storage for Backend {
4271+ type Error = io::Error;
4272+
4273+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error> {
4274+ Box::new(
4275+ fs::File::open(self.key_to_path(key))
4276+ .and_then(fs::File::metadata)
4277+ .then(move |result| {
4278+ Ok(match result {
4279+ Ok((file, metadata)) => {
4280+ let stream = Framed::new(file, BytesCodec::new())
4281+ .map(BytesMut::freeze);
4282+
4283+ Some(LFSObject::new(
4284+ metadata.len(),
4285+ Box::new(stream),
4286+ ))
4287+ }
4288+ Err(err) => match err.kind() {
4289+ io::ErrorKind::NotFound => None,
4290+ _ => return Err(err),
4291+ },
4292+ })
4293+ }),
4294+ )
4295+ }
4296+
4297+ fn put(
4298+ &self,
4299+ key: &Oid,
4300+ value: LFSObject,
4301+ ) -> StorageFuture<(), Self::Error> {
4302+ let path = self.key_to_path(key);
4303+ let dir = path.parent().unwrap().to_path_buf();
4304+
4305+ let incomplete = self.root.join("incomplete");
4306+ let temp_path = incomplete.join(Uuid::new_v4().to_string());
4307+ let temp_path2 = temp_path.clone();
4308+
4309+ Box::new(
4310+ fs::create_dir_all(incomplete)
4311+ .and_then(move |()| fs::File::create(temp_path))
4312+ .and_then(move |file| {
4313+ value.stream().forward(Framed::new(file, BytesCodec::new()))
4314+ })
4315+ .and_then(move |_| fs::create_dir_all(dir))
4316+ .and_then(move |()| fs::rename(temp_path2, path)),
4317+ )
4318+ }
4319+
4320+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error> {
4321+ let path = self.key_to_path(key);
4322+
4323+ Box::new(
4324+ fs::metadata(path)
4325+ .map(move |metadata| Some(metadata.len()))
4326+ .or_else(move |err| match err.kind() {
4327+ io::ErrorKind::NotFound => Ok(None),
4328+ _ => Err(err),
4329+ }),
4330+ )
4331+ }
4332+
4333+ fn delete(&self, key: &Oid) -> StorageFuture<(), Self::Error> {
4334+ Box::new(fs::remove_file(self.key_to_path(key)).or_else(move |err| {
4335+ match err.kind() {
4336+ io::ErrorKind::NotFound => Ok(()),
4337+ _ => Err(err),
4338+ }
4339+ }))
4340+ }
4341+
4342+ /// Lists the objects that are on disk.
4343+ ///
4344+ /// The directory structure is assumed to be like this:
4345+ ///
4346+ /// objects
4347+ /// ├── 00
4348+ /// │ ├── 07
4349+ /// │ │ └── 0007941906960...
4350+ /// │ └── ff
4351+ /// │ └── 00ff9e9c69224...
4352+ /// ├── 01
4353+ /// │ ├── 89
4354+ /// │ │ └── 0189e5fd19477...
4355+ /// │ └── f5
4356+ /// │ └── 01f5c45c65e62...
4357+ /// ^^^^
4358+ ///
4359+ /// Note that the first four characters are repeated in the file name so
4360+ /// that transforming the file name into an object ID is simpler.
4361+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error> {
4362+ let path = self.root.join("objects");
4363+
4364+ Box::new(
4365+ fs::read_dir(path)
4366+ .flatten_stream()
4367+ .map(move |entry| fs::read_dir(entry.path()).flatten_stream())
4368+ .flatten()
4369+ .map(move |entry| fs::read_dir(entry.path()).flatten_stream())
4370+ .flatten()
4371+ .and_then(move |entry| {
4372+ let path = entry.path();
4373+ future::poll_fn(move || entry.poll_metadata())
4374+ .map(move |metadata| (path, metadata))
4375+ })
4376+ .filter_map(move |(path, metadata)| {
4377+ if let Some(oid) = path
4378+ .file_name()
4379+ .and_then(OsStr::to_str)
4380+ .and_then(|s| Oid::from_str(s).ok())
4381+ {
4382+ if metadata.is_file() {
4383+ Some((oid, metadata.len()))
4384+ } else {
4385+ None
4386+ }
4387+ } else {
4388+ None
4389+ }
4390+ }),
4391+ )
4392+ }
4393+ }
4394 diff --git a/src/storage/encrypt.rs b/src/storage/encrypt.rs
4395new file mode 100644
4396index 0000000..2e7394f
4397--- /dev/null
4398+++ b/src/storage/encrypt.rs
4399 @@ -0,0 +1,125 @@
4400+ // Copyright (c) 2019 Jason White
4401+ //
4402+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4403+ // of this software and associated documentation files (the "Software"), to deal
4404+ // in the Software without restriction, including without limitation the rights
4405+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4406+ // copies of the Software, and to permit persons to whom the Software is
4407+ // furnished to do so, subject to the following conditions:
4408+ //
4409+ // The above copyright notice and this permission notice shall be included in
4410+ // all copies or substantial portions of the Software.
4411+ //
4412+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4413+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4414+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4415+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4416+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4417+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4418+ // SOFTWARE.
4419+ use chacha::{ChaCha, KeyStream};
4420+ use std::io;
4421+
4422+ use bytes::{Bytes, BytesMut};
4423+ use futures::{Future, Stream};
4424+
4425+ use crate::lfs::Oid;
4426+
4427+ use super::{LFSObject, Storage, StorageFuture, StorageStream};
4428+
4429+ /// A storage adaptor that encrypts/decrypts all data that passes through.
4430+ pub struct Backend<S> {
4431+ storage: S,
4432+ key: [u8; 32],
4433+ }
4434+
4435+ impl<S> Backend<S> {
4436+ pub fn new(key: [u8; 32], storage: S) -> Self {
4437+ Backend { key, storage }
4438+ }
4439+ }
4440+
4441+ fn xor_stream<S>(
4442+ mut chacha: ChaCha,
4443+ stream: S,
4444+ ) -> impl Stream<Item = Bytes, Error = io::Error>
4445+ where
4446+ S: Stream<Item = Bytes, Error = io::Error>,
4447+ {
4448+ stream.and_then(move |bytes| {
4449+ let mut bytes = BytesMut::from(bytes);
4450+
4451+ chacha.xor_read(bytes.as_mut()).map_err(|_| {
4452+ io::Error::new(
4453+ io::ErrorKind::Other,
4454+ "reached end of xchacha20 keystream",
4455+ )
4456+ })?;
4457+
4458+ Ok(bytes.freeze())
4459+ })
4460+ }
4461+
4462+ impl<S> Storage for Backend<S>
4463+ where
4464+ S: Storage + Send + Sync + 'static,
4465+ S::Error: 'static,
4466+ {
4467+ type Error = S::Error;
4468+
4469+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error> {
4470+ // Use the first part of the SHA256 as the nonce.
4471+ let mut nonce: [u8; 24] = [0; 24];
4472+ nonce.copy_from_slice(&key.bytes()[0..24]);
4473+
4474+ let chacha = ChaCha::new_xchacha20(&self.key, &nonce);
4475+
4476+ Box::new(self.storage.get(key).and_then(move |obj| match obj {
4477+ Some(obj) => {
4478+ let (len, stream) = obj.into_parts();
4479+ Ok(Some(LFSObject::new(
4480+ len,
4481+ Box::new(xor_stream(chacha, stream)),
4482+ )))
4483+ }
4484+ None => Ok(None),
4485+ }))
4486+ }
4487+
4488+ fn put(
4489+ &self,
4490+ key: &Oid,
4491+ value: LFSObject,
4492+ ) -> StorageFuture<(), Self::Error> {
4493+ // Use the first part of the SHA256 as the nonce.
4494+ let mut nonce: [u8; 24] = [0; 24];
4495+ nonce.copy_from_slice(&key.bytes()[0..24]);
4496+
4497+ let chacha = ChaCha::new_xchacha20(&self.key, &nonce);
4498+
4499+ let (len, stream) = value.into_parts();
4500+ let stream = xor_stream(chacha, stream);
4501+
4502+ self.storage.put(key, LFSObject::new(len, Box::new(stream)))
4503+ }
4504+
4505+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error> {
4506+ self.storage.size(key)
4507+ }
4508+
4509+ fn delete(&self, key: &Oid) -> StorageFuture<(), Self::Error> {
4510+ self.storage.delete(key)
4511+ }
4512+
4513+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error> {
4514+ self.storage.list()
4515+ }
4516+
4517+ fn total_size(&self) -> Option<u64> {
4518+ self.storage.total_size()
4519+ }
4520+
4521+ fn max_size(&self) -> Option<u64> {
4522+ self.storage.max_size()
4523+ }
4524+ }
4525 diff --git a/src/storage/mod.rs b/src/storage/mod.rs
4526new file mode 100644
4527index 0000000..2e5407a
4528--- /dev/null
4529+++ b/src/storage/mod.rs
4530 @@ -0,0 +1,193 @@
4531+ // Copyright (c) 2019 Jason White
4532+ //
4533+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4534+ // of this software and associated documentation files (the "Software"), to deal
4535+ // in the Software without restriction, including without limitation the rights
4536+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4537+ // copies of the Software, and to permit persons to whom the Software is
4538+ // furnished to do so, subject to the following conditions:
4539+ //
4540+ // The above copyright notice and this permission notice shall be included in
4541+ // all copies or substantial portions of the Software.
4542+ //
4543+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4544+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4545+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4546+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4547+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4548+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4549+ // SOFTWARE.
4550+ mod cached;
4551+ mod disk;
4552+ mod encrypt;
4553+ mod s3;
4554+ mod verify;
4555+
4556+ pub use cached::{Backend as Cached, Error as CacheError};
4557+ pub use disk::Backend as Disk;
4558+ pub use encrypt::Backend as Encrypted;
4559+ pub use s3::{Backend as S3, Error as S3Error};
4560+ pub use verify::Backend as Verify;
4561+
4562+ use std::fmt;
4563+
4564+ use bytes::Bytes;
4565+ use std::io;
4566+
4567+ use crate::lfs::Oid;
4568+ use futures::{sync::mpsc, Future, Sink, Stream};
4569+
4570+ pub type S3DiskCache = Cached<Disk, S3>;
4571+
4572+ /// Future returned by storage operations.
4573+ pub type StorageFuture<T, E> = Box<dyn Future<Item = T, Error = E> + Send>;
4574+
4575+ /// Stream returned by storage operations.
4576+ pub type StorageStream<T, E> = Box<dyn Stream<Item = T, Error = E> + Send>;
4577+
4578+ /// The byte stream of an LFS object.
4579+ pub type ByteStream = Box<dyn Stream<Item = Bytes, Error = io::Error> + Send>;
4580+
4581+ /// An LFS object to be uploaded or downloaded.
4582+ pub struct LFSObject {
4583+ /// Size of the object.
4584+ len: u64,
4585+
4586+ /// The stream of bytes.
4587+ stream: ByteStream,
4588+ }
4589+
4590+ impl LFSObject {
4591+ pub fn new(len: u64, stream: ByteStream) -> Self {
4592+ LFSObject { len, stream }
4593+ }
4594+
4595+ pub fn len(&self) -> u64 {
4596+ self.len
4597+ }
4598+
4599+ pub fn stream(self) -> ByteStream {
4600+ self.stream
4601+ }
4602+
4603+ pub fn into_parts(self) -> (u64, ByteStream) {
4604+ (self.len, self.stream)
4605+ }
4606+
4607+ /// Duplicates the underlying byte stream such that we have two identical
4608+ /// LFS object streams that must be consumed in lock-step.
4609+ ///
4610+ /// This is useful for caching LFS objects while simultaneously sending them
4611+ /// to a client.
4612+ pub fn split(self) -> (Self, Self) {
4613+ let (len, stream) = self.into_parts();
4614+
4615+ let (sender, receiver) = mpsc::channel(1);
4616+
4617+ let stream = stream.and_then(move |chunk| {
4618+ // TODO: Find a way to not clone the sender.
4619+ sender
4620+ .clone()
4621+ .send(chunk.clone())
4622+ .and_then(move |_| Ok(chunk))
4623+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
4624+ });
4625+
4626+ let a = LFSObject {
4627+ len,
4628+ stream: Box::new(stream),
4629+ };
4630+
4631+ let b = LFSObject {
4632+ len,
4633+ stream: Box::new(receiver.map_err(|()| {
4634+ io::Error::new(
4635+ io::ErrorKind::Other,
4636+ "failed during body duplication",
4637+ )
4638+ })),
4639+ };
4640+
4641+ (a, b)
4642+ }
4643+ }
4644+
4645+ /// Trait for abstracting away the storage medium.
4646+ pub trait Storage {
4647+ type Error: fmt::Display + Send;
4648+
4649+ /// Gets an entry from the storage medium.
4650+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error>;
4651+
4652+ /// Sets an entry in the storage medium.
4653+ fn put(
4654+ &self,
4655+ key: &Oid,
4656+ value: LFSObject,
4657+ ) -> StorageFuture<(), Self::Error>;
4658+
4659+ /// Gets the size of the object. Returns `None` if the object does not
4660+ /// exist.
4661+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error>;
4662+
4663+ /// Deletes an object.
4664+ ///
4665+ /// Permanent storage backends may choose to never delete objects, always
4666+ /// returning success.
4667+ fn delete(&self, key: &Oid) -> StorageFuture<(), Self::Error>;
4668+
4669+ /// Returns a stream of all the object IDs in the storage medium.
4670+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error>;
4671+
4672+ /// Gets the total size of the storage, if known.
4673+ fn total_size(&self) -> Option<u64> {
4674+ None
4675+ }
4676+
4677+ /// Gets the maximum size of the storage.
4678+ ///
4679+ /// This should return `None` if the storage size is unbounded. This is only
4680+ /// applicable to caches.
4681+ fn max_size(&self) -> Option<u64> {
4682+ None
4683+ }
4684+ }
4685+
4686+ impl<S> Storage for Box<S>
4687+ where
4688+ S: Storage + ?Sized,
4689+ {
4690+ type Error = S::Error;
4691+
4692+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error> {
4693+ (**self).get(key)
4694+ }
4695+
4696+ fn put(
4697+ &self,
4698+ key: &Oid,
4699+ value: LFSObject,
4700+ ) -> StorageFuture<(), Self::Error> {
4701+ (**self).put(key, value)
4702+ }
4703+
4704+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error> {
4705+ (**self).size(key)
4706+ }
4707+
4708+ fn delete(&self, key: &Oid) -> StorageFuture<(), Self::Error> {
4709+ (**self).delete(key)
4710+ }
4711+
4712+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error> {
4713+ (**self).list()
4714+ }
4715+
4716+ fn total_size(&self) -> Option<u64> {
4717+ (**self).total_size()
4718+ }
4719+
4720+ fn max_size(&self) -> Option<u64> {
4721+ (**self).max_size()
4722+ }
4723+ }
4724 diff --git a/src/storage/s3.rs b/src/storage/s3.rs
4725new file mode 100644
4726index 0000000..def688c
4727--- /dev/null
4728+++ b/src/storage/s3.rs
4729 @@ -0,0 +1,240 @@
4730+ // Copyright (c) 2019 Jason White
4731+ //
4732+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4733+ // of this software and associated documentation files (the "Software"), to deal
4734+ // in the Software without restriction, including without limitation the rights
4735+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4736+ // copies of the Software, and to permit persons to whom the Software is
4737+ // furnished to do so, subject to the following conditions:
4738+ //
4739+ // The above copyright notice and this permission notice shall be included in
4740+ // all copies or substantial portions of the Software.
4741+ //
4742+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4743+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4744+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4745+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4746+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4747+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4748+ // SOFTWARE.
4749+ use bytes::Bytes;
4750+ use derive_more::{Display, From};
4751+ use futures::{future, stream, Future, Stream};
4752+ use http::StatusCode;
4753+ use rusoto_core::Region;
4754+ use rusoto_s3::{
4755+ GetObjectError, GetObjectRequest, HeadBucketError, HeadBucketRequest,
4756+ HeadObjectError, HeadObjectRequest, PutObjectError, PutObjectRequest,
4757+ S3Client, StreamingBody, S3,
4758+ };
4759+
4760+ use super::{LFSObject, Storage, StorageFuture, StorageStream};
4761+
4762+ use crate::lfs::Oid;
4763+
4764+ #[derive(Debug, From, Display)]
4765+ pub enum Error {
4766+ GetObjectError(GetObjectError),
4767+ PutObjectError(PutObjectError),
4768+ HeadObjectError(HeadObjectError),
4769+
4770+ /// Initialization error.
4771+ Init(InitError),
4772+
4773+ /// The uploaded object is too large.
4774+ TooLarge(u64),
4775+ }
4776+
4777+ impl ::std::error::Error for Error {}
4778+
4779+ #[derive(Debug, Display)]
4780+ pub enum InitError {
4781+ #[display(fmt = "Invalid S3 bucket name")]
4782+ Bucket,
4783+
4784+ #[display(fmt = "Invalid S3 credentials")]
4785+ Credentials,
4786+
4787+ #[display(fmt = "{}", _0)]
4788+ Other(HeadBucketError),
4789+ }
4790+
4791+ impl From<HeadBucketError> for InitError {
4792+ fn from(err: HeadBucketError) -> Self {
4793+ match err {
4794+ HeadBucketError::Unknown(r) => {
4795+ // Rusoto really sucks at correctly reporting errors.
4796+ // Lets work around that here.
4797+ match r.status {
4798+ StatusCode::NOT_FOUND => InitError::Bucket,
4799+ StatusCode::FORBIDDEN => InitError::Credentials,
4800+ _ => InitError::Other(HeadBucketError::Unknown(r)),
4801+ }
4802+ }
4803+ x => InitError::Other(x),
4804+ }
4805+ }
4806+ }
4807+
4808+ /// Amazon S3 storage backend.
4809+ pub struct Backend<C = S3Client> {
4810+ /// S3 client.
4811+ client: C,
4812+
4813+ /// Name of the bucket to use.
4814+ bucket: String,
4815+
4816+ /// Prefix for objects.
4817+ prefix: String,
4818+ }
4819+
4820+ impl Backend {
4821+ pub fn new(
4822+ bucket: String,
4823+ prefix: String,
4824+ ) -> impl Future<Item = Self, Error = Error> {
4825+ Backend::with_client(S3Client::new(Region::default()), bucket, prefix)
4826+ }
4827+ }
4828+
4829+ impl<C> Backend<C>
4830+ where
4831+ C: S3,
4832+ {
4833+ pub fn with_client(
4834+ client: C,
4835+ bucket: String,
4836+ prefix: String,
4837+ ) -> impl Future<Item = Self, Error = Error> {
4838+ // Peform a HEAD operation to check that the bucket exists and that our
4839+ // credentials work. This helps catch very common errors early on
4840+ // application startup.
4841+ let req = HeadBucketRequest {
4842+ bucket: bucket.clone(),
4843+ };
4844+
4845+ client
4846+ .head_bucket(req)
4847+ .map_err(InitError::from)
4848+ .from_err()
4849+ .map(move |()| Backend {
4850+ client,
4851+ bucket,
4852+ prefix,
4853+ })
4854+ }
4855+
4856+ fn key_to_path(&self, oid: &Oid) -> String {
4857+ format!("{}/{}", self.prefix, oid.path())
4858+ }
4859+ }
4860+
4861+ impl<C> Storage for Backend<C>
4862+ where
4863+ C: S3,
4864+ {
4865+ type Error = Error;
4866+
4867+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error> {
4868+ let request = GetObjectRequest {
4869+ bucket: self.bucket.clone(),
4870+ key: self.key_to_path(key),
4871+ response_content_type: Some("application/octet-stream".into()),
4872+ ..Default::default()
4873+ };
4874+
4875+ Box::new(
4876+ self.client
4877+ .get_object(request)
4878+ .then(move |result| match result {
4879+ Ok(object) => Ok(Some(LFSObject::new(
4880+ object.content_length.unwrap() as u64,
4881+ Box::new(object.body.unwrap().map(Bytes::from)),
4882+ ))),
4883+ Err(err) => {
4884+ if let GetObjectError::NoSuchKey(_) = &err {
4885+ Ok(None)
4886+ } else {
4887+ Err(err)
4888+ }
4889+ }
4890+ })
4891+ .from_err(),
4892+ )
4893+ }
4894+
4895+ fn put(
4896+ &self,
4897+ key: &Oid,
4898+ value: LFSObject,
4899+ ) -> StorageFuture<(), Self::Error> {
4900+ // TODO:
4901+ // * Put an upper limit on the amount of data that can be uploaded.
4902+ // * Encrypt the data with a private key(?)
4903+ let (len, stream) = value.into_parts();
4904+
4905+ let stream = stream.map(|chunk| {
4906+ // Since Rusoto doesn't use a reference counted chunk of bytes,
4907+ // we must copy to a new `Vec<u8>` here. See:
4908+ // https://github.com/rusoto/rusoto/issues/1028
4909+ Vec::from(chunk.as_ref())
4910+ });
4911+
4912+ let request = PutObjectRequest {
4913+ bucket: self.bucket.clone(),
4914+ key: self.key_to_path(key),
4915+ content_length: Some(len as i64),
4916+ body: Some(StreamingBody::new(stream)),
4917+ ..Default::default()
4918+ };
4919+
4920+ Box::new(self.client.put_object(request).map(|_| ()).from_err())
4921+ }
4922+
4923+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error> {
4924+ let request = HeadObjectRequest {
4925+ bucket: self.bucket.clone(),
4926+ key: self.key_to_path(key),
4927+ ..Default::default()
4928+ };
4929+
4930+ Box::new(
4931+ self.client
4932+ .head_object(request)
4933+ .then(|result| match result {
4934+ Ok(object) => {
4935+ Ok(Some(object.content_length.unwrap() as u64))
4936+ }
4937+ Err(err) => {
4938+ match &err {
4939+ HeadObjectError::Unknown(e) => {
4940+ // There is a bug in Rusoto that causes it to
4941+ // always return an "unknown" error when the key
4942+ // does not exist. Thus we must check the error
4943+ // code manually. See:
4944+ // https://github.com/rusoto/rusoto/issues/716
4945+ if e.status == 404 {
4946+ Ok(None)
4947+ } else {
4948+ Err(err)
4949+ }
4950+ }
4951+ HeadObjectError::NoSuchKey(_) => Ok(None),
4952+ _ => Err(err),
4953+ }
4954+ }
4955+ })
4956+ .from_err(),
4957+ )
4958+ }
4959+
4960+ /// This never deletes objects from S3 and always returns success.
4961+ fn delete(&self, _key: &Oid) -> StorageFuture<(), Self::Error> {
4962+ Box::new(future::ok(()))
4963+ }
4964+
4965+ /// Always returns an empty stream. This may be changed in the future.
4966+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error> {
4967+ Box::new(stream::empty())
4968+ }
4969+ }
4970 diff --git a/src/storage/verify.rs b/src/storage/verify.rs
4971new file mode 100644
4972index 0000000..32ac44f
4973--- /dev/null
4974+++ b/src/storage/verify.rs
4975 @@ -0,0 +1,146 @@
4976+ // Copyright (c) 2019 Jason White
4977+ //
4978+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4979+ // of this software and associated documentation files (the "Software"), to deal
4980+ // in the Software without restriction, including without limitation the rights
4981+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4982+ // copies of the Software, and to permit persons to whom the Software is
4983+ // furnished to do so, subject to the following conditions:
4984+ //
4985+ // The above copyright notice and this permission notice shall be included in
4986+ // all copies or substantial portions of the Software.
4987+ //
4988+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4989+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4990+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4991+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4992+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4993+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4994+ // SOFTWARE.
4995+ use std::io;
4996+ use std::sync::Arc;
4997+
4998+ use derive_more::{Display, From};
4999+ use futures::{
5000+ future::{self, Either},
5001+ Future, Stream,
5002+ };
5003+
5004+ use crate::lfs::Oid;
5005+ use crate::sha256::{Sha256VerifyError, VerifyStream};
5006+
5007+ use super::{LFSObject, Storage, StorageFuture, StorageStream};
5008+
5009+ #[derive(Debug, Display, From)]
5010+ enum Error {
5011+ Verify(Sha256VerifyError),
5012+ Io(io::Error),
5013+ }
5014+
5015+ impl std::error::Error for Error {}
5016+
5017+ /// Verifies LFS objects as they are uploaded or downloaded.
5018+ ///
5019+ /// If corruption is detected upon upload, the object is rejected.
5020+ ///
5021+ /// If corruption is detected upon download, the object is deleted from the
5022+ /// underlying storage backend.
5023+ ///
5024+ /// Note that this must be composed with decrypted data. Otherwise, the
5025+ /// verification will always fail. Thus, this should be the last thing between
5026+ /// the client and the other storage backends.
5027+ pub struct Backend<S> {
5028+ storage: Arc<S>,
5029+ }
5030+
5031+ impl<S> Backend<S> {
5032+ pub fn new(storage: S) -> Self {
5033+ Backend {
5034+ storage: Arc::new(storage),
5035+ }
5036+ }
5037+ }
5038+
5039+ impl<S> Storage for Backend<S>
5040+ where
5041+ S: Storage + Send + Sync + 'static,
5042+ S::Error: 'static,
5043+ {
5044+ type Error = S::Error;
5045+
5046+ /// Verifies that the downloaded object has a valid SHA256. If it doesn't,
5047+ /// then it means our storage is corrupted. Thus, we automatically delete it
5048+ /// from storage if it is corrupted. If storage backends are composed
5049+ /// correctly, then it should only delete cache storage (not permanent
5050+ /// storage).
5051+ fn get(&self, key: &Oid) -> StorageFuture<Option<LFSObject>, Self::Error> {
5052+ let key = *key;
5053+
5054+ let storage = self.storage.clone();
5055+
5056+ Box::new(self.storage.get(&key).map(move |obj| match obj {
5057+ Some(obj) => {
5058+ let (len, stream) = obj.into_parts();
5059+
5060+ let stream =
5061+ VerifyStream::new(stream.map_err(Error::from), len, key)
5062+ .or_else(move |err| match err {
5063+ Error::Verify(err) => {
5064+ log::error!(
5065+ "Deleting corrupted object {} ({})",
5066+ key,
5067+ err
5068+ );
5069+
5070+ Either::A(storage.delete(&key).then(|_| {
5071+ Err(io::Error::new(
5072+ io::ErrorKind::Other,
5073+ "found corrupted object",
5074+ ))
5075+ }))
5076+ }
5077+ Error::Io(err) => Either::B(future::err(err)),
5078+ });
5079+
5080+ Some(LFSObject::new(len, Box::new(stream)))
5081+ }
5082+ None => None,
5083+ }))
5084+ }
5085+
5086+ fn put(
5087+ &self,
5088+ key: &Oid,
5089+ value: LFSObject,
5090+ ) -> StorageFuture<(), Self::Error> {
5091+ let (len, stream) = value.into_parts();
5092+
5093+ let stream = VerifyStream::new(stream.map_err(Error::from), len, *key)
5094+ .map_err(move |err| match err {
5095+ Error::Verify(err) => io::Error::new(io::ErrorKind::Other, err),
5096+ Error::Io(err) => io::Error::new(io::ErrorKind::Other, err),
5097+ });
5098+
5099+ self.storage.put(key, LFSObject::new(len, Box::new(stream)))
5100+ }
5101+
5102+ fn size(&self, key: &Oid) -> StorageFuture<Option<u64>, Self::Error> {
5103+ self.storage.size(key)
5104+ }
5105+
5106+ fn delete(&self, key: &Oid) -> StorageFuture<(), Self::Error> {
5107+ self.storage.delete(key)
5108+ }
5109+
5110+ fn list(&self) -> StorageStream<(Oid, u64), Self::Error> {
5111+ self.storage.list()
5112+ }
5113+
5114+ fn total_size(&self) -> Option<u64> {
5115+ self.storage.total_size()
5116+ }
5117+
5118+ fn max_size(&self) -> Option<u64> {
5119+ self.storage.max_size()
5120+ }
5121+ }