From ef8b0ad6b10dafc979df34fb65e06e22c1130ee9 Mon Sep 17 00:00:00 2001 From: Slack Coder Date: Tue, 5 Mar 2019 11:10:17 +0100 Subject: rename command -> slackware.com --- README.md | 20 +-- not-slackware.com | 471 ------------------------------------------------------ slackware.com | 471 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 481 insertions(+), 481 deletions(-) delete mode 100755 not-slackware.com create mode 100755 slackware.com diff --git a/README.md b/README.md index caf8894..bed098b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -not-slackware.com +slackware.com ================= A minimalistic tool for interfacing with Slackware's mirrors and website from the command line. @@ -8,7 +8,7 @@ Locally, I rename this to slackware.com. Use this tool at your own risk. Note: Currently this project is pre-release and may be vulnerable to downgrade attacks. ``` - not-slackware.com [command] + slackware.com [command] changelog - show changes described in the Changelog since the last update @@ -40,7 +40,7 @@ Examples Download all updates: ``` -not-slackware.com updates | xargs not-slackware.com download +slackware.com updates | xargs slackware.com download ``` Cron script to download updates and email changes to root: @@ -52,22 +52,22 @@ export QUIET=true export SLACKWARE_MIRROR=https://download.dlackware.com/slackware/slackware-current/ export PATH=$PATH:/usr/local/sbin -not-slackware.com sync -TO_UPDATE="$(not-slackware.com updates)" +slackware.com sync +TO_UPDATE="$(slackware.com updates)" if [ -n "$TO_UPDATE" ]; then - not-slackware.com updates | xargs not-slackware.com download - not-slackware.com changelog | mail -s "updates" root + slackware.com updates | xargs slackware.com download + slackware.com changelog | mail -s "updates" root fi ``` Installation ------------ -Download the not-slackware.com script, and copy it to /usr/local/sbin: +Download the slackware.com script, and copy it to /usr/local/sbin: ``` -chmd +x not-slackware.com -mv not-slackware.com /usr/local/sbin +chmd +x slackware.com +mv slackware.com /usr/local/sbin ``` Import Slackware's GPG key for the user running the tool: diff --git a/not-slackware.com b/not-slackware.com deleted file mode 100755 index 8c4ba65..0000000 --- a/not-slackware.com +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/env ruby - -require "time" - -# TODO: List changelog entries per package -# TODO: List mirrors -# TODO: List removed packages mentioned in changelog -# TODO: Read input from STDIN using '-' -# TODO: Rename command to slackware.com -# TODO: Silence when backgrounded like 'scp' -# BUG: File verification with Checksums appears to fail when QUIET is enabled, - -env_vars = {} -ROOT = ENV["ROOT"] || "/" - env_vars["ROOT"] = ["target root directory", "/"] -CACHE = ENV["PACKAGE_CACHE"] || File.join(ROOT, "var/cache/packages") - env_vars["PACKAGE_CACHE"] = ["local directory for downloads", "$ROOT/var/cache/packages"] -QUIET = ENV["QUIET"] || "false" - env_vars["QUIET"] = ["turn off informational output: 'false' or 'true'", "false"] -MIRROR = ENV["SLACKWARE_MIRROR"] || "https://download.dlackware.com/slackware/slackware-current/" - env_vars["SLACKWARE_MIRROR"] = ["remote root slackware package location", "https://download.dlackware.com/slackware/slackware-current/"] - -CHANGELOG = "ChangeLog.txt" -CHECKSUMS ="CHECKSUMS.md5" -PACKAGES = File.join(ROOT, "/var/log/packages") - -# -# Errors -# - -class DownloadError < StandardError - def to_s - "could not download '#{super.to_s}'" - end -end - -class VerificationError < StandardError - def initialize(file_name, info) - @file_name = file_name - @info = info - end - - def to_s - msg = "verification failed for #{@file_name}" - msg += ": #{@info}" if @info - return msg - end -end - -class NotFoundError < StandardError - def to_s - "no '#{super.to_s}' package found" - end -end - -class SyncRequiredError < StandardError - def to_s - exe_name = File.basename($0) - "Try running '#{exe_name} sync': #{super.to_s}" - end -end - - -def exitOnError(arg) - if arg.is_a? StandardError - exitOnError([arg]) - end - return unless arg.is_a? Array - - errs = arg.select{|v| v.is_a? StandardError} - return if errs.empty? - - errs.each{|err| STDERR.puts "#{err}"} - exit(1) -end - -def verifySynced(path) - unless File.exists?(path) - exitOnError(SyncRequiredError.new("#{path} not found")) - end -end - -# -# Domain -# - -class Package - def self.from_path(package_path) - name = File.basename(package_path) - ext = File.extname(name) - pkg_name = name.slice(0, name.length - ext.length) - return pkg_name - end - - def self.name(package) - # TODO: Check if fail - reg_package=%r{^(.*?)-([^-]+)-([^-]+)-([^-]+)$} - name = reg_package.match(package) - return name[1] - end - - def self.merge(arg1, arg2) - # this is not needed - lookup = {} - arg1.each{|p| lookup[Package.name(p)] = p} - - filtered = arg2.select{|p| - name = Package.name(p) - ! lookup[name] - } - merged = arg1.concat(filtered) - return merged - end - - def self.patched_packages(package_paths) - patch_paths = package_paths.select{|p| p.include?("/patches/")} - patches = patch_paths.map{|p| Package.from_path(p)} - all = package_paths.map{|p| Package.from_path(p)} - return self.merge(patches, all) - end -end - -class Selector - def self.is_match?(selector, pkg) - pkg == selector || Package.name(pkg) == selector - end -end - -class PackagePath - def self.only_base_packages(package_paths) - package_paths.reject{|pkg| - pkg.include?("extra/") || - pkg.include?("pasture/") || - pkg.include?("testing/") - } - end - - # TODO: Check if package not found - # TODO: Correctly return multiple matches - def self.select(package_paths, selector) - if selector.include?("/") then - # file path is given, lets treat it strictly - found = package_paths.select{|p| p.include?(selector)} - return found.first if found.length > 0 - end - - patch_paths = package_paths.select{|p| p.include?("/patches/")} - found = patch_paths.select{|pkg_path| - Selector.is_match?(selector, Package.from_path(pkg_path)) - } - return found.first if found.length > 0 - - other_paths = package_paths.select{|p| !p.include?("/patches/")} - found = other_paths.select{|pkg_path| - Selector.is_match?(selector, Package.from_path(pkg_path)) - } - return found.first - end -end - -class ChangeLog - BREAK="+--------------------------+\n" - PACKAGE_ADDED="Added" - PACKAGE_UPDATED="Updated" - PACKAGE_REBUILT="Rebuilt" - PACKAGE_REMOVED="Removed" - PACKAGE_REG=%r{^(?\S+):\s+(?\S+).} - - def self.parse_entry(entry) - packages = entry.scan(PACKAGE_REG) - # PROBLEM: entry not properly described - # PROBLEM: package names do not have consistent naming - return { - content: entry, - packages: packages, - date: Time.parse(entry.lines.first), - } - end - - def initialize(path) - File.open(path, "r") do |buf| - entries = buf.read().encode("UTF-8", invalid: :replace).split(BREAK) - @entries = entries.map{|e| ChangeLog.parse_entry(e)} - end - end - - def new_packages_since(tim) - changes = @entries.select{|entry| entry[:date] > tim} - ret = changes.map{|entry| - ret = entry[:packages] - .select{|pkg| pkg[1] == PACKAGE_ADDED} - .map{|pkg| pkg[0]} - ret = PackagePath.only_base_packages(ret) - }.flatten - end - - def updates_since(tim) - changes = @entries.select{|entry| entry[:date] > tim} - changes.map{|entry| entry[:content]}.join(BREAK) - end - - def guess_latest_update(installed_packages) - is_installed = {} - installed_packages.each{|pkg| is_installed[pkg] = true} - - last_install_at = @entries.find_index{|entry| - entry[:packages].any?{|entry_pkg| - pkg_name = Package.from_path(entry_pkg[0]) - is_installed[pkg_name] - } - } - last_install_at = @entries.last if last_install_at == nil - @entries[last_install_at][:date] - end -end - -class ChecksumFile - def self.package_paths(path) - err = verifySynced(path) - return err if err - - # could fail - reg_package=%r{(\w+)\s+(\S+.txz)\n} - contents = File.read(path) - paths = contents.scan(reg_package).map{|m| - m[1] - } - return paths - end -end - -def print_array(arr) - arr.each{|i| - puts i - } -end - -class Local - def self.downloaded_packages() - ret = Dir.glob("#{CACHE}/*.txz").map{|p| Package.from_path(p)} - return ret - end - - def self.installed_packages() - Dir.glob("#{PACKAGES}/*").map{|p| File.basename(p)} - end - - def self.packages() - Package.merge(self.downloaded_packages(), self.installed_packages()) - end - - def self.gpg_verify(file_path) - ok = system("gpg2", "--verify-files", file_path, [:out, :err] => "/dev/null") - ok ? nil : VerificationError.new(file_path, "GPG check failed") - end - - def self.md5sum_verify(checksum_path, file_path) - err = verifySynced(checksum_path) - return err if err - - target_checksum = "" - ok = IO.popen(["md5sum", file_path]) {|out| - line = out.gets() - target_checksum = line.split()[0] if line - } - return VerificationError.new(file_path) unless ok - - target_filename = "" - ok = IO.popen(["grep", target_checksum, checksum_path]) {|out| - line = out.gets() - target_filename = line.split()[1] if line - } - return VerificationError.new(file_path, "bad checksum #{target_checksum}") unless ok - - if File.basename(target_filename) != File.basename(file_path) - return VerificationError.new(file_path, "bad checksum #{target_checksum}") - end - return nil - end - - def self.download(src, dst = nil) - dst = File.basename(src) unless dst - dst = File.join(dst, File.basename(src)) if File.directory?(dst) - File.unlink(dst) if File.exists?(dst) - - command = ["wget", - "-O", dst, - src - ] - command << "--quiet" if QUIET == "true" - ok = system(*command) - ok ? nil : DownloadError.new(url) - end - - def self.download_and_verify(mirror, remote_file_path) - file_name = File.basename(remote_file_path) - - if File.extname(file_name) == ".txz" || file_name == CHECKSUMS - remote_file_path_sig = "#{remote_file_path}.asc" - local_file_path_sig = File.join(CACHE, "#{file_name}.asc") - err = self.download(File.join(mirror, remote_file_path_sig), CACHE) - File.unlink(local_file_path_sig) if err && File.exist?(local_file_path_sig) - return err if err - end - - file_name = File.basename(remote_file_path) - err = self.download(File.join(mirror, remote_file_path), CACHE) - return err if err - local_file_path = File.join(CACHE, file_name) - - if File.extname(file_name) == ".txz" || file_name == CHECKSUMS - err = self.gpg_verify(local_file_path_sig) - File.unlink(local_file_path) if err && File.exist?(local_file_path) - return err if err - end - unless file_name == CHECKSUMS - checksum_path = File.join(CACHE, CHECKSUMS) - err = self.md5sum_verify(checksum_path, local_file_path) - File.unlink(local_file_path) if err && File.exist?(local_file_path) - return err if err - end - end -end - -# -# Commands -# - -def command_changelog(changelog_path, installed_packages) - exitOnError(verifySynced(changelog_path)) - - changelog = ChangeLog.new(changelog_path) - since = changelog.guess_latest_update(installed_packages) - updates = changelog.updates_since(since) - puts updates unless updates.empty? -end - -def command_download(checksum_path, selectors) - package_paths = ChecksumFile.package_paths(checksum_path) - - arg_paths = selectors.map{|selector| - ret = PackagePath.select(package_paths, selector) - ret || NotFoundError.new(selector) - } - exitOnError(arg_paths) - - res = arg_paths.map{|path| - Local.download_and_verify(MIRROR, path) - } - exitOnError(res) -end - -def command_list(checksum_path) - package_paths = ChecksumFile.package_paths(checksum_path) - remote_packages = Package.patched_packages(package_paths) - print_array(remote_packages.sort()) -end - - -def command_sync(mirror) - err = Local.download_and_verify(mirror, CHECKSUMS) - exitOnError(err) - err = Local.download_and_verify(mirror, CHANGELOG) - exitOnError(err) -end - -def command_updates(changelog_path, checksum_path, installed_packages) - # TODO(MUST): protect against downgrade attacks - - package_paths = ChecksumFile.package_paths(checksum_path) - package_paths = PackagePath.only_base_packages(package_paths) - remote_packages = Package.patched_packages(package_paths) - local_packages = installed_packages - - lookup = {} - local_packages.each{|p| - name = Package.name(p) - lookup[name] = p - } - - updates = remote_packages.select{|p| - name = Package.name(p) - lookup[name] && lookup[name] != p - } - - changelog = ChangeLog.new(changelog_path) - since = changelog.guess_latest_update(installed_packages) - new_packages = changelog.new_packages_since(since) - updates = updates.concat(new_packages) - - print_array(updates.sort()) -end - -# -# Main -# - -class Command - def initialize() - @commands = {} - @env_vars = {} - self.add_subcommand("help", "list commands offered by this tool", ->{self.help(STDOUT)}) - end - - def add_subcommand(name, description, command) - @commands[name] = { name:name, desc: description, cmd: command} - end - - def add_env_var(name, description, default) - @env_vars[name] = { name:name, desc: description, default: default} - end - - def run(name) - cmd = @commands[name] - cmd = cmd || {cmd: ->{self.help(STDERR)}} - cmd[:cmd].call() - end - - def help(fd) - exe_name = File.basename($0) - fd.puts "#{exe_name} [command]" - fd.puts "" - - @commands.to_a.sort.each{|h| - v = h[1] - fd.puts "\t#{v[:name]}" - fd.puts "\t\t- #{v[:desc]}" - } - - fd.puts "" - fd.puts "Environment Variables" - fd.puts "" - @env_vars.to_a.sort.each{|h| - v = h[1] - fd.puts "\t#{v[:name]} (#{v[:default]})" - fd.puts "\t\t- #{v[:desc]}" - } - - end -end - -command = Command.new() - -env_vars.each{|h,v| command.add_env_var(h, v[0], v[1])} -command.add_subcommand( - "changelog", - "show changes described in the Changelog since the last update", - ->{command_changelog(File.join(CACHE,CHANGELOG), Local.installed_packages())}, -) -command.add_subcommand( - "download", - "download the given packages", - ->{to_download = ARGV.drop(1); command_download(File.join(CACHE, CHECKSUMS), to_download)}, -) -# TODO: list w/ directory prefix and include pasture etc. -command.add_subcommand( - "list", - "print all packages on the mirror", - ->{command_list(File.join(CACHE,CHECKSUMS))}, -) -command.add_subcommand( - "sync", - "update knowledge about mirror's content", - ->{command_sync(MIRROR)}, -) -command.add_subcommand( - "updates", - "print all new and updated packages on mirror", - ->{command_updates(File.join(CACHE,CHANGELOG), File.join(CACHE, CHECKSUMS), Local.packages())} -) -command.run(ARGV[0]) diff --git a/slackware.com b/slackware.com new file mode 100755 index 0000000..8c4ba65 --- /dev/null +++ b/slackware.com @@ -0,0 +1,471 @@ +#!/usr/bin/env ruby + +require "time" + +# TODO: List changelog entries per package +# TODO: List mirrors +# TODO: List removed packages mentioned in changelog +# TODO: Read input from STDIN using '-' +# TODO: Rename command to slackware.com +# TODO: Silence when backgrounded like 'scp' +# BUG: File verification with Checksums appears to fail when QUIET is enabled, + +env_vars = {} +ROOT = ENV["ROOT"] || "/" + env_vars["ROOT"] = ["target root directory", "/"] +CACHE = ENV["PACKAGE_CACHE"] || File.join(ROOT, "var/cache/packages") + env_vars["PACKAGE_CACHE"] = ["local directory for downloads", "$ROOT/var/cache/packages"] +QUIET = ENV["QUIET"] || "false" + env_vars["QUIET"] = ["turn off informational output: 'false' or 'true'", "false"] +MIRROR = ENV["SLACKWARE_MIRROR"] || "https://download.dlackware.com/slackware/slackware-current/" + env_vars["SLACKWARE_MIRROR"] = ["remote root slackware package location", "https://download.dlackware.com/slackware/slackware-current/"] + +CHANGELOG = "ChangeLog.txt" +CHECKSUMS ="CHECKSUMS.md5" +PACKAGES = File.join(ROOT, "/var/log/packages") + +# +# Errors +# + +class DownloadError < StandardError + def to_s + "could not download '#{super.to_s}'" + end +end + +class VerificationError < StandardError + def initialize(file_name, info) + @file_name = file_name + @info = info + end + + def to_s + msg = "verification failed for #{@file_name}" + msg += ": #{@info}" if @info + return msg + end +end + +class NotFoundError < StandardError + def to_s + "no '#{super.to_s}' package found" + end +end + +class SyncRequiredError < StandardError + def to_s + exe_name = File.basename($0) + "Try running '#{exe_name} sync': #{super.to_s}" + end +end + + +def exitOnError(arg) + if arg.is_a? StandardError + exitOnError([arg]) + end + return unless arg.is_a? Array + + errs = arg.select{|v| v.is_a? StandardError} + return if errs.empty? + + errs.each{|err| STDERR.puts "#{err}"} + exit(1) +end + +def verifySynced(path) + unless File.exists?(path) + exitOnError(SyncRequiredError.new("#{path} not found")) + end +end + +# +# Domain +# + +class Package + def self.from_path(package_path) + name = File.basename(package_path) + ext = File.extname(name) + pkg_name = name.slice(0, name.length - ext.length) + return pkg_name + end + + def self.name(package) + # TODO: Check if fail + reg_package=%r{^(.*?)-([^-]+)-([^-]+)-([^-]+)$} + name = reg_package.match(package) + return name[1] + end + + def self.merge(arg1, arg2) + # this is not needed + lookup = {} + arg1.each{|p| lookup[Package.name(p)] = p} + + filtered = arg2.select{|p| + name = Package.name(p) + ! lookup[name] + } + merged = arg1.concat(filtered) + return merged + end + + def self.patched_packages(package_paths) + patch_paths = package_paths.select{|p| p.include?("/patches/")} + patches = patch_paths.map{|p| Package.from_path(p)} + all = package_paths.map{|p| Package.from_path(p)} + return self.merge(patches, all) + end +end + +class Selector + def self.is_match?(selector, pkg) + pkg == selector || Package.name(pkg) == selector + end +end + +class PackagePath + def self.only_base_packages(package_paths) + package_paths.reject{|pkg| + pkg.include?("extra/") || + pkg.include?("pasture/") || + pkg.include?("testing/") + } + end + + # TODO: Check if package not found + # TODO: Correctly return multiple matches + def self.select(package_paths, selector) + if selector.include?("/") then + # file path is given, lets treat it strictly + found = package_paths.select{|p| p.include?(selector)} + return found.first if found.length > 0 + end + + patch_paths = package_paths.select{|p| p.include?("/patches/")} + found = patch_paths.select{|pkg_path| + Selector.is_match?(selector, Package.from_path(pkg_path)) + } + return found.first if found.length > 0 + + other_paths = package_paths.select{|p| !p.include?("/patches/")} + found = other_paths.select{|pkg_path| + Selector.is_match?(selector, Package.from_path(pkg_path)) + } + return found.first + end +end + +class ChangeLog + BREAK="+--------------------------+\n" + PACKAGE_ADDED="Added" + PACKAGE_UPDATED="Updated" + PACKAGE_REBUILT="Rebuilt" + PACKAGE_REMOVED="Removed" + PACKAGE_REG=%r{^(?\S+):\s+(?\S+).} + + def self.parse_entry(entry) + packages = entry.scan(PACKAGE_REG) + # PROBLEM: entry not properly described + # PROBLEM: package names do not have consistent naming + return { + content: entry, + packages: packages, + date: Time.parse(entry.lines.first), + } + end + + def initialize(path) + File.open(path, "r") do |buf| + entries = buf.read().encode("UTF-8", invalid: :replace).split(BREAK) + @entries = entries.map{|e| ChangeLog.parse_entry(e)} + end + end + + def new_packages_since(tim) + changes = @entries.select{|entry| entry[:date] > tim} + ret = changes.map{|entry| + ret = entry[:packages] + .select{|pkg| pkg[1] == PACKAGE_ADDED} + .map{|pkg| pkg[0]} + ret = PackagePath.only_base_packages(ret) + }.flatten + end + + def updates_since(tim) + changes = @entries.select{|entry| entry[:date] > tim} + changes.map{|entry| entry[:content]}.join(BREAK) + end + + def guess_latest_update(installed_packages) + is_installed = {} + installed_packages.each{|pkg| is_installed[pkg] = true} + + last_install_at = @entries.find_index{|entry| + entry[:packages].any?{|entry_pkg| + pkg_name = Package.from_path(entry_pkg[0]) + is_installed[pkg_name] + } + } + last_install_at = @entries.last if last_install_at == nil + @entries[last_install_at][:date] + end +end + +class ChecksumFile + def self.package_paths(path) + err = verifySynced(path) + return err if err + + # could fail + reg_package=%r{(\w+)\s+(\S+.txz)\n} + contents = File.read(path) + paths = contents.scan(reg_package).map{|m| + m[1] + } + return paths + end +end + +def print_array(arr) + arr.each{|i| + puts i + } +end + +class Local + def self.downloaded_packages() + ret = Dir.glob("#{CACHE}/*.txz").map{|p| Package.from_path(p)} + return ret + end + + def self.installed_packages() + Dir.glob("#{PACKAGES}/*").map{|p| File.basename(p)} + end + + def self.packages() + Package.merge(self.downloaded_packages(), self.installed_packages()) + end + + def self.gpg_verify(file_path) + ok = system("gpg2", "--verify-files", file_path, [:out, :err] => "/dev/null") + ok ? nil : VerificationError.new(file_path, "GPG check failed") + end + + def self.md5sum_verify(checksum_path, file_path) + err = verifySynced(checksum_path) + return err if err + + target_checksum = "" + ok = IO.popen(["md5sum", file_path]) {|out| + line = out.gets() + target_checksum = line.split()[0] if line + } + return VerificationError.new(file_path) unless ok + + target_filename = "" + ok = IO.popen(["grep", target_checksum, checksum_path]) {|out| + line = out.gets() + target_filename = line.split()[1] if line + } + return VerificationError.new(file_path, "bad checksum #{target_checksum}") unless ok + + if File.basename(target_filename) != File.basename(file_path) + return VerificationError.new(file_path, "bad checksum #{target_checksum}") + end + return nil + end + + def self.download(src, dst = nil) + dst = File.basename(src) unless dst + dst = File.join(dst, File.basename(src)) if File.directory?(dst) + File.unlink(dst) if File.exists?(dst) + + command = ["wget", + "-O", dst, + src + ] + command << "--quiet" if QUIET == "true" + ok = system(*command) + ok ? nil : DownloadError.new(url) + end + + def self.download_and_verify(mirror, remote_file_path) + file_name = File.basename(remote_file_path) + + if File.extname(file_name) == ".txz" || file_name == CHECKSUMS + remote_file_path_sig = "#{remote_file_path}.asc" + local_file_path_sig = File.join(CACHE, "#{file_name}.asc") + err = self.download(File.join(mirror, remote_file_path_sig), CACHE) + File.unlink(local_file_path_sig) if err && File.exist?(local_file_path_sig) + return err if err + end + + file_name = File.basename(remote_file_path) + err = self.download(File.join(mirror, remote_file_path), CACHE) + return err if err + local_file_path = File.join(CACHE, file_name) + + if File.extname(file_name) == ".txz" || file_name == CHECKSUMS + err = self.gpg_verify(local_file_path_sig) + File.unlink(local_file_path) if err && File.exist?(local_file_path) + return err if err + end + unless file_name == CHECKSUMS + checksum_path = File.join(CACHE, CHECKSUMS) + err = self.md5sum_verify(checksum_path, local_file_path) + File.unlink(local_file_path) if err && File.exist?(local_file_path) + return err if err + end + end +end + +# +# Commands +# + +def command_changelog(changelog_path, installed_packages) + exitOnError(verifySynced(changelog_path)) + + changelog = ChangeLog.new(changelog_path) + since = changelog.guess_latest_update(installed_packages) + updates = changelog.updates_since(since) + puts updates unless updates.empty? +end + +def command_download(checksum_path, selectors) + package_paths = ChecksumFile.package_paths(checksum_path) + + arg_paths = selectors.map{|selector| + ret = PackagePath.select(package_paths, selector) + ret || NotFoundError.new(selector) + } + exitOnError(arg_paths) + + res = arg_paths.map{|path| + Local.download_and_verify(MIRROR, path) + } + exitOnError(res) +end + +def command_list(checksum_path) + package_paths = ChecksumFile.package_paths(checksum_path) + remote_packages = Package.patched_packages(package_paths) + print_array(remote_packages.sort()) +end + + +def command_sync(mirror) + err = Local.download_and_verify(mirror, CHECKSUMS) + exitOnError(err) + err = Local.download_and_verify(mirror, CHANGELOG) + exitOnError(err) +end + +def command_updates(changelog_path, checksum_path, installed_packages) + # TODO(MUST): protect against downgrade attacks + + package_paths = ChecksumFile.package_paths(checksum_path) + package_paths = PackagePath.only_base_packages(package_paths) + remote_packages = Package.patched_packages(package_paths) + local_packages = installed_packages + + lookup = {} + local_packages.each{|p| + name = Package.name(p) + lookup[name] = p + } + + updates = remote_packages.select{|p| + name = Package.name(p) + lookup[name] && lookup[name] != p + } + + changelog = ChangeLog.new(changelog_path) + since = changelog.guess_latest_update(installed_packages) + new_packages = changelog.new_packages_since(since) + updates = updates.concat(new_packages) + + print_array(updates.sort()) +end + +# +# Main +# + +class Command + def initialize() + @commands = {} + @env_vars = {} + self.add_subcommand("help", "list commands offered by this tool", ->{self.help(STDOUT)}) + end + + def add_subcommand(name, description, command) + @commands[name] = { name:name, desc: description, cmd: command} + end + + def add_env_var(name, description, default) + @env_vars[name] = { name:name, desc: description, default: default} + end + + def run(name) + cmd = @commands[name] + cmd = cmd || {cmd: ->{self.help(STDERR)}} + cmd[:cmd].call() + end + + def help(fd) + exe_name = File.basename($0) + fd.puts "#{exe_name} [command]" + fd.puts "" + + @commands.to_a.sort.each{|h| + v = h[1] + fd.puts "\t#{v[:name]}" + fd.puts "\t\t- #{v[:desc]}" + } + + fd.puts "" + fd.puts "Environment Variables" + fd.puts "" + @env_vars.to_a.sort.each{|h| + v = h[1] + fd.puts "\t#{v[:name]} (#{v[:default]})" + fd.puts "\t\t- #{v[:desc]}" + } + + end +end + +command = Command.new() + +env_vars.each{|h,v| command.add_env_var(h, v[0], v[1])} +command.add_subcommand( + "changelog", + "show changes described in the Changelog since the last update", + ->{command_changelog(File.join(CACHE,CHANGELOG), Local.installed_packages())}, +) +command.add_subcommand( + "download", + "download the given packages", + ->{to_download = ARGV.drop(1); command_download(File.join(CACHE, CHECKSUMS), to_download)}, +) +# TODO: list w/ directory prefix and include pasture etc. +command.add_subcommand( + "list", + "print all packages on the mirror", + ->{command_list(File.join(CACHE,CHECKSUMS))}, +) +command.add_subcommand( + "sync", + "update knowledge about mirror's content", + ->{command_sync(MIRROR)}, +) +command.add_subcommand( + "updates", + "print all new and updated packages on mirror", + ->{command_updates(File.join(CACHE,CHANGELOG), File.join(CACHE, CHECKSUMS), Local.packages())} +) +command.run(ARGV[0]) -- cgit v1.2.3