1+ use std:: collections:: HashMap ;
2+ use std:: fs;
13use std:: path:: PathBuf ;
24
35use appimageupdate:: config;
@@ -9,8 +11,8 @@ use clap::Parser;
911#[ command( about = "AppImage companion tool taking care of updates for the commandline." , long_about = None ) ]
1012#[ command( version) ]
1113struct Cli {
12- #[ arg( value_name = "APPIMAGE" ) ]
13- path : Option < PathBuf > ,
14+ #[ arg( value_name = "APPIMAGE" , num_args ( 1 .. ) ) ]
15+ paths : Vec < PathBuf > ,
1416
1517 #[ arg( short = 'O' , long) ]
1618 overwrite : bool ,
@@ -69,20 +71,129 @@ fn run(cli: Cli) -> Result<(), Error> {
6971 if !cli. github_api_proxy . is_empty ( ) {
7072 config:: set_proxies ( cli. github_api_proxy . clone ( ) ) ;
7173 }
72- let path = cli. path . ok_or_else ( || {
73- Error :: AppImage ( "No AppImage path provided. Use --help for usage." . into ( ) )
74- } ) ?;
75- let mut updater = if let Some ( ref update_info) = cli. update_info {
76- Updater :: with_update_info ( & path, update_info) ?
77- } else {
78- Updater :: new ( & path) ?
74+ if cli. paths . is_empty ( ) {
75+ return Err ( Error :: AppImage (
76+ "No AppImage path provided. Use --help for usage." . into ( ) ,
77+ ) ) ;
78+ }
79+ let appimages = collect_appimages ( & cli. paths ) ?;
80+ if appimages. is_empty ( ) {
81+ return Err ( Error :: AppImage ( "No AppImages found." . into ( ) ) ) ;
82+ }
83+
84+ let mut groups: HashMap < String , Vec < PathBuf > > = HashMap :: new ( ) ;
85+ let mut ungrouped: Vec < PathBuf > = Vec :: new ( ) ;
86+
87+ for path in & appimages {
88+ if let Ok ( updater) = create_updater ( & cli, path)
89+ && let Ok ( zsync_url) = updater. zsync_url ( )
90+ {
91+ groups. entry ( zsync_url) . or_default ( ) . push ( path. clone ( ) ) ;
92+ continue ;
93+ }
94+ ungrouped. push ( path. clone ( ) ) ;
95+ }
96+
97+ let mut errors = Vec :: new ( ) ;
98+ let mut updated_files: HashMap < String , PathBuf > = HashMap :: new ( ) ;
99+
100+ for ( zsync_url, paths) in & groups {
101+ if paths. len ( ) > 1 {
102+ println ! (
103+ "\n === Group ({} AppImages, same update source) ===" ,
104+ paths. len( )
105+ ) ;
106+ }
107+ for path in paths {
108+ if appimages. len ( ) > 1 {
109+ println ! ( "\n === {} ===" , path. display( ) ) ;
110+ }
111+ if let Err ( e) = handle_appimage ( & cli, path, zsync_url, & mut updated_files) {
112+ eprintln ! ( "Error updating {}: {}" , path. display( ) , e) ;
113+ errors. push ( path. clone ( ) ) ;
114+ }
115+ }
116+ }
117+
118+ for path in & ungrouped {
119+ println ! ( "\n === {} ===" , path. display( ) ) ;
120+ if let Err ( e) = handle_appimage ( & cli, path, "" , & mut updated_files) {
121+ eprintln ! ( "Error updating {}: {}" , path. display( ) , e) ;
122+ errors. push ( path. clone ( ) ) ;
123+ }
124+ }
125+
126+ if !errors. is_empty ( ) {
127+ eprintln ! ( "\n Failed to update {} AppImage(s)" , errors. len( ) ) ;
128+ std:: process:: exit ( 1 ) ;
129+ }
130+ Ok ( ( ) )
131+ }
132+
133+ fn collect_appimages ( paths : & [ PathBuf ] ) -> Result < Vec < PathBuf > , Error > {
134+ let mut appimages = Vec :: new ( ) ;
135+ for path in paths {
136+ if path. is_dir ( ) {
137+ for entry in fs:: read_dir ( path) ? {
138+ let entry = entry?;
139+ let entry_path = entry. path ( ) ;
140+ if entry_path. is_file ( ) && is_appimage ( & entry_path) {
141+ appimages. push ( entry_path) ;
142+ }
143+ }
144+ } else if path. is_file ( ) {
145+ appimages. push ( path. clone ( ) ) ;
146+ } else {
147+ return Err ( Error :: AppImage ( format ! (
148+ "Path does not exist: {}" ,
149+ path. display( )
150+ ) ) ) ;
151+ }
152+ }
153+ appimages. sort ( ) ;
154+ appimages. dedup ( ) ;
155+ Ok ( appimages)
156+ }
157+
158+ fn is_appimage ( path : & PathBuf ) -> bool {
159+ use std:: fs:: File ;
160+ use std:: io:: { Read , Seek , SeekFrom } ;
161+
162+ let Ok ( mut file) = File :: open ( path) else {
163+ return false ;
79164 } ;
80- if let Some ( output_dir) = config:: get_output_dir ( cli. output_dir ) {
165+ let mut magic = [ 0u8 ; 3 ] ;
166+ if file. seek ( SeekFrom :: Start ( 8 ) ) . is_err ( ) {
167+ return false ;
168+ }
169+ if file. read_exact ( & mut magic) . is_err ( ) {
170+ return false ;
171+ }
172+ & magic[ 0 ..2 ] == b"AI" && ( magic[ 2 ] == 1 || magic[ 2 ] == 2 )
173+ }
174+
175+ fn create_updater ( cli : & Cli , path : & PathBuf ) -> Result < Updater , Error > {
176+ if let Some ( ref update_info) = cli. update_info {
177+ Updater :: with_update_info ( path, update_info)
178+ } else {
179+ Updater :: new ( path)
180+ }
181+ }
182+
183+ fn handle_appimage (
184+ cli : & Cli ,
185+ path : & PathBuf ,
186+ zsync_url : & str ,
187+ updated_files : & mut HashMap < String , PathBuf > ,
188+ ) -> Result < ( ) , Error > {
189+ let mut updater = create_updater ( cli, path) ?;
190+ if let Some ( output_dir) = config:: get_output_dir ( cli. output_dir . clone ( ) ) {
81191 updater = updater. output_dir ( & output_dir) ;
82192 }
83193 if cli. overwrite {
84194 updater = updater. overwrite ( true ) ;
85195 }
196+
86197 if cli. describe {
87198 let source_path = updater. source_path ( ) ;
88199 let source_size = updater. source_size ( ) ;
@@ -94,6 +205,7 @@ fn run(cli: Cli) -> Result<(), Error> {
94205 println ! ( "Update Info: {}" , updater. update_info( ) ) ;
95206 return Ok ( ( ) ) ;
96207 }
208+
97209 if cli. check_for_update {
98210 let has_update = updater. check_for_update ( ) ?;
99211 if has_update {
@@ -103,6 +215,7 @@ fn run(cli: Cli) -> Result<(), Error> {
103215 }
104216 std:: process:: exit ( if has_update { 1 } else { 0 } ) ;
105217 }
218+
106219 let source_path = updater. source_path ( ) . to_path_buf ( ) ;
107220 let source_size = updater. source_size ( ) ;
108221 let ( target_path, target_size) = updater. target_info ( ) ?;
@@ -117,41 +230,71 @@ fn run(cli: Cli) -> Result<(), Error> {
117230 format_size( target_size)
118231 ) ;
119232 println ! ( ) ;
120- if updater. check_for_update ( ) ? {
121- println ! ( "Performing delta update..." ) ;
122- let ( new_path, stats) = updater. perform_update ( ) ?;
123- if stats. blocks_reused > 0 || stats. blocks_downloaded > 0 {
124- println ! ( ) ;
125- println ! (
126- "Reused: {:>10} ({} blocks)" ,
127- format_size( stats. bytes_reused( ) ) ,
128- stats. blocks_reused
129- ) ;
130- println ! (
131- "Downloaded: {:>10} ({} blocks)" ,
132- format_size( stats. bytes_downloaded( ) ) ,
133- stats. blocks_downloaded
134- ) ;
135- println ! (
136- "Saved: {:>10} ({}%)" ,
137- format_size( stats. bytes_reused( ) ) ,
138- stats. saved_percentage( )
139- ) ;
140- }
141- println ! ( ) ;
142- println ! ( "Updated: {}" , new_path. display( ) ) ;
143- let remove_old = config:: get_remove_old ( if cli. remove_old { Some ( true ) } else { None } ) ;
144- if remove_old {
145- if let Some ( ref backup) = stats. backup_path {
146- std:: fs:: remove_file ( backup) ?;
147- println ! ( "Removed old AppImage" ) ;
148- } else if new_path != source_path {
149- std:: fs:: remove_file ( source_path) ?;
233+
234+ if !updater. check_for_update ( ) ? {
235+ println ! ( "Already up to date!" ) ;
236+ return Ok ( ( ) ) ;
237+ }
238+
239+ if let Some ( existing) = updated_files
240+ . get ( zsync_url)
241+ . filter ( |_| !zsync_url. is_empty ( ) )
242+ {
243+ if existing == & target_path {
244+ println ! ( "Already updated (same target)" ) ;
245+ } else {
246+ println ! ( "Copying from {}..." , existing. display( ) ) ;
247+ let perms = fs:: metadata ( & source_path) . ok ( ) . map ( |m| m. permissions ( ) ) ;
248+ fs:: copy ( existing, & target_path) ?;
249+ if let Some ( perms) = perms {
250+ fs:: set_permissions ( & target_path, perms) ?;
251+ }
252+ println ! ( "Updated: {}" , target_path. display( ) ) ;
253+ let remove_old = config:: get_remove_old ( if cli. remove_old { Some ( true ) } else { None } ) ;
254+ if remove_old && target_path != source_path {
255+ fs:: remove_file ( & source_path) ?;
150256 println ! ( "Removed old AppImage" ) ;
151257 }
152258 }
153- } else {
154- println ! ( "Already up to date!" ) ;
259+ return Ok ( ( ) ) ;
260+ }
261+
262+ println ! ( "Performing delta update..." ) ;
263+ let ( new_path, stats) = updater. perform_update ( ) ?;
264+ if stats. blocks_reused > 0 || stats. blocks_downloaded > 0 {
265+ println ! ( ) ;
266+ println ! (
267+ "Reused: {:>10} ({} blocks)" ,
268+ format_size( stats. bytes_reused( ) ) ,
269+ stats. blocks_reused
270+ ) ;
271+ println ! (
272+ "Downloaded: {:>10} ({} blocks)" ,
273+ format_size( stats. bytes_downloaded( ) ) ,
274+ stats. blocks_downloaded
275+ ) ;
276+ println ! (
277+ "Saved: {:>10} ({}%)" ,
278+ format_size( stats. bytes_reused( ) ) ,
279+ stats. saved_percentage( )
280+ ) ;
281+ }
282+ println ! ( ) ;
283+ println ! ( "Updated: {}" , new_path. display( ) ) ;
284+
285+ if !zsync_url. is_empty ( ) {
286+ updated_files. insert ( zsync_url. to_string ( ) , new_path. clone ( ) ) ;
287+ }
288+
289+ let remove_old = config:: get_remove_old ( if cli. remove_old { Some ( true ) } else { None } ) ;
290+ if remove_old {
291+ if let Some ( ref backup) = stats. backup_path {
292+ fs:: remove_file ( backup) ?;
293+ println ! ( "Removed old AppImage" ) ;
294+ } else if new_path != source_path {
295+ fs:: remove_file ( & source_path) ?;
296+ println ! ( "Removed old AppImage" ) ;
297+ }
155298 }
156299 Ok ( ( ) )
157300}
0 commit comments