4cf0c99a Andy McCall 24 days ago ↶
1
2
3
4
5
6
7
8
use crate :: home:: Home;
9
use anyhow:: { bail, Context, Result } ;
10
use jet:: images:: { self , Chunk, Manifest} ;
11
use std:: collections:: HashSet;
12
use std:: path:: { Path, PathBuf} ;
13
use std:: sync:: Mutex;
14
use std:: time:: { Duration, Instant} ;
15
16
17
const PARALLEL : usize = 8 ;
01043bff Andy McCall 23 days ago ↶
18
19
const ASK : usize = 20_000 ;
4cf0c99a Andy McCall 24 days ago ↶
20
21
22
pub struct Client {
b14aeaae Andy McCall 23 days ago ↶
23
root : String,
4cf0c99a Andy McCall 24 days ago ↶
24
base : String,
25
token : String,
26
agent : ureq:: Agent,
27
}
28
29
30
pub struct Moved {
31
pub chunks : usize ,
32
pub bytes : u64 ,
b14aeaae Andy McCall 23 days ago ↶
33
34
pub peer_bytes : u64 ,
4cf0c99a Andy McCall 24 days ago ↶
35
pub elapsed : Duration,
36
}
37
38
impl Client {
39
pub fn new ( hub : & str , token : & str , ca : Option< & Path> ) -> Result< Client> {
40
let tls = oxy_core:: protocol:: tls_client_config( ca) .map_err ( | e | anyhow:: anyhow! ( " {e}" ) ) ? ;
41
let agent = ureq:: AgentBuilder:: new( ) .tls_config ( tls) .timeout ( Duration:: from_secs( 120 ) ) .build ( ) ;
b14aeaae Andy McCall 23 days ago ↶
42
let root = hub.trim_end_matches ( ' /' ) .to_string ( ) ;
43
Ok ( Client { base: format! ( " {root} /api/v1" ) , root, token: token.into ( ) , agent } )
4cf0c99a Andy McCall 24 days ago ↶
44
}
45
46
fn auth ( & self ) -> String {
47
format! ( " Bearer {} " , self .token)
48
}
49
50
fn fail ( url : & str , e : ureq:: Error) -> anyhow:: Error {
51
match e {
52
ureq:: Error:: Status( code, r) => {
53
let text = r.into_string ( ) .unwrap_or_default ( ) ;
54
let detail = serde_json:: from_str:: < serde_json:: Value> ( & text)
55
.ok ( )
56
.and_then ( | v | v[ " detail" ] .as_str ( ) .map ( String :: from) )
57
.unwrap_or ( text) ;
58
anyhow:: anyhow! ( " {url}: HTTP {code}: {detail}" )
59
}
60
e => anyhow:: anyhow! ( " {url}: {e}" ) ,
61
}
62
}
63
64
65
pub fn manifest ( & self , hash : & str ) -> Result< Option< Manifest> > {
66
let url = format! ( " {} /flow/images/{hash} " , self .base) ;
67
match self .agent.get ( & url) .set ( " Authorization" , & self .auth ( ) ) .call ( ) {
68
Ok ( r) => Ok ( Some ( r.into_json ( ) ? ) ) ,
69
Err ( ureq:: Error:: Status( 404 , _ ) ) => Ok ( None ) ,
70
Err ( e) => Err ( Self :: fail( & url, e) ) ,
71
}
72
}
73
b14aeaae Andy McCall 23 days ago ↶
74
75
76
pub fn hub_identity ( & self ) -> Result< oxy_core:: identity:: Identity> {
77
let url = format! ( " {} /.well-known/oxyhub-key" , self .root) ;
78
let r = self .agent.get ( & url) .call ( ) .map_err ( | e | Self :: fail( & url, e) ) ? ;
79
let mut out = Vec :: new( ) ;
80
std:: io:: Read:: read_to_end( & mut r.into_reader ( ) , & mut out) ? ;
81
oxy_core:: identity:: Identity:: decode( & out) .map_err ( | e | anyhow:: anyhow! ( " {url}: not a hub identity: {e}" ) )
82
}
83
84
85
86
pub fn announce ( & self , hash : & str , state : & str ) -> Result< flow_core:: api:: Announced> {
87
let url = format! ( " {} /flow/swarm/announce" , self .base) ;
88
let body = flow_core:: api:: Announce { image: hash.into ( ) , state: state.into ( ) } ;
89
Ok ( self .agent.post ( & url) .set ( " Authorization" , & self .auth ( ) ) .send_json ( & body) .map_err ( | e | Self :: fail( & url, e) ) ? .into_json ( ) ? )
90
}
91
92
pub ( crate ) fn chunk ( & self , hash : & str ) -> Result< Vec< u8 > > {
4cf0c99a Andy McCall 24 days ago ↶
93
let url = format! ( " {} /flow/images/chunks/{hash} " , self .base) ;
94
let r = self .agent.get ( & url) .set ( " Authorization" , & self .auth ( ) ) .call ( ) .map_err ( | e | Self :: fail( & url, e) ) ? ;
95
let mut out = Vec :: new( ) ;
96
std:: io:: Read:: read_to_end( & mut r.into_reader ( ) , & mut out) ? ;
97
Ok ( out)
98
}
99
100
101
102
pub fn push ( & self , home : & Home, hash : & str ) -> Result< Moved> {
103
let start = Instant:: now( ) ;
104
let path = home.rootfs ( hash) ;
105
if ! path.is_file ( ) {
106
bail! ( " {} is not in this Flow home ({}); import it first" , & hash[ .. 12. min ( hash.len ( ) ) ] , path.display ( ) ) ;
107
}
108
let m = images:: scan_file( & path, home.image_arch ( hash) , | _, _| Ok ( ( ) ) ) ? ;
109
if m.hash ! = hash {
110
bail! ( " {} holds an image named {}, not {}: the file was changed" , path.display ( ) , & m.hash[ .. 12 ] , & hash[ .. 12 ] ) ;
111
}
112
let mut missing: HashSet< String> = HashSet:: new( ) ;
01043bff Andy McCall 23 days ago ↶
113
114
115
for ask in m.needed ( ) .chunks ( ASK ) {
4cf0c99a Andy McCall 24 days ago ↶
116
let url = format! ( " {} /flow/images/missing" , self .base) ;
117
let r: serde_json:: Value = self
118
.agent
119
.post ( & url)
120
.set ( " Authorization" , & self .auth ( ) )
121
.send_json ( serde_json:: json! ( { " chunks" : ask } ) )
122
.map_err ( | e | Self :: fail( & url, e) ) ?
123
.into_json ( ) ? ;
124
missing.extend ( r[ " missing" ] .as_array ( ) .into_iter ( ) .flatten ( ) .filter_map ( | h | h.as_str ( ) .map ( String :: from) ) ) ;
125
}
126
127
let ( count, bytes) = ( missing.len ( ) , Mutex:: new( 0 u64 ) ) ;
128
let queue: Mutex< Vec< ( String, Vec< u8 > ) > > = Mutex:: new( Vec :: new( ) ) ;
129
let failed: Mutex< Option< anyhow:: Error> > = Mutex:: new( None ) ;
130
let sent = Mutex:: new( HashSet:: new( ) ) ;
131
let send = | c : & str , b : & [u8 ]| -> Result< ( ) > {
132
let url = format! ( " {} /flow/images/chunks/{c} " , self .base) ;
133
self .agent.put ( & url) .set ( " Authorization" , & self .auth ( ) ) .send_bytes ( b) .map_err ( | e | Self :: fail( & url, e) ) ? ;
134
* bytes.lock ( ) .unwrap ( ) + = b.len ( ) as u64 ;
135
Ok ( ( ) )
136
} ;
137
images:: scan_file( & path, None , | c : & Chunk, b : & [u8 ]| {
138
if missing.contains ( c.hash ( ) ) & & sent.lock ( ) .unwrap ( ) .insert ( c.hash ( ) .to_string ( ) ) {
139
queue.lock ( ) .unwrap ( ) .push ( ( c.hash ( ) .to_string ( ) , b.to_vec ( ) ) ) ;
140
}
141
if queue.lock ( ) .unwrap ( ) .len ( ) > = PARALLEL * 4 {
142
let batch: Vec< _ > = std:: mem:: take( & mut * queue.lock ( ) .unwrap ( ) ) ;
143
send_batch ( & batch, & send, & failed) ;
144
}
145
match failed.lock ( ) .unwrap ( ) .take ( ) {
146
Some ( e) => Err ( images:: Error:: Bad( format! ( " {e:#} " ) ) ) ,
147
None => Ok ( ( ) ) ,
148
}
149
} ) ? ;
150
let rest: Vec< _ > = std:: mem:: take( & mut * queue.lock ( ) .unwrap ( ) ) ;
151
send_batch ( & rest, & send, & failed) ;
152
if let Some ( e) = failed.lock ( ) .unwrap ( ) .take ( ) {
153
return Err ( e) ;
154
}
155
let url = format! ( " {} /flow/images/{hash} " , self .base) ;
8a4f5174 Andy McCall 9 days ago ↶
156
157
158
self .agent
159
.put ( & url)
160
.timeout ( Duration:: from_secs( 3600 ) )
161
.set ( " Authorization" , & self .auth ( ) )
162
.send_json ( & m)
163
.map_err ( | e | Self :: fail( & url, e) ) ? ;
4cf0c99a Andy McCall 24 days ago ↶
164
let bytes = * bytes.lock ( ) .unwrap ( ) ;
b14aeaae Andy McCall 23 days ago ↶
165
Ok ( Moved { chunks: count, bytes, peer_bytes: 0 , elapsed: start.elapsed ( ) } )
4cf0c99a Andy McCall 24 days ago ↶
166
}
167
168
169
170
171
pub fn fetch ( & self , home : & Home, hash : & str ) -> Result< Option< Moved> > {
172
let start = Instant:: now( ) ;
173
let Some ( m) = self .manifest ( hash) ? else { return Ok ( None ) } ;
174
if m.hash ! = hash {
175
bail! ( " the hub's manifest for {} names {}" , & hash[ .. 12 ] , & m.hash[ .. 12 ] ) ;
176
}
177
let cache = chunk_cache ( home) ;
b14aeaae Andy McCall 23 days ago ↶
178
save_manifest ( & home.root, & m) ? ;
4cf0c99a Andy McCall 24 days ago ↶
179
let wanted: Vec< String> = m.needed ( ) .into_iter ( ) .filter ( | h | ! cache_path ( & cache, h) .is_file ( ) ) .map ( String :: from) .collect ( ) ;
180
let ( bytes, failed) = ( Mutex:: new( 0 u64 ) , Mutex:: new( None : : < anyhow:: Error> ) ) ;
181
let next = Mutex:: new( wanted.iter ( ) ) ;
182
std:: thread:: scope( | s | {
183
for _ in 0 .. PARALLEL {
184
s.spawn ( | | loop {
185
let Some ( h) = next.lock ( ) .unwrap ( ) .next ( ) else { break } ;
186
if failed.lock ( ) .unwrap ( ) .is_some ( ) {
187
break ;
188
}
189
let got = self .chunk ( h) .and_then ( | b | {
190
if blake3:: hash( & b) .to_hex ( ) .as_str ( ) ! = h {
191
bail! ( " chunk {} came back with other bytes" , & h[ .. 12 ] ) ;
192
}
193
let p = cache_path ( & cache, h) ;
194
std:: fs:: create_dir_all( p.parent ( ) .expect ( " a chunk directory" ) ) ? ;
195
let tmp = p.with_extension ( " part" ) ;
196
std:: fs:: write( & tmp, & b) ? ;
197
std:: fs:: rename( & tmp, & p) ? ;
198
* bytes.lock ( ) .unwrap ( ) + = b.len ( ) as u64 ;
199
Ok ( ( ) )
200
} ) ;
201
if let Err ( e) = got {
202
* failed.lock ( ) .unwrap ( ) = Some ( e) ;
203
break ;
204
}
205
} ) ;
206
}
207
} ) ;
208
if let Some ( e) = failed.into_inner ( ) .unwrap ( ) {
209
return Err ( e) ;
210
}
b14aeaae Andy McCall 23 days ago ↶
211
write_image ( home, & m) ? ;
212
Ok ( Some ( Moved { chunks: wanted.len ( ) , bytes: bytes.into_inner ( ) .unwrap ( ) , peer_bytes: 0 , elapsed: start.elapsed ( ) } ) )
213
}
214
}
215
216
217
218
pub ( crate ) fn write_image ( home : & Home, m : & Manifest) -> Result< ( ) > {
219
let cache = chunk_cache ( home) ;
220
let out = home.rootfs ( & m.hash) ;
221
std:: fs:: create_dir_all( out.parent ( ) .expect ( " the cas directory" ) ) ? ;
222
images:: assemble( m, & out, | c | Ok ( std:: fs:: read( cache_path ( & cache, c.hash ( ) ) ) ? ) )
223
.with_context ( | | format! ( " write image {} " , & m.hash[ .. 12 ] ) ) ? ;
224
if let Some ( arch) = & m.arch {
225
let manifest = serde_json:: json! ( { " arch" : arch, " imported_ms" : std:: time:: SystemTime:: now( ) .duration_since ( std:: time:: UNIX_EPOCH ) .map ( | d | d.as_millis ( ) as u64 ) .unwrap_or ( 0 ) , " from" : " hub" } ) ;
226
std:: fs:: write( home.image_manifest ( & m.hash) , serde_json:: to_vec( & manifest) ? ) ? ;
4cf0c99a Andy McCall 24 days ago ↶
227
}
b14aeaae Andy McCall 23 days ago ↶
228
Ok ( ( ) )
229
}
230
32f54b39 Andy McCall 9 days ago ↶
231
impl Client {
232
233
234
235
236
237
pub fn offer_held ( & self , home : & Home) -> Result< usize > {
238
let images = home.images ( ) ;
239
if images.is_empty ( ) {
240
return Ok ( 0 ) ;
241
}
242
let url = format! ( " {} /flow/images/held" , self .base) ;
243
let wanted: flow_core:: api:: WantedManifests =
244
match self .agent.post ( & url) .set ( " Authorization" , & self .auth ( ) ) .send_json ( flow_core:: api:: HeldImages { images } ) {
245
Ok ( r) => r.into_json ( ) ? ,
8b7034c5 Andy McCall 9 days ago ↶
246
247
248
Err ( ureq:: Error:: Status( 404 | 405 , _ ) ) => return Ok ( 0 ) ,
32f54b39 Andy McCall 9 days ago ↶
249
Err ( e) => return Err ( Self :: fail( & url, e) ) ,
250
} ;
251
let mut sent = 0 ;
252
for hash in wanted.wanted.iter ( ) .filter ( | h | images:: is_hash( h) ) {
253
let path = home.rootfs ( hash) ;
254
if ! path.is_file ( ) {
255
continue ;
256
}
257
let kept = std:: fs:: read( manifest_path ( & home.root, hash) ) .ok ( ) .and_then ( | b | serde_json:: from_slice:: < Manifest> ( & b) .ok ( ) ) ;
258
let m = match kept.filter ( | m | m.hash = = * hash ) {
259
Some ( m) => m,
260
None => {
261
let m = images:: scan_file( & path, home.image_arch ( hash) , | _, _| Ok ( ( ) ) ) ? ;
262
if m.hash ! = * hash {
263
continue ;
264
}
265
save_manifest ( & home.root, & m) ? ;
266
m
267
}
268
} ;
269
let url = format! ( " {} /flow/images/{hash} /held" , self .base) ;
270
self .agent.put ( & url) .set ( " Authorization" , & self .auth ( ) ) .send_json ( & m) .map_err ( | e | Self :: fail( & url, e) ) ? ;
271
sent + = 1 ;
272
}
273
Ok ( sent)
274
}
275
}
276
b14aeaae Andy McCall 23 days ago ↶
277
278
279
pub ( crate ) fn manifest_path ( root : & Path, hash : & str ) -> PathBuf {
280
root.join ( " manifests" ) .join ( format! ( " {hash} .json" ) )
281
}
282
283
pub ( crate ) fn save_manifest ( root : & Path, m : & Manifest) -> Result< ( ) > {
284
let p = manifest_path ( root, & m.hash) ;
285
std:: fs:: create_dir_all( p.parent ( ) .expect ( " the manifests directory" ) ) ? ;
286
let tmp = p.with_extension ( " part" ) ;
287
std:: fs:: write( & tmp, serde_json:: to_vec( m) ? ) ? ;
288
std:: fs:: rename( & tmp, & p) ? ;
289
Ok ( ( ) )
4cf0c99a Andy McCall 24 days ago ↶
290
}
291
292
fn send_batch ( batch : & [( String , Vec <u8 >) ], send : & ( dyn Fn (&str , &[u8 ]) -> Result< ( ) > + Sync) , failed: & Mutex< Option< anyhow:: Error> > ) {
293
std:: thread:: scope( | s | {
294
for part in batch.chunks ( batch.len ( ) .div_ceil ( PARALLEL ) .max ( 1 ) ) {
295
s.spawn ( move | | {
296
for ( h, b) in part {
297
if let Err ( e) = send ( h, b) {
298
* failed.lock ( ) .unwrap ( ) = Some ( e) ;
299
return ;
300
}
301
}
302
} ) ;
303
}
304
} ) ;
305
}
306
307
308
pub fn chunk_cache ( home : & Home) -> PathBuf {
309
home.root.join ( " chunks" )
310
}
311
b14aeaae Andy McCall 23 days ago ↶
312
pub ( crate ) fn cache_path ( cache : & Path, hash : & str ) -> PathBuf {
4cf0c99a Andy McCall 24 days ago ↶
313
cache.join ( & hash[ .. 2 ] ) .join ( hash)
314
}
315
316
317
pub fn human ( bytes : u64 ) -> String {
318
if bytes > = 1 < < 30 {
319
format! ( " {:.1} GiB" , bytes as f64 / ( 1 u64 < < 30 ) as f64 )
320
} else {
321
format! ( " {} MiB" , bytes > > 20 )
322
}
323
}