#!/usr/bin/env ruby require 'base64' require 'fileutils' require 'json' require 'net/http' require 'pathname' require 'tmpdir' require 'uri' SUPPORTED_EXTENSIONS = %w[.png .jpg .jpeg .webp].freeze def format_bytes(bytes) format('%.2f MB', bytes.to_f / 1024 / 1024) end def collect_files(target) if File.file?(target) return SUPPORTED_EXTENSIONS.include?(File.extname(target).downcase) ? [target] : [] end return [] unless Dir.exist?(target) Dir.glob(File.join(target, '**', '*')) .select { |path| File.file?(path) } .select { |path| SUPPORTED_EXTENSIONS.include?(File.extname(path).downcase) } end def build_http(uri) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http.read_timeout = 120 http.open_timeout = 30 http end def tinypng_shrink(api_key, file_path) uri = URI('https://api.tinify.com/shrink') request = Net::HTTP::Post.new(uri) request['Authorization'] = "Basic #{Base64.strict_encode64("api:#{api_key}")}" request['Content-Type'] = 'application/octet-stream' request.body = File.binread(file_path) response = build_http(uri).request(request) unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) message = begin body = JSON.parse(response.body) body['message'] || response.body rescue StandardError response.body end raise "Shrink failed (#{response.code}): #{message}" end JSON.parse(response.body) end def download_file(url, destination) uri = URI(url) request = Net::HTTP::Get.new(uri) response = build_http(uri).request(request) unless response.is_a?(Net::HTTPSuccess) raise "Download failed (#{response.code}): #{response.body}" end File.binwrite(destination, response.body) end def compress_file(api_key, file_path) original_size = File.size(file_path) shrink_result = tinypng_shrink(api_key, file_path) output = shrink_result.fetch('output') temp_path = File.join(Dir.tmpdir, "#{File.basename(file_path)}.tinypng_tmp") download_file(output.fetch('url'), temp_path) compressed_size = File.size(temp_path) FileUtils.mv(temp_path, file_path, force: true) { filePath: file_path, originalSize: original_size, compressedSize: compressed_size, savedBytes: original_size - compressed_size, ratio: output['ratio'] } ensure FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path) end api_key = ENV['TINYPNG_API_KEY'] abort('Missing TINYPNG_API_KEY environment variable') unless api_key && !api_key.empty? retry_failed = ARGV.delete('--retry-failed') targets = ARGV abort('Usage: tinypng_batch.rb