deb-s3-lock-fix 0.11.8.fix0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +294 -0
- data/bin/deb-s3 +10 -0
- data/lib/deb/s3/cli.rb +767 -0
- data/lib/deb/s3/lock.rb +96 -0
- data/lib/deb/s3/manifest.rb +144 -0
- data/lib/deb/s3/package.rb +309 -0
- data/lib/deb/s3/release.rb +161 -0
- data/lib/deb/s3/templates/package.erb +66 -0
- data/lib/deb/s3/templates/release.erb +20 -0
- data/lib/deb/s3/utils.rb +115 -0
- data/lib/deb/s3.rb +6 -0
- metadata +125 -0
data/lib/deb/s3/lock.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require "aws-sdk-dynamodb"
|
2
|
+
require "securerandom"
|
3
|
+
require "etc"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
class Deb::S3::Lock
|
7
|
+
attr_reader :user, :host_with_uuid
|
8
|
+
|
9
|
+
DYNAMODB_TABLE_NAME = 'deb-s3-lock'
|
10
|
+
|
11
|
+
def initialize(user, host_with_uuid)
|
12
|
+
@user = user
|
13
|
+
@host_with_uuid = host_with_uuid
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.dynamodb
|
17
|
+
@dynamodb ||= begin
|
18
|
+
validate_environment_variables!
|
19
|
+
Aws::DynamoDB::Client.new(
|
20
|
+
access_key_id: ENV['DEB_S3_ACCESS_KEY_ID'],
|
21
|
+
secret_access_key: ENV['DEB_S3_SECRET_ACCESS_KEY'],
|
22
|
+
region: ENV['AWS_BUILDERS_REGION']
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.validate_environment_variables!
|
28
|
+
%w[DEB_S3_LOCK_ACCESS_KEY_ID DEB_S3_LOCK_SECRET_ACCESS_KEY AWS_BUILDERS_REGION].each do |var|
|
29
|
+
raise "Environment variable #{var} not set." unless ENV[var]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def lock(codename, max_attempts = 60, max_wait_interval = 10)
|
35
|
+
uuid = SecureRandom.uuid
|
36
|
+
lock_body = "#{Etc.getlogin}@#{Socket.gethostname}-#{uuid}"
|
37
|
+
lock_key = codename
|
38
|
+
|
39
|
+
$stderr.puts("Current job's hostname with UUID: #{lock_body}")
|
40
|
+
|
41
|
+
max_attempts.times do |i|
|
42
|
+
wait_interval = [2**i, max_wait_interval].min
|
43
|
+
|
44
|
+
expiration_time = Time.now.to_i + 45
|
45
|
+
begin
|
46
|
+
dynamodb.put_item({
|
47
|
+
table_name: DYNAMODB_TABLE_NAME,
|
48
|
+
item: {
|
49
|
+
'lock_key' => "52",
|
50
|
+
'lock_body' => lock_body,
|
51
|
+
'ttl' => expiration_time
|
52
|
+
},
|
53
|
+
condition_expression: "attribute_not_exists(lock_key)"
|
54
|
+
})
|
55
|
+
return
|
56
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
57
|
+
lock_holder = current_lock_holder(codename)
|
58
|
+
current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
59
|
+
$stderr.puts("[#{current_time}] Repository is locked by another user: #{lock_holder.user} at host #{lock_holder.host_with_uuid}")
|
60
|
+
$stderr.puts("Attempting to obtain a lock after #{wait_interval} second(s).")
|
61
|
+
sleep(wait_interval)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
raise "Unable to obtain a lock after #{max_attempts} attemtps, giving up."
|
66
|
+
end
|
67
|
+
|
68
|
+
def unlock(codename)
|
69
|
+
# dynamodb.delete_item({
|
70
|
+
# table_name: DYNAMODB_TABLE_NAME,
|
71
|
+
# key: {
|
72
|
+
# 'lock_key' => codename
|
73
|
+
# }
|
74
|
+
# })
|
75
|
+
end
|
76
|
+
|
77
|
+
def current_lock_holder(codename)
|
78
|
+
response = dynamodb.get_item({
|
79
|
+
table_name: DYNAMODB_TABLE_NAME,
|
80
|
+
key: {
|
81
|
+
'lock_key' => codename
|
82
|
+
}
|
83
|
+
})
|
84
|
+
|
85
|
+
if response.item
|
86
|
+
lockdata = response.item['lock_body']
|
87
|
+
user, host_with_uuid = lockdata.split("@", 2)
|
88
|
+
Deb::S3::Lock.new(user, host_with_uuid)
|
89
|
+
else
|
90
|
+
Deb::S3::Lock.new("unknown", "unknown")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private :dynamodb, :validate_environment_variables!
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "tempfile"
|
3
|
+
require "zlib"
|
4
|
+
require 'deb/s3/utils'
|
5
|
+
require 'deb/s3/package'
|
6
|
+
|
7
|
+
class Deb::S3::Manifest
|
8
|
+
include Deb::S3::Utils
|
9
|
+
|
10
|
+
attr_accessor :codename
|
11
|
+
attr_accessor :component
|
12
|
+
attr_accessor :cache_control
|
13
|
+
attr_accessor :architecture
|
14
|
+
attr_accessor :fail_if_exists
|
15
|
+
attr_accessor :skip_package_upload
|
16
|
+
|
17
|
+
attr_accessor :files
|
18
|
+
|
19
|
+
attr_reader :packages
|
20
|
+
attr_reader :packages_to_be_upload
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@packages = []
|
24
|
+
@packages_to_be_upload = []
|
25
|
+
@component = nil
|
26
|
+
@architecture = nil
|
27
|
+
@files = {}
|
28
|
+
@cache_control = ""
|
29
|
+
@fail_if_exists = false
|
30
|
+
@skip_package_upload = false
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def retrieve(codename, component, architecture, cache_control, fail_if_exists, skip_package_upload=false)
|
35
|
+
m = if s = Deb::S3::Utils.s3_read("dists/#{codename}/#{component}/binary-#{architecture}/Packages")
|
36
|
+
self.parse_packages(s)
|
37
|
+
else
|
38
|
+
self.new
|
39
|
+
end
|
40
|
+
|
41
|
+
m.codename = codename
|
42
|
+
m.component = component
|
43
|
+
m.architecture = architecture
|
44
|
+
m.cache_control = cache_control
|
45
|
+
m.fail_if_exists = fail_if_exists
|
46
|
+
m.skip_package_upload = skip_package_upload
|
47
|
+
m
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_packages(str)
|
51
|
+
m = self.new
|
52
|
+
str.split("\n\n").each do |s|
|
53
|
+
next if s.chomp.empty?
|
54
|
+
m.packages << Deb::S3::Package.parse_string(s)
|
55
|
+
end
|
56
|
+
m
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def add(pkg, preserve_versions, needs_uploading=true)
|
61
|
+
if self.fail_if_exists
|
62
|
+
packages.each { |p|
|
63
|
+
next unless p.name == pkg.name && \
|
64
|
+
p.full_version == pkg.full_version && \
|
65
|
+
File.basename(p.url_filename(@codename)) != \
|
66
|
+
File.basename(pkg.url_filename(@codename))
|
67
|
+
raise AlreadyExistsError,
|
68
|
+
"package #{pkg.name}_#{pkg.full_version} already exists " \
|
69
|
+
"with different filename (#{p.url_filename(@codename)})"
|
70
|
+
}
|
71
|
+
end
|
72
|
+
if preserve_versions
|
73
|
+
packages.delete_if { |p| p.name == pkg.name && p.full_version == pkg.full_version }
|
74
|
+
else
|
75
|
+
packages.delete_if { |p| p.name == pkg.name }
|
76
|
+
end
|
77
|
+
packages << pkg
|
78
|
+
packages_to_be_upload << pkg if needs_uploading
|
79
|
+
pkg
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete_package(pkg, versions=nil)
|
83
|
+
deleted = []
|
84
|
+
new_packages = @packages.select { |p|
|
85
|
+
# Include packages we didn't name
|
86
|
+
if p.name != pkg
|
87
|
+
p
|
88
|
+
# Also include the packages not matching a specified version
|
89
|
+
elsif (!versions.nil? and p.name == pkg and !versions.include?(p.version) and !versions.include?("#{p.version}-#{p.iteration}") and !versions.include?(p.full_version))
|
90
|
+
p
|
91
|
+
end
|
92
|
+
}
|
93
|
+
deleted = @packages - new_packages
|
94
|
+
@packages = new_packages
|
95
|
+
deleted
|
96
|
+
end
|
97
|
+
|
98
|
+
def generate
|
99
|
+
@packages.collect { |pkg| pkg.generate(@codename) }.join("\n")
|
100
|
+
end
|
101
|
+
|
102
|
+
def write_to_s3
|
103
|
+
manifest = self.generate
|
104
|
+
|
105
|
+
unless self.skip_package_upload
|
106
|
+
# store any packages that need to be stored
|
107
|
+
@packages_to_be_upload.each do |pkg|
|
108
|
+
yield pkg.url_filename(@codename) if block_given?
|
109
|
+
s3_store(pkg.filename, pkg.url_filename(@codename), 'application/octet-stream; charset=binary', self.cache_control, self.fail_if_exists)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# generate the Packages file
|
114
|
+
pkgs_temp = Tempfile.new("Packages")
|
115
|
+
pkgs_temp.write manifest
|
116
|
+
pkgs_temp.close
|
117
|
+
f = "dists/#{@codename}/#{@component}/binary-#{@architecture}/Packages"
|
118
|
+
yield f if block_given?
|
119
|
+
s3_store(pkgs_temp.path, f, 'text/plain; charset=utf-8', self.cache_control)
|
120
|
+
@files["#{@component}/binary-#{@architecture}/Packages"] = hashfile(pkgs_temp.path)
|
121
|
+
pkgs_temp.unlink
|
122
|
+
|
123
|
+
# generate the Packages.gz file
|
124
|
+
gztemp = Tempfile.new("Packages.gz")
|
125
|
+
gztemp.close
|
126
|
+
Zlib::GzipWriter.open(gztemp.path) { |gz| gz.write manifest }
|
127
|
+
f = "dists/#{@codename}/#{@component}/binary-#{@architecture}/Packages.gz"
|
128
|
+
yield f if block_given?
|
129
|
+
s3_store(gztemp.path, f, 'application/x-gzip; charset=binary', self.cache_control)
|
130
|
+
@files["#{@component}/binary-#{@architecture}/Packages.gz"] = hashfile(gztemp.path)
|
131
|
+
gztemp.unlink
|
132
|
+
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
def hashfile(path)
|
137
|
+
{
|
138
|
+
:size => File.size(path),
|
139
|
+
:sha1 => Digest::SHA1.file(path).hexdigest,
|
140
|
+
:sha256 => Digest::SHA2.file(path).hexdigest,
|
141
|
+
:md5 => Digest::MD5.file(path).hexdigest
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "digest/sha1"
|
3
|
+
require "digest/sha2"
|
4
|
+
require "digest/md5"
|
5
|
+
require "open3"
|
6
|
+
require "socket"
|
7
|
+
require "tmpdir"
|
8
|
+
require "uri"
|
9
|
+
|
10
|
+
require 'deb/s3/utils'
|
11
|
+
|
12
|
+
class Deb::S3::Package
|
13
|
+
include Deb::S3::Utils
|
14
|
+
|
15
|
+
attr_accessor :name
|
16
|
+
attr_accessor :version
|
17
|
+
attr_accessor :epoch
|
18
|
+
attr_accessor :iteration
|
19
|
+
attr_accessor :maintainer
|
20
|
+
attr_accessor :vendor
|
21
|
+
attr_accessor :url
|
22
|
+
attr_accessor :category
|
23
|
+
attr_accessor :license
|
24
|
+
attr_accessor :architecture
|
25
|
+
attr_accessor :description
|
26
|
+
|
27
|
+
attr_accessor :dependencies
|
28
|
+
|
29
|
+
# Any other attributes specific to this package.
|
30
|
+
# This is where you'd put rpm, deb, or other specific attributes.
|
31
|
+
attr_accessor :attributes
|
32
|
+
|
33
|
+
# hashes
|
34
|
+
attr_accessor :sha1
|
35
|
+
attr_accessor :sha256
|
36
|
+
attr_accessor :md5
|
37
|
+
attr_accessor :size
|
38
|
+
|
39
|
+
attr_accessor :filename
|
40
|
+
|
41
|
+
class << self
|
42
|
+
include Deb::S3::Utils
|
43
|
+
|
44
|
+
def parse_file(package)
|
45
|
+
p = self.new
|
46
|
+
p.extract_info(extract_control(package))
|
47
|
+
p.apply_file_info(package)
|
48
|
+
p.filename = package
|
49
|
+
p
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_string(s)
|
53
|
+
p = self.new
|
54
|
+
p.extract_info(s)
|
55
|
+
p
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_control(package)
|
59
|
+
if system("which dpkg > /dev/null 2>&1")
|
60
|
+
output, status = Open3.capture2("dpkg", "-f", package)
|
61
|
+
output
|
62
|
+
else
|
63
|
+
# use ar to determine control file name (control.ext)
|
64
|
+
package_files = `ar t #{package}`
|
65
|
+
control_file = package_files.split("\n").select do |file|
|
66
|
+
file.start_with?("control.")
|
67
|
+
end.first
|
68
|
+
if control_file === "control.tar.gz"
|
69
|
+
compression = "z"
|
70
|
+
elsif control_file === "control.tar.zst"
|
71
|
+
compression = "I zstd"
|
72
|
+
else
|
73
|
+
compression = "J"
|
74
|
+
end
|
75
|
+
|
76
|
+
# ar fails to find the control.tar.gz tarball within the .deb
|
77
|
+
# on Mac OS. Try using ar to list the control file, if found,
|
78
|
+
# use ar to extract, otherwise attempt with tar which works on OS X.
|
79
|
+
extract_control_tarball_cmd = "ar p #{package} #{control_file}"
|
80
|
+
|
81
|
+
begin
|
82
|
+
safesystem("ar t #{package} #{control_file} &> /dev/null")
|
83
|
+
rescue SafeSystemError
|
84
|
+
warn "Failed to find control data in .deb with ar, trying tar."
|
85
|
+
extract_control_tarball_cmd = "tar -#{compression} -xf #{package} --to-stdout #{control_file}"
|
86
|
+
end
|
87
|
+
|
88
|
+
Dir.mktmpdir do |path|
|
89
|
+
safesystem("#{extract_control_tarball_cmd} | tar -#{compression} -xf - -C #{path}")
|
90
|
+
File.read(File.join(path, "control"))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize
|
97
|
+
@attributes = {}
|
98
|
+
|
99
|
+
# Reference
|
100
|
+
# http://www.debian.org/doc/manuals/maint-guide/first.en.html
|
101
|
+
# http://wiki.debian.org/DeveloperConfiguration
|
102
|
+
# https://github.com/jordansissel/fpm/issues/37
|
103
|
+
if ENV.include?("DEBEMAIL") and ENV.include?("DEBFULLNAME")
|
104
|
+
# Use DEBEMAIL and DEBFULLNAME as the default maintainer if available.
|
105
|
+
@maintainer = "#{ENV["DEBFULLNAME"]} <#{ENV["DEBEMAIL"]}>"
|
106
|
+
else
|
107
|
+
# TODO(sissel): Maybe support using 'git config' for a default as well?
|
108
|
+
# git config --get user.name, etc can be useful.
|
109
|
+
#
|
110
|
+
# Otherwise default to user@currenthost
|
111
|
+
@maintainer = "<#{ENV["USER"]}@#{Socket.gethostname}>"
|
112
|
+
end
|
113
|
+
|
114
|
+
@name = nil
|
115
|
+
@architecture = "native"
|
116
|
+
@description = "no description given"
|
117
|
+
@version = nil
|
118
|
+
@epoch = nil
|
119
|
+
@iteration = nil
|
120
|
+
@url = nil
|
121
|
+
@category = "default"
|
122
|
+
@license = "unknown"
|
123
|
+
@vendor = "none"
|
124
|
+
@sha1 = nil
|
125
|
+
@sha256 = nil
|
126
|
+
@md5 = nil
|
127
|
+
@size = nil
|
128
|
+
@filename = nil
|
129
|
+
@url_filename = nil
|
130
|
+
|
131
|
+
@dependencies = []
|
132
|
+
end
|
133
|
+
|
134
|
+
def full_version
|
135
|
+
return nil if [epoch, version, iteration].all?(&:nil?)
|
136
|
+
[[epoch, version].compact.join(":"), iteration].compact.join("-")
|
137
|
+
end
|
138
|
+
|
139
|
+
def url_filename=(f)
|
140
|
+
@url_filename = f
|
141
|
+
end
|
142
|
+
|
143
|
+
def url_filename(codename)
|
144
|
+
@url_filename || "pool/#{codename}/#{self.name[0]}/#{self.name[0..1]}/#{File.basename(self.filename)}"
|
145
|
+
end
|
146
|
+
|
147
|
+
def generate(codename)
|
148
|
+
template("package.erb").result(binding)
|
149
|
+
end
|
150
|
+
|
151
|
+
# from fpm
|
152
|
+
def parse_depends(data)
|
153
|
+
return [] if data.nil? or data.empty?
|
154
|
+
# parse dependencies. Debian dependencies come in one of two forms:
|
155
|
+
# * name
|
156
|
+
# * name (op version)
|
157
|
+
# They are all on one line, separated by ", "
|
158
|
+
|
159
|
+
dep_re = /^([^ ]+)(?: \(([>=<]+) ([^)]+)\))?$/
|
160
|
+
return data.split(/, */).collect do |dep|
|
161
|
+
m = dep_re.match(dep)
|
162
|
+
if m
|
163
|
+
name, op, version = m.captures
|
164
|
+
# this is the proper form of dependency
|
165
|
+
if op && version && op != "" && version != ""
|
166
|
+
"#{name} (#{op} #{version})".strip
|
167
|
+
else
|
168
|
+
name.strip
|
169
|
+
end
|
170
|
+
else
|
171
|
+
# Assume normal form dependency, "name op version".
|
172
|
+
dep
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end # def parse_depends
|
176
|
+
|
177
|
+
# from fpm
|
178
|
+
def fix_dependency(dep)
|
179
|
+
# Deb dependencies are: NAME (OP VERSION), like "zsh (> 3.0)"
|
180
|
+
# Convert anything that looks like 'NAME OP VERSION' to this format.
|
181
|
+
if dep =~ /[\(,\|]/
|
182
|
+
# Don't "fix" ones that could appear well formed already.
|
183
|
+
else
|
184
|
+
# Convert ones that appear to be 'name op version'
|
185
|
+
name, op, version = dep.split(/ +/)
|
186
|
+
if !version.nil?
|
187
|
+
# Convert strings 'foo >= bar' to 'foo (>= bar)'
|
188
|
+
dep = "#{name} (#{debianize_op(op)} #{version})"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
name_re = /^[^ \(]+/
|
193
|
+
name = dep[name_re]
|
194
|
+
if name =~ /[A-Z]/
|
195
|
+
dep = dep.gsub(name_re) { |n| n.downcase }
|
196
|
+
end
|
197
|
+
|
198
|
+
if dep.include?("_")
|
199
|
+
dep = dep.gsub("_", "-")
|
200
|
+
end
|
201
|
+
|
202
|
+
# Convert gem ~> X.Y.Z to '>= X.Y.Z' and << X.Y+1.0
|
203
|
+
if dep =~ /\(~>/
|
204
|
+
name, version = dep.gsub(/[()~>]/, "").split(/ +/)[0..1]
|
205
|
+
nextversion = version.split(".").collect { |v| v.to_i }
|
206
|
+
l = nextversion.length
|
207
|
+
nextversion[l-2] += 1
|
208
|
+
nextversion[l-1] = 0
|
209
|
+
nextversion = nextversion.join(".")
|
210
|
+
return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"]
|
211
|
+
elsif (m = dep.match(/(\S+)\s+\(!= (.+)\)/))
|
212
|
+
# Append this to conflicts
|
213
|
+
self.conflicts += [dep.gsub(/!=/,"=")]
|
214
|
+
return []
|
215
|
+
elsif (m = dep.match(/(\S+)\s+\(= (.+)\)/)) and
|
216
|
+
self.attributes[:deb_ignore_iteration_in_dependencies?]
|
217
|
+
# Convert 'foo (= x)' to 'foo (>= x)' and 'foo (<< x+1)'
|
218
|
+
# but only when flag --ignore-iteration-in-dependencies is passed.
|
219
|
+
name, version = m[1..2]
|
220
|
+
nextversion = version.split('.').collect { |v| v.to_i }
|
221
|
+
nextversion[-1] += 1
|
222
|
+
nextversion = nextversion.join(".")
|
223
|
+
return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"]
|
224
|
+
else
|
225
|
+
# otherwise the dep is probably fine
|
226
|
+
return dep.rstrip
|
227
|
+
end
|
228
|
+
end # def fix_dependency
|
229
|
+
|
230
|
+
# from fpm
|
231
|
+
def extract_info(control)
|
232
|
+
fields = parse_control(control)
|
233
|
+
|
234
|
+
# Parse 'epoch:version-iteration' in the version string
|
235
|
+
full_version = fields.delete('Version')
|
236
|
+
if full_version !~ /^(?:([0-9]+):)?(.+?)(?:-(.*))?$/
|
237
|
+
raise "Unsupported version string '#{full_version}'"
|
238
|
+
end
|
239
|
+
self.epoch, self.version, self.iteration = $~.captures
|
240
|
+
|
241
|
+
self.architecture = fields.delete('Architecture')
|
242
|
+
self.category = fields.delete('Section')
|
243
|
+
self.license = fields.delete('License') || self.license
|
244
|
+
self.maintainer = fields.delete('Maintainer')
|
245
|
+
self.name = fields.delete('Package')
|
246
|
+
self.url = fields.delete('Homepage')
|
247
|
+
self.vendor = fields.delete('Vendor') || self.vendor
|
248
|
+
self.attributes[:deb_priority] = fields.delete('Priority')
|
249
|
+
self.attributes[:deb_origin] = fields.delete('Origin')
|
250
|
+
self.attributes[:deb_installed_size] = fields.delete('Installed-Size')
|
251
|
+
|
252
|
+
# Packages manifest fields
|
253
|
+
self.url_filename = fields.delete('Filename')
|
254
|
+
self.sha1 = fields.delete('SHA1')
|
255
|
+
self.sha256 = fields.delete('SHA256')
|
256
|
+
self.md5 = fields.delete('MD5sum')
|
257
|
+
self.size = fields.delete('Size')
|
258
|
+
self.description = fields.delete('Description')
|
259
|
+
|
260
|
+
#self.config_files = config_files
|
261
|
+
|
262
|
+
self.dependencies += Array(parse_depends(fields.delete('Depends')))
|
263
|
+
|
264
|
+
self.attributes[:deb_recommends] = fields.delete('Recommends')
|
265
|
+
self.attributes[:deb_suggests] = fields.delete('Suggests')
|
266
|
+
self.attributes[:deb_enhances] = fields.delete('Enhances')
|
267
|
+
self.attributes[:deb_pre_depends] = fields.delete('Pre-Depends')
|
268
|
+
|
269
|
+
self.attributes[:deb_breaks] = fields.delete('Breaks')
|
270
|
+
self.attributes[:deb_conflicts] = fields.delete('Conflicts')
|
271
|
+
self.attributes[:deb_provides] = fields.delete('Provides')
|
272
|
+
self.attributes[:deb_replaces] = fields.delete('Replaces')
|
273
|
+
|
274
|
+
self.attributes[:deb_field] = Hash[fields.map { |k, v|
|
275
|
+
[k.sub(/\AX[BCS]{0,3}-/, ''), v]
|
276
|
+
}]
|
277
|
+
end # def extract_info
|
278
|
+
|
279
|
+
def apply_file_info(file)
|
280
|
+
self.size = File.size(file)
|
281
|
+
self.sha1 = Digest::SHA1.file(file).hexdigest
|
282
|
+
self.sha256 = Digest::SHA2.file(file).hexdigest
|
283
|
+
self.md5 = Digest::MD5.file(file).hexdigest
|
284
|
+
end
|
285
|
+
|
286
|
+
def parse_control(control)
|
287
|
+
field = nil
|
288
|
+
value = ""
|
289
|
+
{}.tap do |fields|
|
290
|
+
control.each_line do |line|
|
291
|
+
if line =~ /^(\s+)(\S.*)$/
|
292
|
+
indent, rest = $1, $2
|
293
|
+
# Continuation
|
294
|
+
if indent.size == 1 && rest == "."
|
295
|
+
value << "\n"
|
296
|
+
rest = ""
|
297
|
+
elsif value.size > 0
|
298
|
+
value << "\n"
|
299
|
+
end
|
300
|
+
value << rest
|
301
|
+
elsif line =~ /^([-\w]+):(.*)$/
|
302
|
+
fields[field] = value if field
|
303
|
+
field, value = $1, $2.strip
|
304
|
+
end
|
305
|
+
end
|
306
|
+
fields[field] = value if field
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
class Deb::S3::Release
|
5
|
+
include Deb::S3::Utils
|
6
|
+
|
7
|
+
attr_accessor :codename
|
8
|
+
attr_accessor :origin
|
9
|
+
attr_accessor :suite
|
10
|
+
attr_accessor :architectures
|
11
|
+
attr_accessor :components
|
12
|
+
attr_accessor :cache_control
|
13
|
+
|
14
|
+
attr_accessor :files
|
15
|
+
attr_accessor :policy
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@origin = nil
|
19
|
+
@suite = nil
|
20
|
+
@codename = nil
|
21
|
+
@architectures = []
|
22
|
+
@components = []
|
23
|
+
@cache_control = ""
|
24
|
+
@files = {}
|
25
|
+
@policy = :public_read
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def retrieve(codename, origin=nil, suite=nil, cache_control=nil)
|
30
|
+
if s = Deb::S3::Utils.s3_read("dists/#{codename}/Release")
|
31
|
+
rel = self.parse_release(s)
|
32
|
+
else
|
33
|
+
rel = self.new
|
34
|
+
end
|
35
|
+
rel.codename = codename
|
36
|
+
rel.origin = origin unless origin.nil?
|
37
|
+
rel.suite = suite unless suite.nil?
|
38
|
+
rel.cache_control = cache_control
|
39
|
+
rel
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_release(str)
|
43
|
+
rel = self.new
|
44
|
+
rel.parse(str)
|
45
|
+
rel
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def filename
|
50
|
+
"dists/#{@codename}/Release"
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse(str)
|
54
|
+
parse = lambda do |field|
|
55
|
+
value = str[/^#{field}: .*/]
|
56
|
+
if value.nil?
|
57
|
+
return nil
|
58
|
+
else
|
59
|
+
return value.split(": ",2).last
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# grab basic fields
|
64
|
+
self.codename = parse.call("Codename")
|
65
|
+
self.origin = parse.call("Origin") || nil
|
66
|
+
self.suite = parse.call("Suite") || nil
|
67
|
+
self.architectures = (parse.call("Architectures") || "").split(/\s+/)
|
68
|
+
self.components = (parse.call("Components") || "").split(/\s+/)
|
69
|
+
|
70
|
+
# find all the hashes
|
71
|
+
str.scan(/^\s+([^\s]+)\s+(\d+)\s+(.+)$/).each do |(hash,size,name)|
|
72
|
+
self.files[name] ||= { :size => size.to_i }
|
73
|
+
case hash.length
|
74
|
+
when 32
|
75
|
+
self.files[name][:md5] = hash
|
76
|
+
when 40
|
77
|
+
self.files[name][:sha1] = hash
|
78
|
+
when 64
|
79
|
+
self.files[name][:sha256] = hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate
|
85
|
+
template("release.erb").result(binding)
|
86
|
+
end
|
87
|
+
|
88
|
+
def write_to_s3
|
89
|
+
# validate some other files are present
|
90
|
+
if block_given?
|
91
|
+
self.validate_others { |f| yield f }
|
92
|
+
else
|
93
|
+
self.validate_others
|
94
|
+
end
|
95
|
+
|
96
|
+
# generate the Release file
|
97
|
+
release_tmp = Tempfile.new("Release")
|
98
|
+
release_tmp.puts self.generate
|
99
|
+
release_tmp.close
|
100
|
+
yield self.filename if block_given?
|
101
|
+
s3_store(release_tmp.path, self.filename, 'text/plain; charset=utf-8', self.cache_control)
|
102
|
+
|
103
|
+
# sign the file, if necessary
|
104
|
+
if Deb::S3::Utils.signing_key
|
105
|
+
key_param = Deb::S3::Utils.signing_key.any? ? "-u #{Deb::S3::Utils.signing_key.join(" -u ")}" : ""
|
106
|
+
if system("gpg -a #{key_param} --digest-algo SHA256 #{Deb::S3::Utils.gpg_options} -s --clearsign #{release_tmp.path}")
|
107
|
+
local_file = release_tmp.path+".asc"
|
108
|
+
remote_file = "dists/#{@codename}/InRelease"
|
109
|
+
yield remote_file if block_given?
|
110
|
+
raise "Unable to locate InRelease file" unless File.exist?(local_file)
|
111
|
+
s3_store(local_file, remote_file, 'application/pgp-signature; charset=us-ascii', self.cache_control)
|
112
|
+
File.unlink(local_file)
|
113
|
+
else
|
114
|
+
raise "Signing the InRelease file failed."
|
115
|
+
end
|
116
|
+
if system("gpg -a #{key_param} --digest-algo SHA256 #{Deb::S3::Utils.gpg_options} -b #{release_tmp.path}")
|
117
|
+
local_file = release_tmp.path+".asc"
|
118
|
+
remote_file = self.filename+".gpg"
|
119
|
+
yield remote_file if block_given?
|
120
|
+
raise "Unable to locate Release signature file" unless File.exist?(local_file)
|
121
|
+
s3_store(local_file, remote_file, 'application/pgp-signature; charset=us-ascii', self.cache_control)
|
122
|
+
File.unlink(local_file)
|
123
|
+
else
|
124
|
+
raise "Signing the Release file failed."
|
125
|
+
end
|
126
|
+
else
|
127
|
+
# remove an existing Release.gpg, if it was there
|
128
|
+
s3_remove(self.filename+".gpg")
|
129
|
+
end
|
130
|
+
|
131
|
+
release_tmp.unlink
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_manifest(manifest)
|
135
|
+
self.components << manifest.component unless self.components.include?(manifest.component)
|
136
|
+
self.architectures << manifest.architecture unless self.architectures.include?(manifest.architecture)
|
137
|
+
self.files.merge!(manifest.files)
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_others
|
141
|
+
to_apply = []
|
142
|
+
self.components.each do |comp|
|
143
|
+
%w(amd64 i386 armhf).each do |arch|
|
144
|
+
next if self.files.has_key?("#{comp}/binary-#{arch}/Packages")
|
145
|
+
|
146
|
+
m = Deb::S3::Manifest.new
|
147
|
+
m.codename = self.codename
|
148
|
+
m.component = comp
|
149
|
+
m.architecture = arch
|
150
|
+
if block_given?
|
151
|
+
m.write_to_s3 { |f| yield f }
|
152
|
+
else
|
153
|
+
m.write_to_s3
|
154
|
+
end
|
155
|
+
to_apply << m
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
to_apply.each { |m| self.update_manifest(m) }
|
160
|
+
end
|
161
|
+
end
|