From d32b5e99afc6f0cffefa594510cda0e4f414db75 Mon Sep 17 00:00:00 2001 From: Ken D'Ambrosio Date: Fri, 22 May 2026 22:50:35 +0000 Subject: Speed up update.rb and fix UI always forcing full rescan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - update.rb: skip exiftool on images marked exif_absent (set after first failed attempt); prevents repeated slow scans of old photos with no EXIF - update.rb: explicit directory argument now implies force — passing a path always rescans that subtree regardless of sentinel mtime - app.rb: /admin/update no longer hardcodes --force; sentinel-based skipping is used by default, making UI updates finish in seconds instead of minutes - admin/album.erb: add "Force rescan all" checkbox to Run Update button; checked state passes force=1 to the server and restores --force behavior - README.md, DESIGN.md: document sentinel skipping, exif_absent flag, and explicit-directory force behavior Co-Authored-By: Claude Sonnet 4.6 --- app.rb | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) (limited to 'app.rb') diff --git a/app.rb b/app.rb index ff7740c..6b11d5d 100644 --- a/app.rb +++ b/app.rb @@ -37,6 +37,7 @@ configure do set :bind, '127.0.0.1' set :port, 4567 set :logging, true + Rack::Utils.multipart_part_limit = 2000 # default 128; allow bulk photo uploads end configure :production do @@ -478,9 +479,13 @@ end post '/admin/update' do require_admin! rel = params[:rel].to_s.chomp('/') + force = params[:force].to_s == '1' job_id = SecureRandom.hex(8) script = File.join(__dir__, 'scripts', 'update.rb') - cmd = rel.empty? ? ['ruby', script] : ['ruby', script, rel] + args = [] + args << '--force' if force + args << rel unless rel.empty? + cmd = ['ruby', script, *args] UPDATE_JOBS_MUTEX.synchronize do UPDATE_JOBS[job_id] = { status: :running, lines: [] } @@ -515,6 +520,71 @@ get '/admin/update/:id' do { status: job[:status], lines: job[:lines] }.to_json end +post '/admin/upload' do + require_admin! + + rel = params['rel'].to_s.chomp('/') + + sub_name = params['new_album_name'].to_s.strip + sub_name = '' if sub_name.match?(%r{[/\x00]}) || %w[. ..].include?(sub_name) + + target_rel = if !sub_name.empty? + rel.empty? ? sub_name : "#{rel}/#{sub_name}" + else + rel + end + + target_dir = if target_rel.empty? + MEDIA_ROOT + else + full = File.expand_path(target_rel, MEDIA_ROOT) + halt 400, 'Invalid path' unless full.start_with?("#{MEDIA_ROOT}/") + full + end + + FileUtils.mkdir_p(target_dir) + + files = params['files[]'] || params['files'] + files = [files] unless files.is_a?(Array) + files = files.compact + + saved = 0 + files.each do |f| + next unless f.is_a?(Hash) && f[:filename].to_s.strip != '' + name = File.basename(f[:filename].to_s.encode('UTF-8', invalid: :replace, undef: :replace).gsub("\x00", '')) + next if name.empty? + ext = File.extname(name).downcase.delete_prefix('.') + next unless MEDIA_EXTS.include?(ext) + dest = File.join(target_dir, name) + FileUtils.cp(f[:tempfile].path, dest) + saved += 1 + end + + job_id = SecureRandom.hex(8) + script = File.join(__dir__, 'scripts', 'update.rb') + cmd = target_rel.empty? ? ['ruby', script] : ['ruby', script, target_rel] + + UPDATE_JOBS_MUTEX.synchronize { UPDATE_JOBS[job_id] = { status: :running, lines: [] } } + + Thread.new do + begin + IO.popen(cmd, err: [:child, :out]) do |io| + io.each_line { |line| UPDATE_JOBS_MUTEX.synchronize { UPDATE_JOBS[job_id][:lines] << line.chomp } } + end + code = $?.exitstatus + UPDATE_JOBS_MUTEX.synchronize { UPDATE_JOBS[job_id][:status] = code.zero? ? :done : :error } + rescue => e + UPDATE_JOBS_MUTEX.synchronize do + UPDATE_JOBS[job_id][:status] = :error + UPDATE_JOBS[job_id][:lines] << "Error: #{e.message}" + end + end + end + + content_type :json + { job_id: job_id, saved: saved, album_rel: target_rel }.to_json +end + # ── Thumbnail generation ─────────────────────────────────────────────────────── def generate_thumb(source, dest, ext) -- cgit v1.2.3