suppository 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +76 -0
  3. data/.rubocop.yml +5 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +19 -0
  6. data/Gemfile +4 -0
  7. data/Guardfile +30 -0
  8. data/LICENSE.txt +674 -0
  9. data/README.md +103 -0
  10. data/Rakefile +34 -0
  11. data/bin/suppository +36 -0
  12. data/fixtures/curl_7.22.0-3ubuntu4.11_amd64.deb +0 -0
  13. data/fixtures/vim_7.3.547-7_amd64.deb +0 -0
  14. data/lib/suppository/add_command.rb +116 -0
  15. data/lib/suppository/checksummed.rb +24 -0
  16. data/lib/suppository/cli.rb +26 -0
  17. data/lib/suppository/command_runner.rb +30 -0
  18. data/lib/suppository/create_command.rb +75 -0
  19. data/lib/suppository/dpkg_deb.rb +29 -0
  20. data/lib/suppository/dpkg_deb_line.rb +28 -0
  21. data/lib/suppository/exceptions.rb +20 -0
  22. data/lib/suppository/gzip.rb +14 -0
  23. data/lib/suppository/help.rb +22 -0
  24. data/lib/suppository/help_command.rb +13 -0
  25. data/lib/suppository/logger.rb +24 -0
  26. data/lib/suppository/master_deb.rb +62 -0
  27. data/lib/suppository/package.rb +23 -0
  28. data/lib/suppository/release.rb +57 -0
  29. data/lib/suppository/repository.rb +16 -0
  30. data/lib/suppository/tty.rb +43 -0
  31. data/lib/suppository/version.rb +3 -0
  32. data/lib/suppository/version_command.rb +12 -0
  33. data/lib/suppository.rb +5 -0
  34. data/spec/spec_helper.rb +55 -0
  35. data/spec/suppository/add_command_spec.rb +141 -0
  36. data/spec/suppository/cli_spec.rb +50 -0
  37. data/spec/suppository/command_runner_spec.rb +26 -0
  38. data/spec/suppository/create_command_spec.rb +80 -0
  39. data/spec/suppository/dpkg_deb_line_spec.rb +36 -0
  40. data/spec/suppository/dpkg_deb_spec.rb +65 -0
  41. data/spec/suppository/gzip_spec.rb.rb +22 -0
  42. data/spec/suppository/help_command_spec.rb +13 -0
  43. data/spec/suppository/help_spec.rb +12 -0
  44. data/spec/suppository/logger_spec.rb +64 -0
  45. data/spec/suppository/master_deb_spec.rb +84 -0
  46. data/spec/suppository/package_spec.rb +63 -0
  47. data/spec/suppository/release_spec.rb +70 -0
  48. data/spec/suppository/repository_spec.rb +53 -0
  49. data/spec/suppository/tty_spec.rb +92 -0
  50. data/spec/suppository/version_command_spec.rb +13 -0
  51. data/spec/suppository/version_spec.rb +13 -0
  52. data/spec/suppository_spec.rb +83 -0
  53. data/suppository.gemspec +34 -0
  54. metadata +286 -0
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Super Simple Apt Repository
2
+ [![Build Status](https://travis-ci.org/TheBookPeople/suppository.svg?branch=develop)](https://travis-ci.org/TheBookPeople/suppository) [![Code Climate](https://codeclimate.com/github/TheBookPeople/suppository/badges/gpa.svg)](https://codeclimate.com/github/TheBookPeople/suppository) [![Test Coverage](https://codeclimate.com/github/TheBookPeople/suppository/badges/coverage.svg)](https://codeclimate.com/github/TheBookPeople/suppository)
3
+
4
+ Based on the ideas from Super Simple Apt Repository https://github.com/lukepfarrar/suppository.
5
+
6
+ A RubyGem that can be used to manage a simple apt repository.
7
+
8
+ ## Installation
9
+
10
+ $ gem install suppository
11
+
12
+ ## Usage
13
+
14
+ ### Help
15
+
16
+ $ suppository help
17
+
18
+ ### Version
19
+
20
+ $ suppository version
21
+
22
+ ### Create new repository
23
+
24
+ $ suppository create REPOSITORY_PATH
25
+
26
+ ### Add Deb to existing repository
27
+
28
+ $ suppository add REPOSITORY_PATH DIST_NAME COMPONENT_NAME DEB_FILE
29
+
30
+ ## Build
31
+
32
+ ### Prerequisites
33
+
34
+ Tested on Ruby 1.9.3, 2.0.0, 2.1.5 and 2.2.0
35
+
36
+ Bundler
37
+
38
+ RubyGems
39
+
40
+ #### OSX
41
+
42
+ If you are developing on a Mac the you will need to install dpkg and gpg for the tests to pass. The simplest way to install it is with
43
+ Homebrew (see http://brew.sh/ on how to install Homebrew)
44
+
45
+ $ brew install dpkg
46
+ $ brew install gpg
47
+
48
+ #### Ubuntu / Debian
49
+
50
+ dpkg will already be installed but you might need to install gpg.
51
+
52
+
53
+ ### Run tests
54
+ The default rake task will run code quality checks and all the tests.
55
+
56
+ $ bundle install
57
+ $ bundle exec rake
58
+
59
+ If you want to automatically run the tests during development, you can use Guard. Guard will watch for file changes
60
+ and run the appropriate tests. See https://github.com/guard/guard for more information on guard
61
+
62
+ $ bundle exec guard
63
+
64
+ ### Create Gem
65
+
66
+ $ bundle exec rake build
67
+
68
+ This will run all the tests and then create a gem file. NOTE: Only files tracked by Git will be included in the gem.
69
+
70
+ ### Release
71
+
72
+ Check everything build and the tests pass
73
+
74
+ $ bundle exec build
75
+
76
+ Create release using GitFlow (http://danielkummer.github.io/git-flow-cheatsheet/)
77
+
78
+ $ git flow release start [version]
79
+
80
+ Update the version number and commit changes
81
+
82
+ $ vi lib/suppository/version.rb
83
+
84
+ Finish release
85
+
86
+ $ git flow release finish [version]
87
+
88
+ Push changes
89
+
90
+ $ git push
91
+ $ git checkout develop
92
+ $ git push
93
+ $ git push --tags
94
+
95
+ Travis will now build and deploy to RubyGems.org
96
+
97
+ ## Contributing
98
+
99
+ 1. Fork it ( https://github.com/TheBookPeople/suppository/fork )
100
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
101
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
102
+ 4. Push to the branch (`git push origin my-new-feature`)
103
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+ require 'suppository/version'
6
+
7
+ desc "Run Code quality checks and tests "
8
+ task :default => [:clean,:rubocop,:test]
9
+
10
+ desc "Run Code quality checks, tests and then create Gem File"
11
+ task :build => [:clean,:rubocop,:test,:gem]
12
+
13
+ CLEAN.include("suppository-#{Suppository::VERSION}.gem")
14
+ CLEAN.include('coverage')
15
+
16
+ task :test do
17
+ RSpec::Core::RakeTask.new(:spec) do |t|
18
+ t.pattern = 'spec/**/*_spec.rb'
19
+ t.verbose = false
20
+ end
21
+
22
+ Rake::Task["spec"].execute
23
+ end
24
+
25
+ task :rubocop do
26
+ RuboCop::RakeTask.new(:rubocop) do |task|
27
+ task.patterns = ['lib/**/*.rb']
28
+ task.fail_on_error = true
29
+ end
30
+ end
31
+
32
+ task :gem do
33
+ system "gem build suppository.gemspec"
34
+ end
data/bin/suppository ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # -*- mode: ruby -*-
4
+ require_relative '../lib/suppository/cli'
5
+ require_relative '../lib/suppository/logger'
6
+ require_relative '../lib/suppository/help'
7
+
8
+ std_trap = trap("INT") { exit! 130 } # no backtrace thanks
9
+
10
+ begin
11
+ trap("INT", std_trap) # restore default CTRL-C handler
12
+ Suppository::CLI.run(ARGV)
13
+ rescue UsageError
14
+ Suppository::Logger.log_error "Invalid usage"
15
+ abort Suppository.help
16
+ rescue SystemExit
17
+ puts "Kernel.exit" if ARGV.verbose?
18
+ raise
19
+ rescue Interrupt => e
20
+ puts # seemingly a newline is typical
21
+ exit 130
22
+ rescue RuntimeError, SystemCallError => e
23
+ raise if e.message.empty?
24
+ Suppository::Logger.log_error(e)
25
+ exit 1
26
+ rescue Exception => e
27
+ Suppository::Logger.log_error(e)
28
+ puts "#{Suppository::Tty.white}Please report this bug:"
29
+ puts " #{Suppository::Tty.em}https://github.com/TheBookPeople/suppository/issues#{Suppository::Tty.reset}"
30
+ puts e.backtrace
31
+ exit 1
32
+ end
33
+
34
+
35
+
36
+
Binary file
@@ -0,0 +1,116 @@
1
+
2
+ require 'suppository/master_deb'
3
+ require 'suppository/repository'
4
+ require 'suppository/exceptions'
5
+ require 'suppository/package'
6
+ require 'suppository/release'
7
+ require 'suppository/logger'
8
+ require 'suppository/checksummed'
9
+ require 'suppository/gzip'
10
+ require 'fileutils'
11
+
12
+ module Suppository
13
+ class AddCommand
14
+ include Suppository::Logger
15
+
16
+ def initialize(args)
17
+ @unsigned = parse_params(args)
18
+ @repository = Suppository::Repository.new(args[0])
19
+ @dist = args[1]
20
+ @component = args[2]
21
+ @debs = Dir.glob(args[3])
22
+ end
23
+
24
+ def run
25
+ assert_repository_exists
26
+ assert_dist_exists
27
+ assert_component_exists
28
+
29
+ @debs.each { |deb| add_deb Suppository::Checksummed.new(deb) }
30
+
31
+ Suppository::Release.new(@repository.path, @dist, @unsigned).create
32
+ end
33
+
34
+ private
35
+
36
+ def parse_params(args)
37
+ fail UsageError if args.nil? || args.length < 4 || args.length > 5
38
+ fail UsageError if args.length == 5 && args[4] != '--unsigned'
39
+ args.length == 5
40
+ end
41
+
42
+ def add_deb(deb)
43
+ create_suppository_file(deb)
44
+ create_dist_file(suppository_file(deb), deb)
45
+ f = File.basename(deb.path)
46
+ message = "#{f} added to repository #{@repository.path}, #{@dist} #{@component}"
47
+ log_success message
48
+ end
49
+
50
+ def assert_repository_exists
51
+ message = "#{@repository.path} is not a valid repository.\n"
52
+ message << "You can create a new repository by running the following command\n\n"
53
+ message << " suppository create #{@repository.path}"
54
+ fail InvalidRepositoryError, message unless @repository.exist?
55
+ end
56
+
57
+ def assert_dist_exists
58
+ supported_dist = @repository.dists.join(', ')
59
+ message = "#{@dist} does not exist, try one of the following #{supported_dist}"
60
+ fail InvalidDistribution, message unless File.exist?("#{dist_path}")
61
+ end
62
+
63
+ def assert_component_exists
64
+ message = "#{@component} does not exist, try internal instead"
65
+ fail InvalidComponent, message unless File.exist?("#{component_path}")
66
+ end
67
+
68
+ def create_suppository_file(deb)
69
+ FileUtils.copy_file(deb.path, suppository_file(deb), true)
70
+ end
71
+
72
+ def create_dist_file(master_file, deb)
73
+ @repository.archs.each do |arch|
74
+ FileUtils.ln_s master_file, dist_file(arch, deb), force: true
75
+ update_packages master_file, arch
76
+ end
77
+ end
78
+
79
+ def update_packages(master_file, arch)
80
+ deb = Suppository::MasterDeb.new(master_file)
81
+ file = package_file(arch)
82
+ package_info = Suppository::Package.new(internal_path(arch), deb).content
83
+ open(file, 'a') { |f| f.puts package_info }
84
+ Suppository::Gzip.compress file
85
+ end
86
+
87
+ def dist_file(arch, deb)
88
+ filename = Suppository::MasterDeb.new(suppository_file(deb)).filename
89
+ "#{component_path}/binary-#{arch}/#{filename}"
90
+ end
91
+
92
+ def package_file(arch)
93
+ "#{component_path}/binary-#{arch}/Packages"
94
+ end
95
+
96
+ def internal_path(arch)
97
+ "dists/#{@dist}/#{@component}/binary-#{arch}"
98
+ end
99
+
100
+ def suppository_file(deb)
101
+ "#{suppository}/#{deb.md5}_#{deb.sha1}_#{deb.sha2}.deb"
102
+ end
103
+
104
+ def dist_path
105
+ "#{@repository.path}/dists/#{@dist}"
106
+ end
107
+
108
+ def component_path
109
+ "#{dist_path}/#{@component}"
110
+ end
111
+
112
+ def suppository
113
+ @repository.suppository
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,24 @@
1
+
2
+ require 'digest'
3
+
4
+ module Suppository
5
+ class Checksummed
6
+ attr_reader :path
7
+
8
+ def initialize(path)
9
+ @path = File.expand_path(path)
10
+ end
11
+
12
+ def md5
13
+ @md5 ||= Digest::MD5.file(@path).hexdigest
14
+ end
15
+
16
+ def sha1
17
+ @sha1 ||= Digest::SHA1.file(@path).hexdigest
18
+ end
19
+
20
+ def sha2
21
+ @sha2 ||= Digest::SHA2.file(@path).hexdigest
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+
2
+ require 'suppository/exceptions'
3
+
4
+ module Suppository
5
+ class CLI
6
+ def self.run(args)
7
+ fail UsageError if args.empty?
8
+ cmd = args.delete_at(0)
9
+
10
+ begin
11
+ clazz(cmd).new(args).run
12
+ rescue LoadError
13
+ raise UsageError
14
+ end
15
+ end
16
+
17
+ def self.clazz(cmd)
18
+ require "suppository/#{cmd}_command"
19
+ clazz_name(cmd).split('::').inject(Object) { |a, e| a.const_get e }
20
+ end
21
+
22
+ def self.clazz_name(cmd)
23
+ "Suppository::#{cmd.capitalize}Command"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+
2
+ require 'suppository/exceptions'
3
+
4
+ module Suppository
5
+ class CommandRunner
6
+ def initialize(command, arguments = '')
7
+ @command = command
8
+ @arguments = arguments
9
+ end
10
+
11
+ def run
12
+ assert_exists
13
+ run_command
14
+ end
15
+
16
+ private
17
+
18
+ def assert_exists
19
+ `which "#{@command}"`
20
+ message = "'#{@command}' was not found."
21
+ fail(CommandMissingError, message) unless $CHILD_STATUS.success?
22
+ end
23
+
24
+ def run_command
25
+ output = `#{@command} #{@arguments} 2>&1`
26
+ fail(CommandError, output) unless $CHILD_STATUS.success?
27
+ output
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,75 @@
1
+
2
+ require 'fileutils'
3
+ require 'suppository/logger'
4
+ require 'suppository/repository'
5
+ require 'suppository/exceptions'
6
+ require 'suppository/gzip'
7
+
8
+ module Suppository
9
+ class CreateCommand
10
+ include Suppository::Logger
11
+
12
+ def initialize(args)
13
+ assert_arguments args
14
+ @repository = repository(args[0])
15
+ end
16
+
17
+ def run
18
+ assert_not_created
19
+ create_repository
20
+ end
21
+
22
+ private
23
+
24
+ def assert_arguments(args)
25
+ message = 'Create command needs one argument, the path to the new repository'
26
+ fail UsageError, message if args.nil? || args.length != 1
27
+ end
28
+
29
+ def repository(path)
30
+ Suppository::Repository.new(path)
31
+ end
32
+
33
+ def assert_not_created
34
+ @repository.exist? ? fail("#{path} is already a repository") : ''
35
+ end
36
+
37
+ def create_repository
38
+ FileUtils.mkdir_p "#{suppository}"
39
+ create_dists_folders
40
+ log_success "Created new Repository - #{path}"
41
+ end
42
+
43
+ def create_dists_folders
44
+ @repository.dists.each do |dist|
45
+ create_archs_folders dist
46
+ end
47
+ end
48
+
49
+ def create_archs_folders(dist)
50
+ @repository.archs.each do |arch|
51
+ create_folder(path, dist, arch)
52
+ end
53
+ end
54
+
55
+ def create_folder(path, dist, arch)
56
+ dir_path = "#{path}/dists/#{dist}/internal/binary-#{arch}"
57
+ FileUtils.mkdir_p dir_path
58
+ create_packages_file(dir_path)
59
+ end
60
+
61
+ def create_packages_file(path)
62
+ packages_file = "#{path}/Packages"
63
+ FileUtils.touch packages_file
64
+ Suppository::Gzip.compress packages_file
65
+ end
66
+
67
+ def suppository
68
+ @repository.suppository
69
+ end
70
+
71
+ def path
72
+ @repository.path
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,29 @@
1
+
2
+ require 'suppository/dpkg_deb_line'
3
+ require 'suppository/command_runner'
4
+
5
+ module Suppository
6
+ class DpkgDeb
7
+ attr_reader :attibutes
8
+
9
+ def initialize(deb_path)
10
+ command = CommandRunner.new('dpkg-deb', "-f #{deb_path}")
11
+ @attibutes = parser(command.run)
12
+ end
13
+
14
+ private
15
+
16
+ def parser(output)
17
+ attibutes = {}
18
+ output.each_line do |line|
19
+ attibute = parser_line(line)
20
+ attibutes = attibutes.merge(attibute) { |_, first, second| first + second }
21
+ end
22
+ attibutes
23
+ end
24
+
25
+ def parser_line(output_line)
26
+ DpkgDebLine.new(output_line).attributes
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ module Suppository
2
+ class DpkgDebLine
3
+ DESCRIPTION_FIELD = 'Description'
4
+
5
+ attr_reader :attributes
6
+
7
+ def initialize(line)
8
+ field = split_line(line)
9
+ if description?(line)
10
+ @attributes = { DESCRIPTION_FIELD => line }
11
+ elsif field
12
+ @attributes = { field['fieldname'] => field['fieldvalue'] }
13
+ else
14
+ fail "can't parse line - '#{line}'"
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def split_line(line)
21
+ /^(?<fieldname>[^:]+): (?<fieldvalue>.+)$/.match(line)
22
+ end
23
+
24
+ def description?(line)
25
+ /^ .+$/.match(line)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ class UsageError < RuntimeError
2
+ end
3
+
4
+ class InvalidDistribution < RuntimeError
5
+ end
6
+
7
+ class InvalidComponent < RuntimeError
8
+ end
9
+
10
+ class CommandMissingError < RuntimeError
11
+ end
12
+
13
+ class InvalidMasterDeb < RuntimeError
14
+ end
15
+
16
+ class InvalidRepositoryError < RuntimeError
17
+ end
18
+
19
+ class CommandError < RuntimeError
20
+ end
@@ -0,0 +1,14 @@
1
+
2
+ require 'zlib'
3
+
4
+ module Suppository
5
+ module Gzip
6
+ def self.compress(file)
7
+ Zlib::GzipWriter.open("#{file}.gz") do |gz|
8
+ gz.mtime = File.mtime(file)
9
+ gz.orig_name = file
10
+ gz.write IO.read(file)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ HELP = <<-EOS
2
+ Example usage:
3
+
4
+ suppository help
5
+ - Display this Help message
6
+
7
+ suppository version
8
+ - Display version
9
+
10
+ suppository create <REPOSITORY_PATH>
11
+ - Create new empty repository in REPOSITORY_PATH
12
+
13
+ suppository add <REPOSITORY_PATH> <DIST> <COMPONENT> <DEB_FILE> [--unsigned]
14
+ - Add DEB_FILE to DIST and COMPONENT of repository at REPOSITORY_PATH
15
+
16
+ EOS
17
+
18
+ module Suppository
19
+ def self.help
20
+ HELP
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+
2
+ require 'suppository/help'
3
+
4
+ module Suppository
5
+ class HelpCommand
6
+ def initialize(_)
7
+ end
8
+
9
+ def run
10
+ puts Suppository.help
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+
2
+ require 'suppository/tty'
3
+
4
+ module Suppository
5
+ module Logger
6
+ def log_info(message)
7
+ puts message
8
+ end
9
+
10
+ def log_verbose(message)
11
+ puts "#{Tty.gray}#{message}#{Tty.reset}"
12
+ end
13
+
14
+ def log_error(error)
15
+ $stderr.puts "#{Tty.red}Error#{Tty.reset}: #{error}"
16
+ end
17
+
18
+ def log_success(message)
19
+ puts "#{Tty.green}==>#{Tty.white} #{message}#{Tty.reset}"
20
+ end
21
+
22
+ module_function :log_info, :log_error, :log_success, :log_verbose
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ require 'suppository/exceptions'
2
+ require 'suppository/dpkg_deb'
3
+
4
+ module Suppository
5
+ class MasterDeb
6
+ attr_reader :md5sum, :sha256, :sha1, :dirname
7
+
8
+ def initialize(path)
9
+ @path = path
10
+
11
+ assert_in_suppository
12
+ checksums = checksums_from_name
13
+
14
+ @md5sum = checksums['md5']
15
+ @sha1 = checksums['sha1']
16
+ @sha256 = checksums['sha256']
17
+
18
+ @dirname = File.dirname(path)
19
+ @attr = Suppository::DpkgDeb.new(path).attibutes
20
+ end
21
+
22
+ def filename
23
+ "#{@attr['Package']}_#{@attr['Version']}_#{@attr['Architecture']}.deb"
24
+ end
25
+
26
+ def full_attr
27
+ full_attrs = @attr
28
+ full_attrs['Size'] = size
29
+ full_attrs['MD5Sum'] = @md5sum
30
+ full_attrs['SHA1'] = @sha1
31
+ full_attrs['SHA256'] = @sha256
32
+ full_attrs
33
+ end
34
+
35
+ def size
36
+ File.size(@path)
37
+ end
38
+
39
+ private
40
+
41
+ def assert_in_suppository
42
+ message = 'Master deb must be in the .suppository folder'
43
+ fail InvalidMasterDeb, message unless suppository_file?
44
+ end
45
+
46
+ def suppository_file?
47
+ File.dirname(@path).end_with?('.suppository')
48
+ end
49
+
50
+ def checksums_from_name
51
+ file_name = File.basename(@path)
52
+ matches = filename_regex.match(file_name)
53
+ message = 'Master deb must have the following name {md5}_{sha1}_{sha256}.deb'
54
+ fail InvalidMasterDeb, message unless matches
55
+ matches
56
+ end
57
+
58
+ def filename_regex
59
+ /^(?<md5>[a-f0-9]{32})_(?<sha1>[a-f0-9]{40})_(?<sha256>[a-f0-9]{64})\.deb$/
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+
2
+ module Suppository
3
+ class Package
4
+ def initialize(parent_folder, deb)
5
+ @deb = deb
6
+ @parent_folder = parent_folder
7
+ end
8
+
9
+ def content
10
+ full_attrs = @deb.full_attr
11
+ full_attrs[:Filename] = filename
12
+ full_attrs.sort_by { |k, _v| k == 'Description' ? 1 : 0 }
13
+ .to_a.map { |kv_pair| kv_pair.join(': ') }
14
+ .join("\n") << "\n\n"
15
+ end
16
+
17
+ private
18
+
19
+ def filename
20
+ "#{@parent_folder}/#{@deb.filename}"
21
+ end
22
+ end
23
+ end