11require 'vips'
22require 'zlib'
33require 'stringio'
4+ require 'json'
45require 'concurrent-ruby'
56require_relative 'ext/terrain_downsample_extension'
67require_relative 'vips_tile_validator'
@@ -30,6 +31,7 @@ def initialize(route, source_name)
3031 @consecutive_403_count = 0
3132 @pending_403_tiles = [ ]
3233 @max_403_for_zoom = 1
34+ @json_mutex = Mutex . new
3335
3436 setup_progress_table
3537 load_todays_progress
@@ -193,6 +195,96 @@ def setup_progress_table
193195 end
194196 end
195197
198+ def progress_json_path
199+ mbtiles = @route [ :mbtiles_file ]
200+ return nil unless mbtiles
201+
202+ mbtiles . sub ( /\. mbtiles$/ , '.progress.json' )
203+ rescue
204+ nil
205+ end
206+
207+ def load_json_file
208+ path = progress_json_path
209+ return nil unless path && File . exist? ( path )
210+
211+ JSON . parse ( File . read ( path ) )
212+ rescue => e
213+ LOGGER . warn ( "Failed to read progress JSON: #{ e } " )
214+ nil
215+ end
216+
217+ def load_progress_from_json ( z )
218+ @json_mutex . synchronize do
219+ data = load_json_file
220+ return nil unless data
221+
222+ zoom = data . dig ( 'zooms' , z . to_s )
223+ return nil unless zoom
224+
225+ x , y = zoom [ 'last_x' ] , zoom [ 'last_y' ]
226+ return nil if x . nil? || y . nil?
227+
228+ @tiles_today = [ @tiles_today , zoom [ 'tiles_today' ] . to_i ] . max if zoom [ 'last_scan_date' ] == Date . today . to_s
229+ { x : x , y : y }
230+ end
231+ end
232+
233+ def load_todays_progress_from_json
234+ @json_mutex . synchronize do
235+ data = load_json_file
236+ return nil unless data
237+
238+ today = Date . today . to_s
239+ total = ( data [ 'zooms' ] || { } ) . values . sum do |zoom |
240+ zoom [ 'last_scan_date' ] == today ? zoom [ 'tiles_today' ] . to_i : 0
241+ end
242+ total
243+ end
244+ end
245+
246+ def save_progress_to_json ( x , y , z , status : nil )
247+ path = progress_json_path
248+ return unless path
249+
250+ @json_mutex . synchronize do
251+ data = load_json_file || { 'source' => @source_name , 'zooms' => { } }
252+ data [ 'zooms' ] [ z . to_s ] ||= { }
253+ data [ 'zooms' ] [ z . to_s ] . merge! (
254+ 'last_x' => x ,
255+ 'last_y' => y ,
256+ 'tiles_today' => @tiles_today ,
257+ 'last_scan_date' => Date . today . to_s
258+ )
259+ data [ 'zooms' ] [ z . to_s ] [ 'status' ] = status if status
260+ data [ 'updated_at' ] = Time . now . iso8601
261+
262+ tmp = "#{ path } .tmp"
263+ File . write ( tmp , JSON . generate ( data ) )
264+ File . rename ( tmp , path )
265+ end
266+ rescue => e
267+ LOGGER . warn ( "Failed to save progress to JSON: #{ e } " )
268+ end
269+
270+ def update_status_in_json ( z , status )
271+ path = progress_json_path
272+ return unless path
273+
274+ @json_mutex . synchronize do
275+ data = load_json_file || { 'source' => @source_name , 'zooms' => { } }
276+ data [ 'zooms' ] [ z . to_s ] ||= { }
277+ data [ 'zooms' ] [ z . to_s ] [ 'status' ] = status
278+ data [ 'updated_at' ] = Time . now . iso8601
279+
280+ tmp = "#{ path } .tmp"
281+ File . write ( tmp , JSON . generate ( data ) )
282+ File . rename ( tmp , path )
283+ end
284+ rescue => e
285+ LOGGER . warn ( "Failed to update status in JSON for zoom #{ z } : #{ e } " )
286+ end
287+
196288 def scan_zoom_level ( z , token )
197289 otl_span ( "scan_zoom_level" , { source : @source_name , zoom : z } ) do
198290 if zoom_complete? ( z )
@@ -481,15 +573,19 @@ def get_bounds_for_zoom(z)
481573 end
482574
483575 def load_progress ( z )
576+ json_data = load_progress_from_json ( z )
577+ return json_data if json_data
578+
484579 row = @route [ :db ] [ :tile_scan_progress ] . where ( source : @source_name , zoom_level : z ) . first
485580 return { x : 0 , y : 0 } unless row
486581
487582 @tiles_today = [ @tiles_today , row [ :tiles_today ] . to_i ] . max if row [ :last_scan_date ] == Date . today . to_s
488-
489583 { x : row [ :last_x ] || 0 , y : row [ :last_y ] || 0 }
490584 end
491585
492586 def save_progress ( x , y , z )
587+ save_progress_to_json ( x , y , z )
588+
493589 @route [ :db ] [ :tile_scan_progress ] . insert_conflict (
494590 target : [ :source , :zoom_level ] ,
495591 update : {
@@ -511,6 +607,12 @@ def save_progress(x, y, z)
511607 end
512608
513609 def load_todays_progress
610+ json_total = load_todays_progress_from_json
611+ if json_total
612+ @tiles_today = json_total
613+ return
614+ end
615+
514616 today = Date . today . to_s
515617 rows = @route [ :db ] [ :tile_scan_progress ]
516618 . where ( source : @source_name , last_scan_date : today )
@@ -533,6 +635,7 @@ def convert_to_webp(data)
533635
534636 def update_status ( zoom_level , status )
535637 @route [ :db ] [ :tile_scan_progress ] . where ( source : @source_name , zoom_level : zoom_level ) . update ( status : status )
638+ update_status_in_json ( zoom_level , status )
536639 rescue => e
537640 LOGGER . warn ( "Failed to update status for zoom #{ zoom_level } : #{ e } " )
538641 end
@@ -541,12 +644,40 @@ def update_all_statuses(status)
541644 @route [ :db ] [ :tile_scan_progress ]
542645 . where ( source : @source_name , status : 'active' )
543646 . update ( status : status )
647+
648+ path = progress_json_path
649+ if path
650+ @json_mutex . synchronize do
651+ data = load_json_file
652+ if data
653+ data [ 'zooms' ] . each_value { |z | z [ 'status' ] = status if z [ 'status' ] == 'active' }
654+ data [ 'updated_at' ] = Time . now . iso8601
655+ tmp = "#{ path } .tmp"
656+ File . write ( tmp , JSON . generate ( data ) )
657+ File . rename ( tmp , path )
658+ end
659+ end
660+ end
544661 rescue => e
545662 LOGGER . warn ( "Failed to update all statuses: #{ e } " )
546663 end
547664
548665 def update_active_status ( status )
549666 @route [ :db ] [ :tile_scan_progress ] . where ( source : @source_name , status : 'active' ) . update ( status : status )
667+
668+ path = progress_json_path
669+ if path
670+ @json_mutex . synchronize do
671+ data = load_json_file
672+ if data
673+ data [ 'zooms' ] . each_value { |z | z [ 'status' ] = status if z [ 'status' ] == 'active' }
674+ data [ 'updated_at' ] = Time . now . iso8601
675+ tmp = "#{ path } .tmp"
676+ File . write ( tmp , JSON . generate ( data ) )
677+ File . rename ( tmp , path )
678+ end
679+ end
680+ end
550681 rescue => e
551682 LOGGER . warn ( "Failed to update active status: #{ e } " )
552683 end
@@ -566,18 +697,26 @@ def initialize_zoom_progress
566697 LOGGER . info ( "Reset #{ existing [ :status ] } status for zoom #{ z } of #{ @source_name } on startup" )
567698 elsif existing && existing [ :status ] == 'source_unavailable'
568699 @route [ :db ] [ :tile_scan_progress ] . where ( source : @source_name , zoom_level : z ) . update ( status : 'stopped' )
700+ update_status_in_json ( z , 'stopped' )
569701 LOGGER . info ( "Reset source_unavailable status (keeping coordinates) for zoom #{ z } of #{ @source_name } on startup" )
570702 else
703+ json_zoom = @json_mutex . synchronize { load_json_file &.dig ( 'zooms' , z . to_s ) }
704+ saved_x = json_zoom &.dig ( 'last_x' ) || 0
705+ saved_y = json_zoom &.dig ( 'last_y' ) || 0
706+ saved_date = json_zoom &.dig ( 'last_scan_date' )
707+ saved_status = json_zoom &.dig ( 'status' ) || 'waiting'
708+ restore_status = [ 'completed' , 'stopped' ] . include? ( saved_status ) ? saved_status : 'waiting'
709+
571710 @route [ :db ] [ :tile_scan_progress ] . insert_conflict (
572711 target : [ :source , :zoom_level ]
573712 ) . insert (
574713 source : @source_name ,
575714 zoom_level : z ,
576- last_x : 0 ,
577- last_y : 0 ,
578- tiles_today : 0 ,
579- last_scan_date : nil ,
580- status : 'waiting'
715+ last_x : saved_x ,
716+ last_y : saved_y ,
717+ tiles_today : json_zoom &. dig ( 'tiles_today' ) || 0 ,
718+ last_scan_date : saved_date ,
719+ status : restore_status
581720 )
582721 end
583722 end
@@ -622,6 +761,23 @@ def reset_zoom_progress(z)
622761 last_y : 0 ,
623762 status : 'waiting'
624763 )
764+
765+ path = progress_json_path
766+ if path
767+ @json_mutex . synchronize do
768+ data = load_json_file || { 'source' => @source_name , 'zooms' => { } }
769+ data [ 'zooms' ] [ z . to_s ] = ( data [ 'zooms' ] [ z . to_s ] || { } ) . merge (
770+ 'last_x' => 0 ,
771+ 'last_y' => 0 ,
772+ 'status' => 'waiting'
773+ )
774+ data [ 'updated_at' ] = Time . now . iso8601
775+ tmp = "#{ path } .tmp"
776+ File . write ( tmp , JSON . generate ( data ) )
777+ File . rename ( tmp , path )
778+ end
779+ end
780+
625781 LOGGER . info ( "Reset progress for zoom #{ z } of #{ @source_name } " )
626782 rescue => e
627783 LOGGER . warn ( "Failed to reset progress for zoom #{ z } : #{ e } " )
0 commit comments