Class: Quake::PakDownloader
- Inherits:
-
Object
- Object
- Quake::PakDownloader
- Defined in:
- lib/quake/pak_downloader.rb
Overview
Downloads the shareware Quake pak0.pak if not present. The Quake shareware (episode 1) is freely redistributable; we fetch the archive from archive.org and extract pak0.pak via the system ‘unzip`.
Defined Under Namespace
Classes: DownloadError
Constant Summary collapse
- SHAREWARE_URL =
"https://archive.org/download/quakeshareware/QUAKE_SW.zip"- SHAREWARE_ZIP_SIZE =
approximate, for progress fallback
18_079_976- PAK_FILENAME =
"pak0.pak"
Class Method Summary collapse
- .download(url, dest_path) ⇒ Object
- .download_shareware(destination_data_dir) ⇒ Object
-
.ensure_pak_available(custom_path = nil) ⇒ Object
Returns a path to a usable PAK directory (containing id1/pak0.pak), or nil/raises.
-
.extract_pak(zip_path, destination_data_dir) ⇒ Object
Quake’s shareware ZIP contains an installer, with pak0.pak nested inside ‘resource/Q!ZIP/ID1/PAK0.PAK`.
- .fetch_to_file(url, dest_path, redirect_limit: 5) ⇒ Object
- .print_progress(downloaded, total) ⇒ Object
- .prompt_and_download(destination_data_dir) ⇒ Object
Class Method Details
.download(url, dest_path) ⇒ Object
66 67 68 69 70 71 72 73 74 |
# File 'lib/quake/pak_downloader.rb', line 66 def self.download(url, dest_path) puts puts "Downloading shareware Quake from #{URI.parse(url).host}..." FileUtils.mkdir_p(File.dirname(dest_path)) fetch_to_file(url, dest_path) puts size = File.size(dest_path) raise DownloadError, "Download appears incomplete (#{size} bytes)" if size < 1_000_000 end |
.download_shareware(destination_data_dir) ⇒ Object
58 59 60 61 62 63 64 |
# File 'lib/quake/pak_downloader.rb', line 58 def self.download_shareware(destination_data_dir) Dir.mktmpdir("quake-rb-shareware") do |tmp| zip_path = File.join(tmp, "quake106.zip") download(SHAREWARE_URL, zip_path) extract_pak(zip_path, destination_data_dir) end end |
.ensure_pak_available(custom_path = nil) ⇒ Object
Returns a path to a usable PAK directory (containing id1/pak0.pak), or nil/raises. Search order: ‘custom_path` -> `./data` -> `~/.quake/data`. If nothing is found, prompts the user to download.
22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/quake/pak_downloader.rb', line 22 def self.ensure_pak_available(custom_path = nil) return custom_path if custom_path && File.exist?(File.join(custom_path, "id1", PAK_FILENAME)) local = File.join(Dir.pwd, "data") return local if File.exist?(File.join(local, "id1", PAK_FILENAME)) home = File.join(Dir.home, ".quake", "data") return home if File.exist?(File.join(home, "id1", PAK_FILENAME)) raise DownloadError, "PAK not found at #{custom_path}" if custom_path prompt_and_download(home) end |
.extract_pak(zip_path, destination_data_dir) ⇒ Object
Quake’s shareware ZIP contains an installer, with pak0.pak nested inside ‘resource/Q!ZIP/ID1/PAK0.PAK`. Extract just that one entry into `<destination_data_dir>/id1/pak0.pak`. Uses system `unzip` so we don’t need a runtime dep on rubyzip.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/quake/pak_downloader.rb', line 110 def self.extract_pak(zip_path, destination_data_dir) target_dir = File.join(destination_data_dir, "id1") FileUtils.mkdir_p(target_dir) target_pak = File.join(target_dir, PAK_FILENAME) puts "Extracting pak0.pak..." # -j junks paths so the entry lands as just "PAK0.PAK" # -C makes glob matching case-insensitive Dir.mktmpdir("quake-rb-unzip") do |tmp| ok = system("unzip", "-jo", "-C", zip_path, "*PAK0.PAK", "-d", tmp, out: File::NULL) raise DownloadError, "unzip failed (is `unzip` installed?)" unless ok extracted = Dir[File.join(tmp, "*")].find { |p| File.basename(p).match?(/pak0\.pak/i) } raise DownloadError, "pak0.pak not found inside ZIP" unless extracted FileUtils.mv(extracted, target_pak) end raise DownloadError, "extracted pak0.pak is suspiciously small" if File.size(target_pak) < 1_000_000 puts "Installed: #{target_pak} (#{(File.size(target_pak) / 1_048_576.0).round(1)} MB)" puts end |
.fetch_to_file(url, dest_path, redirect_limit: 5) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/quake/pak_downloader.rb', line 76 def self.fetch_to_file(url, dest_path, redirect_limit: 5) raise DownloadError, "Too many redirects" if redirect_limit <= 0 uri = URI.parse(url) ssl_opts = { use_ssl: uri.scheme == "https" } Net::HTTP.start(uri.host, uri.port, **ssl_opts) do |http| request = Net::HTTP::Get.new(uri) http.request(request) do |response| case response when Net::HTTPRedirection return fetch_to_file(response["location"], dest_path, redirect_limit: redirect_limit - 1) when Net::HTTPSuccess total = response["content-length"]&.to_i || SHAREWARE_ZIP_SIZE downloaded = 0 File.open(dest_path, "wb") do |f| response.read_body do |chunk| f.write(chunk) downloaded += chunk.size print_progress(downloaded, total) end end else raise DownloadError, "HTTP #{response.code} #{response.}" end end end end |
.print_progress(downloaded, total) ⇒ Object
135 136 137 138 139 140 141 142 143 |
# File 'lib/quake/pak_downloader.rb', line 135 def self.print_progress(downloaded, total) percent = (downloaded.to_f / total * 100).clamp(0, 100).to_i = 40 filled = percent * / 100 = ("=" * filled) + ("-" * ( - filled)) mb_d = (downloaded / 1_048_576.0).round(1) mb_t = (total / 1_048_576.0).round(1) print "\r[#{}] #{percent}% (#{mb_d}/#{mb_t} MB)" end |
.prompt_and_download(destination_data_dir) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/quake/pak_downloader.rb', line 36 def self.prompt_and_download(destination_data_dir) puts "No Quake pak0.pak found." puts puts "Download the shareware version of Quake (about 9 MB)?" puts "It includes the first episode and is freely redistributable." puts print "Download shareware Quake? [Y/n] " response = $stdin.gets&.strip&.downcase if response.nil? || response.empty? || response == "y" || response == "yes" download_shareware(destination_data_dir) destination_data_dir else puts puts "To play Quake you need a pak0.pak. Options:" puts " 1. Run `quake-rb` again and accept the shareware download" puts " 2. Copy your own pak0.pak into ./data/id1/" puts " 3. Pass a custom path: quake-rb -basedir /path/to/data" exit 1 end end |