| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  | # typed: false | 
					
						
							|  |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "digest/md5" | 
					
						
							|  |  |  | require "utils/curl" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  | # The Internet Archive API client. | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  | # | 
					
						
							|  |  |  | # @api private | 
					
						
							|  |  |  | class Archive | 
					
						
							|  |  |  |   extend T::Sig | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   include Context | 
					
						
							|  |  |  |   include Utils::Curl | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   class Error < RuntimeError | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-10 16:08:45 +00:00
										 |  |  |   URL_PREFIX = "https://archive.org" | 
					
						
							|  |  |  |   S3_DOMAIN = "s3.us.archive.org" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |   sig { returns(String) } | 
					
						
							|  |  |  |   def inspect | 
					
						
							|  |  |  |     "#<Archive: item=#{@archive_item}>" | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { params(item: T.nilable(String)).void } | 
					
						
							|  |  |  |   def initialize(item: "homebrew") | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  |     raise UsageError, "Must set the Archive item!" unless item | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  |     @archive_item = item | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def open_api(url, *args, auth: true) | 
					
						
							|  |  |  |     if auth | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  |       key = Homebrew::EnvConfig.internet_archive_key | 
					
						
							|  |  |  |       raise UsageError, "HOMEBREW_INTERNET_ARCHIVE_KEY is unset." if key.blank? | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       if key.exclude?(":") | 
					
						
							| 
									
										
										
										
											2021-03-10 16:08:45 +00:00
										 |  |  |         raise UsageError, "Use HOMEBREW_INTERNET_ARCHIVE_KEY=access:secret. See #{URL_PREFIX}/account/s3.php" | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       args += ["--header", "Authorization: AWS #{key}"] | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     curl(*args, url, print_stdout: false, secrets: key) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { | 
					
						
							|  |  |  |     params(local_file:    String, | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |            directory:     String, | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |            remote_file:   String, | 
					
						
							|  |  |  |            warn_on_error: T.nilable(T::Boolean)).void | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |   def upload(local_file, directory:, remote_file:, warn_on_error: false) | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  |     local_file = Pathname.new(local_file) | 
					
						
							|  |  |  |     unless local_file.exist? | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |       msg = "#{local_file} for upload doesn't exist!" | 
					
						
							|  |  |  |       raise Error, msg unless warn_on_error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Warn and return early here since we know this upload is going to fail. | 
					
						
							|  |  |  |       opoo msg | 
					
						
							|  |  |  |       return | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  |     md5_base64 = Digest::MD5.base64digest(local_file.read) | 
					
						
							| 
									
										
										
										
											2021-03-10 16:08:45 +00:00
										 |  |  |     url = "https://#{@archive_item}.#{S3_DOMAIN}/#{directory}/#{remote_file}" | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |     args = ["--upload-file", local_file, "--header", "Content-MD5: #{md5_base64}"] | 
					
						
							|  |  |  |     args << "--fail" unless warn_on_error | 
					
						
							|  |  |  |     result = T.unsafe(self).open_api(url, *args) | 
					
						
							|  |  |  |     return if result.success? && result.stdout.exclude?("Error") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     msg = "Bottle upload failed: #{result.stdout}" | 
					
						
							|  |  |  |     raise msg unless warn_on_error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     opoo msg | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { | 
					
						
							|  |  |  |     params(formula:       Formula, | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |            directory:     String, | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |            warn_on_error: T::Boolean).returns(String) | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |   def mirror_formula(formula, directory: "mirror", warn_on_error: false) | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |     formula.downloader.fetch | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     filename = ERB::Util.url_encode(formula.downloader.basename) | 
					
						
							| 
									
										
										
										
											2021-03-10 16:08:45 +00:00
										 |  |  |     destination_url = "#{URL_PREFIX}/download/#{@archive_item}/#{directory}/#{filename}" | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     odebug "Uploading to #{destination_url}" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     upload( | 
					
						
							|  |  |  |       formula.downloader.cached_location, | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |       directory:     directory, | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |       remote_file:   filename, | 
					
						
							|  |  |  |       warn_on_error: warn_on_error, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     destination_url | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Gets the MD5 hash of the specified remote file. | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # @return the hash, the empty string (if the file doesn't have a hash), nil (if the file doesn't exist) | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |   sig { params(directory: String, remote_file: String).returns(T.nilable(String)) } | 
					
						
							|  |  |  |   def remote_md5(directory:, remote_file:) | 
					
						
							| 
									
										
										
										
											2021-03-10 16:08:45 +00:00
										 |  |  |     url = "https://#{@archive_item}.#{S3_DOMAIN}/#{directory}/#{remote_file}" | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |     result = curl_output "--fail", "--silent", "--head", "--location", url | 
					
						
							|  |  |  |     if result.success? | 
					
						
							|  |  |  |       result.stdout.match(/^ETag: "(\h{32})"/)&.values_at(1)&.first || "" | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       raise Error if result.status.exitstatus != 22 && result.stderr.exclude?("404 Not Found") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       nil | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { params(directory: String, filename: String).returns(String) } | 
					
						
							|  |  |  |   def file_delete_instructions(directory, filename) | 
					
						
							|  |  |  |     <<~EOS | 
					
						
							|  |  |  |       Run: | 
					
						
							| 
									
										
										
										
											2021-03-10 16:08:45 +00:00
										 |  |  |         curl -X DELETE -H "Authorization: AWS $HOMEBREW_INTERNET_ARCHIVE_KEY" https://#{@archive_item}.#{S3_DOMAIN}/#{directory}/#{filename} | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |       Or run: | 
					
						
							|  |  |  |         ia delete #{@archive_item} #{directory}/#{filename} | 
					
						
							|  |  |  |     EOS | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { | 
					
						
							|  |  |  |     params(bottles_hash:  T::Hash[String, T.untyped], | 
					
						
							|  |  |  |            warn_on_error: T.nilable(T::Boolean)).void | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   def upload_bottles(bottles_hash, warn_on_error: false) | 
					
						
							|  |  |  |     bottles_hash.each do |_formula_name, bottle_hash| | 
					
						
							|  |  |  |       directory = bottle_hash["bintray"]["repository"] | 
					
						
							|  |  |  |       bottle_count = bottle_hash["bottle"]["tags"].length | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-23 11:22:55 -08:00
										 |  |  |       bottle_hash["bottle"]["tags"].each_value do |tag_hash| | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |         filename = tag_hash["filename"] # URL encoded in Bottle::Filename#archive | 
					
						
							|  |  |  |         delete_instructions = file_delete_instructions(directory, filename) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         local_filename = tag_hash["local_filename"] | 
					
						
							|  |  |  |         md5 = Digest::MD5.hexdigest(File.read(local_filename)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         odebug "Checking remote file #{@archive_item}/#{directory}/#{filename}" | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |         result = remote_md5(directory: directory, remote_file: filename) | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |         case result | 
					
						
							|  |  |  |         when nil | 
					
						
							|  |  |  |           # File doesn't exist. | 
					
						
							|  |  |  |           odebug "Uploading #{@archive_item}/#{directory}/#{filename}" | 
					
						
							|  |  |  |           upload(local_filename, | 
					
						
							| 
									
										
										
										
											2021-02-22 18:06:44 -08:00
										 |  |  |                  directory:     directory, | 
					
						
							| 
									
										
										
										
											2021-02-21 22:12:17 -08:00
										 |  |  |                  remote_file:   filename, | 
					
						
							|  |  |  |                  warn_on_error: warn_on_error) | 
					
						
							|  |  |  |         when md5 | 
					
						
							|  |  |  |           # File exists, hash matches. | 
					
						
							|  |  |  |           odebug "#{filename} is already published with matching hash." | 
					
						
							|  |  |  |           bottle_count -= 1
 | 
					
						
							|  |  |  |         when "" | 
					
						
							|  |  |  |           # File exists, but can't find hash | 
					
						
							|  |  |  |           failed_message = "#{filename} is already published!" | 
					
						
							|  |  |  |           raise Error, "#{failed_message}\n#{delete_instructions}" unless warn_on_error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           opoo failed_message | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           # File exists, but hash either doesn't exist or is mismatched. | 
					
						
							|  |  |  |           failed_message = <<~EOS | 
					
						
							|  |  |  |             #{filename} is already published with a mismatched hash! | 
					
						
							|  |  |  |               Expected: #{md5} | 
					
						
							|  |  |  |               Actual:   #{result} | 
					
						
							|  |  |  |           EOS | 
					
						
							|  |  |  |           raise Error, "#{failed_message}#{delete_instructions}" unless warn_on_error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           opoo failed_message | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       odebug "Uploaded #{bottle_count} bottles" | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |