From 7950acb21b22e7bc6f10c50e1427850de2834b24 Mon Sep 17 00:00:00 2001 From: Ken D'Ambrosio Date: Wed, 13 May 2026 17:46:28 +0000 Subject: Add folder rename and background update trigger to admin UI - Admin edit form: "Folder name" field renames the directory on save; also moves the thumbnail cache subtree to match the new path - Admin edit page: "Run Update" button spawns update.rb in a background thread, streams output into a terminal-style log panel via 1.5s polling; shows Done/Error status when complete Co-Authored-By: Claude Sonnet 4.6 --- app.rb | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ public/css/style.css | 13 ++++++++++ views/admin/album.erb | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/app.rb b/app.rb index 5aba1b4..0678c67 100644 --- a/app.rb +++ b/app.rb @@ -15,6 +15,9 @@ CACHE_ROOT = (ENV['CACHE_ROOT'] || '/opt/albumen/cache/thumbs').freeze CONFIG_PATH = (ENV['CONFIG_PATH'] || '/opt/albumen/config.yml').freeze THUMB_SIZE = 300 +UPDATE_JOBS = {} +UPDATE_JOBS_MUTEX = Mutex.new + IMAGE_EXTS = %w[jpg jpeg png gif webp heic heif tiff bmp].freeze VIDEO_EXTS = %w[mp4 mov avi mkv webm m4v ogv].freeze AUDIO_EXTS = %w[mp3 flac ogg wav m4a aac].freeze @@ -409,6 +412,28 @@ post '/admin/edit/*' do require_admin! rel = params[:splat].first.chomp('/') dir = resolve_dir(rel) + + unless rel.empty? + new_name = params['folder_name'].to_s.strip + new_name = '' if new_name.include?('/') || new_name.include?("\x00") || + new_name == '.' || new_name == '..' + old_name = File.basename(dir) + if !new_name.empty? && new_name != old_name + parent_dir = File.dirname(dir) + new_dir = File.join(parent_dir, new_name) + unless File.exist?(new_dir) + parent_rel = rel.include?('/') ? rel.split('/')[0..-2].join('/') : '' + new_rel = parent_rel.empty? ? new_name : "#{parent_rel}/#{new_name}" + old_cache = File.join(CACHE_ROOT, rel) + new_cache = File.join(CACHE_ROOT, new_rel) + FileUtils.mv(old_cache, new_cache) if File.directory?(old_cache) + FileUtils.mv(dir, new_dir) + save_edits(new_rel, new_dir) + redirect "/admin/edit/#{new_rel}" + end + end + end + save_edits(rel, dir) redirect "/admin/edit/#{rel}" end @@ -435,6 +460,48 @@ def save_edits(rel, dir) atomic_write(File.join(dir, 'album.json'), JSON.pretty_generate(data)) end +# ── Background update ───────────────────────────────────────────────────────── + +post '/admin/update' do + require_admin! + rel = params[:rel].to_s.chomp('/') + job_id = SecureRandom.hex(8) + script = File.join(__dir__, 'scripts', 'update.rb') + cmd = rel.empty? ? ['ruby', script] : ['ruby', script, rel] + + UPDATE_JOBS_MUTEX.synchronize do + UPDATE_JOBS[job_id] = { status: :running, lines: [] } + end + + Thread.new do + begin + IO.popen(cmd, err: [:child, :out]) do |io| + io.each_line do |line| + UPDATE_JOBS_MUTEX.synchronize { UPDATE_JOBS[job_id][:lines] << line.chomp } + end + end + code = $?.exitstatus + UPDATE_JOBS_MUTEX.synchronize { UPDATE_JOBS[job_id][:status] = code == 0 ? :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 }.to_json +end + +get '/admin/update/:id' do + require_admin! + job = UPDATE_JOBS_MUTEX.synchronize { UPDATE_JOBS[params[:id]]&.dup } + halt 404 unless job + content_type :json + { status: job[:status], lines: job[:lines] }.to_json +end + # ── Thumbnail generation ─────────────────────────────────────────────────────── def generate_thumb(source, dest, ext) diff --git a/public/css/style.css b/public/css/style.css index b5d713e..e6ebff3 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -378,3 +378,16 @@ legend { padding: 0 8px; color: var(--text-dim); font-size: .85rem; } .lb-stage { padding: 48px 40px; } .files-table { font-size: .78rem; } } + +/* ── Admin update panel ────────────────────────────────────────────────── */ +.admin-update { margin-top: 32px; } +.admin-update h2 { font-size: 1rem; color: var(--text-dim); margin-bottom: 6px; } +.update-hint { font-size: .85rem; color: var(--text-dim); margin: 0 0 12px; } +.update-panel { margin-top: 12px; } +.update-status { font-size: .9rem; font-weight: 500; margin-bottom: 6px; } +.update-status.running { color: var(--text-dim); } +.update-status.done { color: #2a9d2a; } +.update-status.error { color: #c0392b; } +.update-log { background: #1a1a1a; color: #e0e0e0; font-size: .8rem; line-height: 1.5; + padding: 12px 14px; border-radius: var(--radius); max-height: 340px; + overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin: 0; } diff --git a/views/admin/album.erb b/views/admin/album.erb index 9d98c5e..49ec4ca 100644 --- a/views/admin/album.erb +++ b/views/admin/album.erb @@ -14,6 +14,13 @@
Album + <% unless @rel.empty? %> +
+ +
+ <% end %>
-- cgit v1.2.3