module Recordings class CleanupLocal ACTIVE_SESSION_STATUSES = %w[live connecting reconnecting paused].freeze REMOVABLE_RECORDING_STATUSES = %w[ready failed expired].freeze Result = Struct.new(:removed, :freed_bytes, :skipped, keyword_init: true) def initialize(online_paths: nil, logger: Rails.logger) @online_paths = online_paths @logger = logger @removed = 0 @freed_bytes = 0 @skipped = 0 end def call online = @online_paths || Mediamtx::Client.new.online_path_names each_live_match_dir do |dir_path, session_id, path_name| if online.include?(path_name) @skipped += 1 next end session = StreamSession.find_by(id: session_id) unless removable?(session) @skipped += 1 next end remove_dir(dir_path) cleanup_mediamtx_path(session) if session end Result.new(removed: @removed, freed_bytes: @freed_bytes, skipped: @skipped) end private def each_live_match_dir base = File.join(MatchLiveTv.recordings_local_path, "live") return unless Dir.exist?(base) Dir.children(base).sort.each do |entry| next unless entry.start_with?("match_") session_id = entry.delete_prefix("match_") path_name = "live/#{entry}" dir_path = File.join(base, entry) next unless File.directory?(dir_path) yield dir_path, session_id, path_name end end def removable?(session) return true if session.nil? return false if ACTIVE_SESSION_STATUSES.include?(session.status) recording = Recording.find_by(stream_session: session) return true if recording&.status.in?(REMOVABLE_RECORDING_STATUSES) return false unless session.status.in?(%w[ended error]) ended_at = session.ended_at || session.updated_at grace = recording&.status == "processing" ? processing_grace : ended_grace ended_at < grace.ago end def processing_grace ENV.fetch("RECORDINGS_LOCAL_PROCESSING_GRACE_HOURS", "6").to_i.hours end def ended_grace ENV.fetch("RECORDINGS_LOCAL_ENDED_GRACE_HOURS", "2").to_i.hours end def remove_dir(path) size = dir_size(path) FileUtils.rm_rf(path) @removed += 1 @freed_bytes += size @logger.info("[Recordings::CleanupLocal] removed #{path} (#{size} bytes)") rescue StandardError => e @logger.warn("[Recordings::CleanupLocal] failed #{path}: #{e.message}") end def dir_size(path) `du -sb #{Shellwords.escape(path)} 2>/dev/null`.split.first.to_i rescue StandardError 0 end def cleanup_mediamtx_path(session) return unless session.status.in?(%w[ended error]) Mediamtx::Client.new.delete_path(session) rescue Mediamtx::Client::Error => e @logger.warn("[Recordings::CleanupLocal] delete_path #{session.id}: #{e.message}") end end end