1
- use anyhow:: { anyhow, bail, Context , Result } ;
2
- use bmap:: { Bmap , Discarder , SeekForward } ;
3
- use clap:: Parser ;
1
+ use anyhow:: { anyhow, bail, ensure, Context , Result } ;
2
+ use async_compression:: futures:: bufread:: GzipDecoder ;
3
+ use bmap:: { AsyncDiscarder , Bmap , Discarder , SeekForward } ;
4
+ use clap:: { arg, command, Command } ;
4
5
use flate2:: read:: GzDecoder ;
6
+ use futures:: TryStreamExt ;
5
7
use indicatif:: { ProgressBar , ProgressState , ProgressStyle } ;
6
8
use nix:: unistd:: ftruncate;
9
+ use reqwest:: { Response , Url } ;
7
10
use std:: ffi:: OsStr ;
8
11
use std:: fmt:: Write ;
9
12
use std:: fs:: File ;
10
13
use std:: io:: Read ;
11
14
use std:: os:: unix:: io:: AsRawFd ;
12
15
use std:: path:: { Path , PathBuf } ;
16
+ use tokio_util:: compat:: TokioAsyncReadCompatExt ;
13
17
14
- #[ derive( Parser , Debug ) ]
18
+ #[ derive( Debug ) ]
19
+ enum Image {
20
+ Path ( PathBuf ) ,
21
+ Url ( Url ) ,
22
+ }
23
+
24
+ #[ derive( Debug ) ]
15
25
struct Copy {
16
- image : PathBuf ,
26
+ image : Image ,
17
27
dest : PathBuf ,
18
28
}
19
29
20
- #[ derive( Parser , Debug ) ]
30
+ #[ derive( Debug ) ]
21
31
22
- enum Command {
32
+ enum Subcommand {
23
33
Copy ( Copy ) ,
24
34
}
25
35
26
- #[ derive( Parser , Debug ) ]
36
+ #[ derive( Debug ) ]
27
37
struct Opts {
28
- #[ command( subcommand) ]
29
- command : Command ,
38
+ command : Subcommand ,
39
+ }
40
+
41
+ impl Opts {
42
+ fn parser ( ) -> Opts {
43
+ let matches = command ! ( )
44
+ . propagate_version ( true )
45
+ . subcommand_required ( true )
46
+ . arg_required_else_help ( true )
47
+ . subcommand (
48
+ Command :: new ( "copy" )
49
+ . about ( "Copy image to block device or file" )
50
+ . arg ( arg ! ( [ IMAGE ] ) . required ( true ) )
51
+ . arg ( arg ! ( [ DESTINATION ] ) . required ( true ) ) ,
52
+ )
53
+ . get_matches ( ) ;
54
+ match matches. subcommand ( ) {
55
+ Some ( ( "copy" , sub_matches) ) => Opts {
56
+ command : Subcommand :: Copy ( {
57
+ Copy {
58
+ image : match Url :: parse ( sub_matches. get_one :: < String > ( "IMAGE" ) . unwrap ( ) ) {
59
+ Ok ( url) => Image :: Url ( url) ,
60
+ Err ( _) => Image :: Path ( PathBuf :: from (
61
+ sub_matches. get_one :: < String > ( "IMAGE" ) . unwrap ( ) ,
62
+ ) ) ,
63
+ } ,
64
+ dest : PathBuf :: from ( sub_matches. get_one :: < String > ( "DESTINATION" ) . unwrap ( ) ) ,
65
+ }
66
+ } ) ,
67
+ } ,
68
+ _ => unreachable ! (
69
+ "Exhausted list of subcommands and subcommand_required prevents `None`"
70
+ ) ,
71
+ }
72
+ }
30
73
}
31
74
32
75
fn append ( path : PathBuf ) -> PathBuf {
@@ -51,6 +94,13 @@ fn find_bmap(img: &Path) -> Option<PathBuf> {
51
94
}
52
95
}
53
96
97
+ fn find_remote_bmap ( mut url : Url ) -> Result < Url > {
98
+ let mut path = PathBuf :: from ( url. path ( ) ) ;
99
+ path. set_extension ( "bmap" ) ;
100
+ url. set_path ( path. to_str ( ) . unwrap ( ) ) ;
101
+ Ok ( url)
102
+ }
103
+
54
104
trait ReadSeekForward : SeekForward + Read { }
55
105
impl < T : Read + SeekForward > ReadSeekForward for T { }
56
106
@@ -78,7 +128,7 @@ impl SeekForward for Decoder {
78
128
}
79
129
}
80
130
81
- fn setup_input ( path : & Path ) -> Result < Decoder > {
131
+ fn setup_local_input ( path : & Path ) -> Result < Decoder > {
82
132
let f = File :: open ( path) ?;
83
133
match path. extension ( ) . and_then ( OsStr :: to_str) {
84
134
Some ( "gz" ) => {
@@ -89,12 +139,44 @@ fn setup_input(path: &Path) -> Result<Decoder> {
89
139
}
90
140
}
91
141
92
- fn copy ( c : Copy ) -> Result < ( ) > {
93
- if !c. image . exists ( ) {
94
- bail ! ( "Image file doesn't exist" )
142
+ async fn setup_remote_input ( url : Url ) -> Result < Response > {
143
+ match PathBuf :: from ( url. path ( ) )
144
+ . extension ( )
145
+ . and_then ( OsStr :: to_str)
146
+ {
147
+ Some ( "gz" ) => reqwest:: get ( url) . await . map_err ( anyhow:: Error :: new) ,
148
+ None => bail ! ( "No file extension found" ) ,
149
+ _ => bail ! ( "Image file format not implemented" ) ,
95
150
}
151
+ }
96
152
97
- let bmap = find_bmap ( & c. image ) . ok_or_else ( || anyhow ! ( "Couldn't find bmap file" ) ) ?;
153
+ fn setup_progress_bar ( bmap : & Bmap ) -> ProgressBar {
154
+ let pb = ProgressBar :: new ( bmap. total_mapped_size ( ) ) ;
155
+ pb. set_style ( ProgressStyle :: with_template ( "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})" )
156
+ . unwrap ( )
157
+ . with_key ( "eta" , |state : & ProgressState , w : & mut dyn Write | write ! ( w, "{:.1}s" , state. eta( ) . as_secs_f64( ) ) . unwrap ( ) )
158
+ . progress_chars ( "#>-" ) ) ;
159
+ pb
160
+ }
161
+
162
+ fn setup_output < T : AsRawFd > ( output : & T , bmap : & Bmap , metadata : std:: fs:: Metadata ) -> Result < ( ) > {
163
+ if metadata. is_file ( ) {
164
+ ftruncate ( output. as_raw_fd ( ) , bmap. image_size ( ) as i64 )
165
+ . context ( "Failed to truncate file" ) ?;
166
+ }
167
+ Ok ( ( ) )
168
+ }
169
+
170
+ async fn copy ( c : Copy ) -> Result < ( ) > {
171
+ match c. image {
172
+ Image :: Path ( path) => copy_local_input ( path, c. dest ) ,
173
+ Image :: Url ( url) => copy_remote_input ( url, c. dest ) . await ,
174
+ }
175
+ }
176
+
177
+ fn copy_local_input ( source : PathBuf , destination : PathBuf ) -> Result < ( ) > {
178
+ ensure ! ( source. exists( ) , "Image file doesn't exist" ) ;
179
+ let bmap = find_bmap ( & source) . ok_or_else ( || anyhow ! ( "Couldn't find bmap file" ) ) ?;
98
180
println ! ( "Found bmap file: {}" , bmap. display( ) ) ;
99
181
100
182
let mut b = File :: open ( & bmap) . context ( "Failed to open bmap file" ) ?;
@@ -105,32 +187,63 @@ fn copy(c: Copy) -> Result<()> {
105
187
let output = std:: fs:: OpenOptions :: new ( )
106
188
. write ( true )
107
189
. create ( true )
108
- . open ( c . dest ) ?;
190
+ . open ( destination ) ?;
109
191
110
- if output. metadata ( ) ?. is_file ( ) {
111
- ftruncate ( output. as_raw_fd ( ) , bmap. image_size ( ) as i64 )
112
- . context ( "Failed to truncate file" ) ?;
113
- }
192
+ setup_output ( & output, & bmap, output. metadata ( ) ?) ?;
114
193
115
- let mut input = setup_input ( & c. image ) ?;
116
- let pb = ProgressBar :: new ( bmap. total_mapped_size ( ) ) ;
117
- pb. set_style ( ProgressStyle :: with_template ( "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})" )
118
- . unwrap ( )
119
- . with_key ( "eta" , |state : & ProgressState , w : & mut dyn Write | write ! ( w, "{:.1}s" , state. eta( ) . as_secs_f64( ) ) . unwrap ( ) )
120
- . progress_chars ( "#>-" ) ) ;
194
+ let mut input = setup_local_input ( & source) ?;
195
+ let pb = setup_progress_bar ( & bmap) ;
121
196
bmap:: copy ( & mut input, & mut pb. wrap_write ( & output) , & bmap) ?;
122
197
pb. finish_and_clear ( ) ;
123
198
124
199
println ! ( "Done: Syncing..." ) ;
125
- output. sync_all ( ) . expect ( "Sync failure" ) ;
200
+ output. sync_all ( ) ?;
201
+
202
+ Ok ( ( ) )
203
+ }
204
+
205
+ async fn copy_remote_input ( source : Url , destination : PathBuf ) -> Result < ( ) > {
206
+ let bmap_url = find_remote_bmap ( source. clone ( ) ) ?;
207
+
208
+ let xml = reqwest:: get ( bmap_url. clone ( ) ) . await ?. text ( ) . await ?;
209
+ println ! ( "Found bmap file: {}" , bmap_url) ;
210
+
211
+ let bmap = Bmap :: from_xml ( & xml) ?;
212
+ let mut output = tokio:: fs:: OpenOptions :: new ( )
213
+ . write ( true )
214
+ . create ( true )
215
+ . open ( destination)
216
+ . await ?;
217
+
218
+ setup_output ( & output, & bmap, output. metadata ( ) . await ?) ?;
219
+
220
+ let res = setup_remote_input ( source) . await ?;
221
+ let stream = res
222
+ . bytes_stream ( )
223
+ . map_err ( |e| std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , e) )
224
+ . into_async_read ( ) ;
225
+ let reader = GzipDecoder :: new ( stream) ;
226
+ let mut input = AsyncDiscarder :: new ( reader) ;
227
+ let pb = setup_progress_bar ( & bmap) ;
228
+ bmap:: copy_async (
229
+ & mut input,
230
+ & mut pb. wrap_async_write ( & mut output) . compat ( ) ,
231
+ & bmap,
232
+ )
233
+ . await ?;
234
+ pb. finish_and_clear ( ) ;
235
+
236
+ println ! ( "Done: Syncing..." ) ;
237
+ output. sync_all ( ) . await ?;
126
238
127
239
Ok ( ( ) )
128
240
}
129
241
130
- fn main ( ) -> Result < ( ) > {
131
- let opts = Opts :: parse ( ) ;
242
+ #[ tokio:: main]
243
+ async fn main ( ) -> Result < ( ) > {
244
+ let opts = Opts :: parser ( ) ;
132
245
133
246
match opts. command {
134
- Command :: Copy ( c) => copy ( c) ,
247
+ Subcommand :: Copy ( c) => copy ( c) . await ,
135
248
}
136
249
}
0 commit comments