dpkg-s3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "tempfile"
3
+ require "socket"
4
+ require "etc"
5
+
6
+ class Dpkg::S3::Lock
7
+ attr_accessor :user
8
+ attr_accessor :host
9
+
10
+ def initialize
11
+ @user = nil
12
+ @host = nil
13
+ end
14
+
15
+ class << self
16
+ def locked?(codename, component = nil, architecture = nil, cache_control = nil)
17
+ Dpkg::S3::Utils.s3_exists?(lock_path(codename, component, architecture, cache_control))
18
+ end
19
+
20
+ def wait_for_lock(codename, component = nil, architecture = nil, cache_control = nil, max_attempts=60, wait=10)
21
+ attempts = 0
22
+ while self.locked?(codename, component, architecture, cache_control) do
23
+ attempts += 1
24
+ throw "Unable to obtain a lock after #{max_attempts}, giving up." if attempts > max_attempts
25
+ sleep(wait)
26
+ end
27
+ end
28
+
29
+ def lock(codename, component = nil, architecture = nil, cache_control = nil)
30
+ lockfile = Tempfile.new("lockfile")
31
+ lockfile.write("#{Etc.getlogin}@#{Socket.gethostname}")
32
+ lockfile.close
33
+
34
+ Dpkg::S3::Utils.s3_store(lockfile.path,
35
+ lock_path(codename, component, architecture, cache_control),
36
+ "text/plain",
37
+ cache_control)
38
+ end
39
+
40
+ def unlock(codename, component = nil, architecture = nil, cache_control = nil)
41
+ Dpkg::S3::Utils.s3_remove(lock_path(codename, component, architecture, cache_control))
42
+ end
43
+
44
+ def current(codename, component = nil, architecture = nil, cache_control = nil)
45
+ lock_content = Dpkg::S3::Utils.s3_read(lock_path(codename, component, architecture, cache_control))
46
+ lock_content = lock_content.split('@')
47
+ lock = Dpkg::S3::Lock.new
48
+ lock.user = lock_content[0]
49
+ lock.host = lock_content[1] if lock_content.size > 1
50
+ lock
51
+ end
52
+
53
+ private
54
+ def lock_path(codename, component = nil, architecture = nil, cache_control = nil)
55
+ "dists/#{codename}/#{component}/binary-#{architecture}/lockfile"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,144 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "tempfile"
3
+ require "zlib"
4
+ require 'dpkg/s3/utils'
5
+ require 'dpkg/s3/package'
6
+
7
+ class Dpkg::S3::Manifest
8
+ include Dpkg::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 = Dpkg::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 << Dpkg::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,312 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "digest/sha1"
3
+ require "digest/sha2"
4
+ require "digest/md5"
5
+ require "socket"
6
+ require "tmpdir"
7
+ require "uri"
8
+
9
+ require 'dpkg/s3/utils'
10
+
11
+ class Dpkg::S3::Package
12
+ include Dpkg::S3::Utils
13
+
14
+ attr_accessor :name
15
+ attr_accessor :version
16
+ attr_accessor :epoch
17
+ attr_accessor :iteration
18
+ attr_accessor :maintainer
19
+ attr_accessor :vendor
20
+ attr_accessor :url
21
+ attr_accessor :category
22
+ attr_accessor :license
23
+ attr_accessor :architecture
24
+ attr_accessor :description
25
+
26
+ attr_accessor :dependencies
27
+
28
+ # Any other attributes specific to this package.
29
+ # This is where you'd put rpm, deb, or other specific attributes.
30
+ attr_accessor :attributes
31
+
32
+ # hashes
33
+ attr_accessor :url_filename
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 Dpkg::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
+ `dpkg -f #{package}`
61
+ else
62
+ # use ar to determine control file name (control.ext)
63
+ package_files = `ar t #{package}`
64
+ control_file = package_files.split("\n").select do |file|
65
+ file.start_with?("control.")
66
+ end.first
67
+ if control_file === "control.tar.gz"
68
+ compression = "z"
69
+ else
70
+ compression = "J"
71
+ end
72
+
73
+ # ar fails to find the control.tar.gz tarball within the .deb
74
+ # on Mac OS. Try using ar to list the control file, if found,
75
+ # use ar to extract, otherwise attempt with tar which works on OS X.
76
+ extract_control_tarball_cmd = "ar p #{package} #{control_file}"
77
+
78
+ begin
79
+ safesystem("ar t #{package} #{control_file} &> /dev/null")
80
+ rescue SafeSystemError
81
+ warn "Failed to find control data in .deb with ar, trying tar."
82
+ extract_control_tarball_cmd = "tar #{compression}xf #{package} --to-stdout #{control_file}"
83
+ end
84
+
85
+ Dir.mktmpdir do |path|
86
+ safesystem("#{extract_control_tarball_cmd} | tar -#{compression}xf - -C #{path}")
87
+ File.read(File.join(path, "control"))
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def initialize
94
+ @attributes = {}
95
+
96
+ # Reference
97
+ # http://www.debian.org/doc/manuals/maint-guide/first.en.html
98
+ # http://wiki.debian.org/DeveloperConfiguration
99
+ # https://github.com/jordansissel/fpm/issues/37
100
+ if ENV.include?("DEBEMAIL") and ENV.include?("DEBFULLNAME")
101
+ # Use DEBEMAIL and DEBFULLNAME as the default maintainer if available.
102
+ @maintainer = "#{ENV["DEBFULLNAME"]} <#{ENV["DEBEMAIL"]}>"
103
+ else
104
+ # TODO(sissel): Maybe support using 'git config' for a default as well?
105
+ # git config --get user.name, etc can be useful.
106
+ #
107
+ # Otherwise default to user@currenthost
108
+ @maintainer = "<#{ENV["USER"]}@#{Socket.gethostname}>"
109
+ end
110
+
111
+ @name = nil
112
+ @architecture = "native"
113
+ @description = "no description given"
114
+ @version = nil
115
+ @epoch = nil
116
+ @iteration = nil
117
+ @url = nil
118
+ @category = "default"
119
+ @license = "unknown"
120
+ @vendor = "none"
121
+ @sha1 = nil
122
+ @sha256 = nil
123
+ @md5 = nil
124
+ @size = nil
125
+ @filename = nil
126
+ @url_filename = nil
127
+
128
+ @dependencies = []
129
+ end
130
+
131
+ def full_version
132
+ return nil if [epoch, version, iteration].all?(&:nil?)
133
+ [[epoch, version].compact.join(":"), iteration].compact.join("-")
134
+ end
135
+
136
+ def filename=(f)
137
+ @filename = f
138
+ @filename
139
+ end
140
+
141
+ def url_filename(codename)
142
+ @url_filename || "pool/#{codename}/#{self.name[0]}/#{self.name[0..1]}/#{File.basename(self.filename)}"
143
+ end
144
+
145
+ def url_filename_encoded(codename)
146
+ @url_filename || "pool/#{codename}/#{self.name[0]}/#{self.name[0..1]}/#{s3_escape(File.basename(self.filename))}"
147
+ end
148
+
149
+ def generate(codename)
150
+ template("package.erb").result(binding)
151
+ end
152
+
153
+ # from fpm
154
+ def parse_depends(data)
155
+ return [] if data.nil? or data.empty?
156
+ # parse dependencies. Debian dependencies come in one of two forms:
157
+ # * name
158
+ # * name (op version)
159
+ # They are all on one line, separated by ", "
160
+
161
+ dep_re = /^([^ ]+)(?: \(([>=<]+) ([^)]+)\))?$/
162
+ return data.split(/, */).collect do |dep|
163
+ m = dep_re.match(dep)
164
+ if m
165
+ name, op, version = m.captures
166
+ # this is the proper form of dependency
167
+ if op && version && op != "" && version != ""
168
+ "#{name} (#{op} #{version})".strip
169
+ else
170
+ name.strip
171
+ end
172
+ else
173
+ # Assume normal form dependency, "name op version".
174
+ dep
175
+ end
176
+ end
177
+ end # def parse_depends
178
+
179
+ # from fpm
180
+ def fix_dependency(dep)
181
+ # Deb dependencies are: NAME (OP VERSION), like "zsh (> 3.0)"
182
+ # Convert anything that looks like 'NAME OP VERSION' to this format.
183
+ if dep =~ /[\(,\|]/
184
+ # Don't "fix" ones that could appear well formed already.
185
+ else
186
+ # Convert ones that appear to be 'name op version'
187
+ name, op, version = dep.split(/ +/)
188
+ if !version.nil?
189
+ # Convert strings 'foo >= bar' to 'foo (>= bar)'
190
+ dep = "#{name} (#{debianize_op(op)} #{version})"
191
+ end
192
+ end
193
+
194
+ name_re = /^[^ \(]+/
195
+ name = dep[name_re]
196
+ if name =~ /[A-Z]/
197
+ dep = dep.gsub(name_re) { |n| n.downcase }
198
+ end
199
+
200
+ if dep.include?("_")
201
+ dep = dep.gsub("_", "-")
202
+ end
203
+
204
+ # Convert gem ~> X.Y.Z to '>= X.Y.Z' and << X.Y+1.0
205
+ if dep =~ /\(~>/
206
+ name, version = dep.gsub(/[()~>]/, "").split(/ +/)[0..1]
207
+ nextversion = version.split(".").collect { |v| v.to_i }
208
+ l = nextversion.length
209
+ nextversion[l-2] += 1
210
+ nextversion[l-1] = 0
211
+ nextversion = nextversion.join(".")
212
+ return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"]
213
+ elsif (m = dep.match(/(\S+)\s+\(!= (.+)\)/))
214
+ # Append this to conflicts
215
+ self.conflicts += [dep.gsub(/!=/,"=")]
216
+ return []
217
+ elsif (m = dep.match(/(\S+)\s+\(= (.+)\)/)) and
218
+ self.attributes[:deb_ignore_iteration_in_dependencies?]
219
+ # Convert 'foo (= x)' to 'foo (>= x)' and 'foo (<< x+1)'
220
+ # but only when flag --ignore-iteration-in-dependencies is passed.
221
+ name, version = m[1..2]
222
+ nextversion = version.split('.').collect { |v| v.to_i }
223
+ nextversion[-1] += 1
224
+ nextversion = nextversion.join(".")
225
+ return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"]
226
+ else
227
+ # otherwise the dep is probably fine
228
+ return dep.rstrip
229
+ end
230
+ end # def fix_dependency
231
+
232
+ # from fpm
233
+ def extract_info(control)
234
+ fields = parse_control(control)
235
+
236
+ # Parse 'epoch:version-iteration' in the version string
237
+ full_version = fields.delete('Version')
238
+ if full_version !~ /^(?:([0-9]+):)?(.+?)(?:-(.*))?$/
239
+ raise "Unsupported version string '#{full_version}'"
240
+ end
241
+ self.epoch, self.version, self.iteration = $~.captures
242
+
243
+ self.architecture = fields.delete('Architecture')
244
+ self.category = fields.delete('Section')
245
+ self.license = fields.delete('License') || self.license
246
+ self.maintainer = fields.delete('Maintainer')
247
+ self.name = fields.delete('Package')
248
+ self.url = fields.delete('Homepage')
249
+ self.vendor = fields.delete('Vendor') || self.vendor
250
+ self.attributes[:deb_priority] = fields.delete('Priority')
251
+ self.attributes[:deb_origin] = fields.delete('Origin')
252
+ self.attributes[:deb_installed_size] = fields.delete('Installed-Size')
253
+
254
+ # Packages manifest fields
255
+ filename = fields.delete('Filename')
256
+ self.url_filename = filename && URI.unescape(filename)
257
+ self.sha1 = fields.delete('SHA1')
258
+ self.sha256 = fields.delete('SHA256')
259
+ self.md5 = fields.delete('MD5sum')
260
+ self.size = fields.delete('Size')
261
+ self.description = fields.delete('Description')
262
+
263
+ #self.config_files = config_files
264
+
265
+ self.dependencies += Array(parse_depends(fields.delete('Depends')))
266
+
267
+ self.attributes[:deb_recommends] = fields.delete('Recommends')
268
+ self.attributes[:deb_suggests] = fields.delete('Suggests')
269
+ self.attributes[:deb_enhances] = fields.delete('Enhances')
270
+ self.attributes[:deb_pre_depends] = fields.delete('Pre-Depends')
271
+
272
+ self.attributes[:deb_breaks] = fields.delete('Breaks')
273
+ self.attributes[:deb_conflicts] = fields.delete('Conflicts')
274
+ self.attributes[:deb_provides] = fields.delete('Provides')
275
+ self.attributes[:deb_replaces] = fields.delete('Replaces')
276
+
277
+ self.attributes[:deb_field] = Hash[fields.map { |k, v|
278
+ [k.sub(/\AX[BCS]{0,3}-/, ''), v]
279
+ }]
280
+ end # def extract_info
281
+
282
+ def apply_file_info(file)
283
+ self.size = File.size(file)
284
+ self.sha1 = Digest::SHA1.file(file).hexdigest
285
+ self.sha256 = Digest::SHA2.file(file).hexdigest
286
+ self.md5 = Digest::MD5.file(file).hexdigest
287
+ end
288
+
289
+ def parse_control(control)
290
+ field = nil
291
+ value = ""
292
+ {}.tap do |fields|
293
+ control.each_line do |line|
294
+ if line =~ /^(\s+)(\S.*)$/
295
+ indent, rest = $1, $2
296
+ # Continuation
297
+ if indent.size == 1 && rest == "."
298
+ value << "\n"
299
+ rest = ""
300
+ elsif value.size > 0
301
+ value << "\n"
302
+ end
303
+ value << rest
304
+ elsif line =~ /^([-\w]+):(.*)$/
305
+ fields[field] = value if field
306
+ field, value = $1, $2.strip
307
+ end
308
+ end
309
+ fields[field] = value if field
310
+ end
311
+ end
312
+ end