artifactory 1.1.0 → 1.2.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -7
  3. data/CHANGELOG.md +18 -0
  4. data/README.md +25 -4
  5. data/Rakefile +5 -2
  6. data/artifactory.gemspec +0 -3
  7. data/lib/artifactory.rb +2 -4
  8. data/lib/artifactory/client.rb +210 -85
  9. data/lib/artifactory/configurable.rb +6 -1
  10. data/lib/artifactory/defaults.rb +52 -3
  11. data/lib/artifactory/errors.rb +21 -14
  12. data/lib/artifactory/resources/artifact.rb +132 -58
  13. data/lib/artifactory/resources/base.rb +120 -6
  14. data/lib/artifactory/resources/build.rb +2 -1
  15. data/lib/artifactory/resources/group.rb +5 -72
  16. data/lib/artifactory/resources/layout.rb +106 -0
  17. data/lib/artifactory/resources/repository.rb +62 -114
  18. data/lib/artifactory/resources/system.rb +5 -1
  19. data/lib/artifactory/resources/user.rb +5 -79
  20. data/lib/artifactory/util.rb +8 -2
  21. data/lib/artifactory/version.rb +1 -1
  22. data/spec/integration/resources/layout_spec.rb +22 -0
  23. data/spec/integration/resources/repository_spec.rb +7 -0
  24. data/spec/integration/resources/system_spec.rb +4 -4
  25. data/spec/support/api_server/repository_endpoints.rb +5 -0
  26. data/spec/support/api_server/system_endpoints.rb +18 -0
  27. data/spec/unit/client_spec.rb +17 -98
  28. data/spec/unit/resources/artifact_spec.rb +99 -13
  29. data/spec/unit/resources/build_spec.rb +1 -1
  30. data/spec/unit/resources/group_spec.rb +1 -1
  31. data/spec/unit/resources/layout_spec.rb +61 -0
  32. data/spec/unit/resources/repository_spec.rb +31 -47
  33. data/spec/unit/resources/system_spec.rb +4 -2
  34. data/spec/unit/resources/user_spec.rb +1 -1
  35. metadata +16 -40
  36. data/locales/en.yml +0 -27
@@ -15,7 +15,12 @@ module Artifactory
15
15
  :endpoint,
16
16
  :username,
17
17
  :password,
18
- :proxy,
18
+ :proxy_address,
19
+ :proxy_password,
20
+ :proxy_port,
21
+ :proxy_username,
22
+ :ssl_pem_file,
23
+ :ssl_verify,
19
24
  :user_agent,
20
25
  ]
21
26
  end
@@ -55,12 +55,61 @@ module Artifactory
55
55
  end
56
56
 
57
57
  #
58
- # The HTTP Proxy information as a string
58
+ # The HTTP Proxy server address as a string
59
59
  #
60
60
  # @return [String, nil]
61
61
  #
62
- def proxy
63
- ENV['ARTIFACTORY_PROXY']
62
+ def proxy_address
63
+ ENV['ARTIFACTORY_PROXY_ADDRESS']
64
+ end
65
+
66
+ #
67
+ # The HTTP Proxy user password as a string
68
+ #
69
+ # @return [String, nil]
70
+ #
71
+ def proxy_password
72
+ ENV['ARTIFACTORY_PROXY_PASSWORD']
73
+ end
74
+
75
+ #
76
+ # The HTTP Proxy server port as a string
77
+ #
78
+ # @return [String, nil]
79
+ #
80
+ def proxy_port
81
+ ENV['ARTIFACTORY_PROXY_PORT']
82
+ end
83
+
84
+ #
85
+ # The HTTP Proxy server username as a string
86
+ #
87
+ # @return [String, nil]
88
+ #
89
+ def proxy_username
90
+ ENV['ARTIFACTORY_PROXY_USERNAME']
91
+ end
92
+
93
+ #
94
+ # The path to a pem file on disk for use with a custom SSL verification
95
+ #
96
+ # @return [String, nil]
97
+ #
98
+ def ssl_pem_file
99
+ ENV['ARTIFACTORY_SSL_PEM_FILE']
100
+ end
101
+
102
+ #
103
+ # Verify SSL requests (default: true)
104
+ #
105
+ # @return [true, false]
106
+ #
107
+ def ssl_verify
108
+ if ENV['ARTIFACTORY_SSL_VERIFY'].nil?
109
+ true
110
+ else
111
+ %w[t y].include?(ENV['ARTIFACTORY_SSL_VERIFY'].downcase[0])
112
+ end
64
113
  end
65
114
  end
66
115
  end
@@ -1,22 +1,29 @@
1
1
  module Artifactory
2
2
  module Error
3
- class ArtifactoryError < StandardError
4
- def initialize(options = {})
5
- class_name = self.class.to_s.split('::').last
6
- error_key = Util.underscore(class_name)
3
+ # Base class for all errors
4
+ class ArtifactoryError < StandardError; end
7
5
 
8
- super I18n.t("artifactory.errors.#{error_key}", options)
6
+ # Class for all HTTP errors
7
+ class HTTPError < ArtifactoryError
8
+ attr_reader :code
9
+ attr_reader :message
10
+
11
+ def initialize(hash = {})
12
+ @code = hash['status'].to_i
13
+ @http = hash['message'].to_s
14
+
15
+ super "The Artifactory server responded with an HTTP Error "\
16
+ "#{@code}: `#{@http}'"
9
17
  end
10
18
  end
11
19
 
12
- #
13
- # Client errors
14
- # ------------------------------
15
- class ConnectionError < ArtifactoryError; end
16
- class BadRequest < ConnectionError; end
17
- class Forbidden < ConnectionError; end
18
- class MethodNotAllowed < ConnectionError; end
19
- class NotFound < ConnectionError; end
20
- class Unauthorized < ConnectionError; end
20
+ # A general connection error with a more informative message
21
+ class ConnectionError < ArtifactoryError
22
+ def initialize(endpoint)
23
+ super "The Artifactory server at `#{endpoint}' is not currently " \
24
+ "accepting connections. Please ensure that the server is " \
25
+ "running an that your authentication information is correct."
26
+ end
27
+ end
21
28
  end
22
29
  end
@@ -198,7 +198,8 @@ module Artifactory
198
198
  format_repos!(params)
199
199
 
200
200
  client.get('/api/search/versions', params)['results']
201
- rescue Error::NotFound
201
+ rescue Error::HTTPError => e
202
+ raise unless e.code == 404
202
203
  []
203
204
  end
204
205
 
@@ -252,73 +253,54 @@ module Artifactory
252
253
  params[:remote] = 1 if options[:remote]
253
254
 
254
255
  client.get('/api/search/latestVersion', params)
255
- rescue Error::NotFound
256
+ rescue Error::HTTPError => e
257
+ raise unless e.code == 404
256
258
  nil
257
259
  end
258
260
 
259
261
  #
260
- # Construct an artifact from the given URL.
261
- #
262
- # @example Create an artifact object from the given URL
263
- # Artifact.from_url('/path/to/some.deb') #=> #<Resource::Artifact>
264
- #
265
- # @param [Artifactory::Client] client
266
- # the client object to make the request with
267
- # @param [String] url
268
- # the URL to find the artifact from
269
- #
270
- # @return [Resource::Artifact]
271
- #
272
- def from_url(url, options = {})
273
- client = extract_client!(options)
274
- from_hash(client.get(url), client: client)
275
- end
276
-
277
- #
278
- # Create a instance from the given Hash. This method extracts the "safe"
279
- # information from the hash and adds them to the instance.
280
- #
281
- # @example Create a new resource from a hash
282
- # Artifact.from_hash('downloadUri' => '...', 'size' => '...')
283
- #
284
- # @param [Artifactory::Client] client
285
- # the client object to make the request with
286
- # @param [Hash] hash
287
- # the hash to create the instance from
288
- #
289
- # @return [Resource::Artifact]
262
+ # @see Artifactory::Resource::Base.from_hash
290
263
  #
291
264
  def from_hash(hash, options = {})
292
- client = extract_client!(options)
293
-
294
- new.tap do |instance|
295
- instance.api_path = hash['uri']
296
- instance.client = client
297
- instance.created = Time.parse(hash['created'])
298
- instance.download_path = hash['downloadUri']
299
- instance.last_modified = Time.parse(hash['lastModified'])
300
- instance.last_updated = Time.parse(hash['lastUpdated'])
301
- instance.md5 = hash['checksums']['md5']
302
- instance.mime_type = hash['mimeType']
303
- instance.repo = hash['repo']
304
- instance.sha1 = hash['checksums']['sha1']
305
- instance.size = hash['size'].to_i
265
+ super.tap do |instance|
266
+ instance.created = Time.parse(instance.created) rescue nil
267
+ instance.last_modified = Time.parse(instance.last_modified) rescue nil
268
+ instance.last_updated = Time.parse(instance.last_updated) rescue nil
269
+ instance.size = instance.size.to_i
306
270
  end
307
271
  end
308
272
  end
309
273
 
310
- attribute :api_path, ->{ raise 'API path missing!' }
274
+ attribute :uri, ->{ raise 'API path missing!' }
275
+ attribute :checksums
311
276
  attribute :created
312
- attribute :download_path, ->{ raise 'Download path missing!' }
277
+ attribute :download_uri, ->{ raise 'Download URI missing!' }
278
+ attribute :key
313
279
  attribute :last_modified
314
280
  attribute :last_updated
315
281
  attribute :local_path, ->{ raise 'Local destination missing!' }
316
282
  attribute :mime_type
317
- attribute :md5
318
283
  attribute :repo
319
- attribute :sha1
320
284
  attribute :size
321
285
 
286
+ #
287
+ # The SHA of this artifact.
288
+ #
289
+ # @return [String]
290
+ #
291
+ def sha1
292
+ checksums && checksums['sha1']
293
+ end
294
+
295
+ #
296
+ # The MD5 of this artifact.
297
+ #
298
+ # @return [String]
299
+ #
300
+ def md5
301
+ checksums && checksums['md5']
302
+ end
303
+
322
304
  #
323
305
  # @see Artifact#copy_or_move
324
306
  #
@@ -334,8 +316,8 @@ module Artifactory
334
316
  # true if the object was deleted successfully, false otherwise
335
317
  #
336
318
  def delete
337
- !!client.delete(download_path)
338
- rescue Error::NotFound
319
+ !!client.delete(download_uri)
320
+ rescue Error::HTTPError
339
321
  false
340
322
  end
341
323
 
@@ -356,7 +338,7 @@ module Artifactory
356
338
  # the list of properties
357
339
  #
358
340
  def properties
359
- @properties ||= client.get(api_path, properties: nil)['properties']
341
+ @properties ||= client.get(uri, properties: nil)['properties']
360
342
  end
361
343
 
362
344
  #
@@ -402,18 +384,110 @@ module Artifactory
402
384
  FileUtils.mkdir_p(target) unless File.exists?(target)
403
385
 
404
386
  # Use the server artifact's filename if one wasn't given
405
- filename = options[:filename] || File.basename(download_path)
387
+ filename = options[:filename] || File.basename(download_uri)
406
388
 
407
389
  # Construct the full path for the file
408
- destination = File.join(targer, filename)
390
+ destination = File.join(target, filename)
409
391
 
410
- File.open(File.join(destination, filename), 'wb') do |file|
411
- file.write(_get(download_path))
392
+ File.open(destination, 'wb') do |file|
393
+ file.write(client.get(download_uri))
412
394
  end
413
395
 
414
396
  destination
415
397
  end
416
398
 
399
+ #
400
+ # Upload an artifact into the repository. If the first parameter is a File
401
+ # object, that file descriptor is passed to the uploader. If the first
402
+ # parameter is a string, it is assumed to be the path to a local file on
403
+ # disk. This method will automatically construct the File object from the
404
+ # given path.
405
+ #
406
+ # @see bit.ly/1dhJRMO Artifactory Matrix Properties
407
+ #
408
+ # @example Upload an artifact from a File instance
409
+ # file = File.new('/local/path/to/file.deb')
410
+ # artifact = Artifact.new
411
+ # artifact.upload(file, 'libs-release-local', file.deb')
412
+ #
413
+ # @example Upload an artifact from a path
414
+ # artifact.upload('/local/path/to/file.deb', 'libs-release-local', 'file.deb')
415
+ #
416
+ # @example Upload an artifact with matrix properties
417
+ # artifact.upload('/local/path/to/file.deb', 'libs-release-local', file.deb', {
418
+ # status: 'DEV',
419
+ # rating: 5,
420
+ # branch: 'master'
421
+ # })
422
+ #
423
+ # @param [String] key
424
+ # the key of the repository to which to upload the file
425
+ # @param [String, File] path_or_io
426
+ # the file or path to the file to upload
427
+ # @param [String] path
428
+ # the path where this resource will live in the remote artifactory
429
+ # repository, relative to the repository key
430
+ # @param [Hash] headers
431
+ # the list of headers to send with the request
432
+ # @param [Hash] properties
433
+ # a list of matrix properties
434
+ #
435
+ # @return [Resource::Artifact]
436
+ #
437
+ def upload(key, path_or_io, path, properties = {}, headers = {})
438
+ file = if respond_to?(:read)
439
+ path_or_io
440
+ else
441
+ File.new(File.expand_path(path_or_io))
442
+ end
443
+
444
+ matrix = to_matrix_properties(properties)
445
+ endpoint = File.join("#{url_safe(key)}#{matrix}", path)
446
+
447
+ response = client.put(endpoint, file, headers)
448
+ self.class.from_hash(response)
449
+ end
450
+
451
+ #
452
+ # Upload an artifact with the given SHA checksum. Consult the artifactory
453
+ # documentation for the possible responses when the checksums fail to
454
+ # match.
455
+ #
456
+ # @see Artifact#upload More syntax examples
457
+ #
458
+ # @example Upload an artifact with a checksum
459
+ # artifact = Artifact.new
460
+ # artifact.upload_with_checksum('/local/file', 'libs-release-local', /remote/path', 'ABCD1234')
461
+ #
462
+ # @param (see Artifact#upload)
463
+ # @param [String] checksum
464
+ # the SHA1 checksum of the artifact to upload
465
+ #
466
+ def upload_with_checksum(key, path_or_io, path, checksum, properties = {})
467
+ upload(key, path_or_io, path, properties,
468
+ 'X-Checksum-Deploy' => true,
469
+ 'X-Checksum-Sha1' => checksum,
470
+ )
471
+ end
472
+
473
+ #
474
+ # Upload an artifact with the given archive. Consult the artifactory
475
+ # documentation for the format of the archive to upload.
476
+ #
477
+ # @see Artifact#upload More syntax examples
478
+ #
479
+ # @example Upload an artifact with a checksum
480
+ # artifact = Artifact.new('libs-release-local')
481
+ # artifact.upload_from_archive('/local/archive', '/remote/path')#
482
+ #
483
+ # @param (see Repository#upload)
484
+ #
485
+ def upload_from_archive(key, path_or_io, path, properties = {})
486
+ upload(key, path_or_io, path, properties,
487
+ 'X-Explode-Archive' => true,
488
+ )
489
+ end
490
+
417
491
  private
418
492
 
419
493
  #
@@ -426,7 +500,7 @@ module Artifactory
426
500
  # @return [String]
427
501
  #
428
502
  def relative_path
429
- @relative_path ||= api_path.split('/api/storage', 2).last
503
+ @relative_path ||= uri.split('/api/storage', 2).last
430
504
  end
431
505
 
432
506
  #
@@ -27,7 +27,7 @@ module Artifactory
27
27
  def attribute(key, default = nil)
28
28
  key = key.to_sym unless key.is_a?(Symbol)
29
29
 
30
- # Set the key on the top attributes, mostly for asthetic purposes
30
+ # Set this attribute in the top-level hash
31
31
  attributes[key] = nil
32
32
 
33
33
  define_method(key) do
@@ -52,6 +52,73 @@ module Artifactory
52
52
  end
53
53
  end
54
54
 
55
+ #
56
+ # The list of attributes defined by this class.
57
+ #
58
+ # @return [Array<Symbol>]
59
+ #
60
+ def attributes
61
+ @attributes ||= {}
62
+ end
63
+
64
+ #
65
+ # Determine if this class has a given attribute.
66
+ #
67
+ # @param [#to_sym] key
68
+ # the key to check as an attribute
69
+ #
70
+ # @return [true, false]
71
+ #
72
+ def has_attribute?(key)
73
+ attributes.has_key?(key.to_sym)
74
+ end
75
+
76
+ #
77
+ # Construct a new object from the given URL.
78
+ #
79
+ # @param [String] url
80
+ # the URL to find the user from
81
+ # @param [Hash] options
82
+ # the list of options
83
+ #
84
+ # @option options [Artifactory::Client] :client
85
+ # the client object to make the request with
86
+ #
87
+ # @return [~Resource::Base]
88
+ #
89
+ def from_url(url, options = {})
90
+ client = extract_client!(options)
91
+ from_hash(client.get(url), client: client)
92
+ end
93
+
94
+ #
95
+ # Construct a new object from the hash.
96
+ #
97
+ # @param [Hash] hash
98
+ # the hash to create the object with
99
+ # @param [Hash] options
100
+ # the list options
101
+ #
102
+ # @option options [Artifactory::Client] :client
103
+ # the client object to make the request with
104
+ #
105
+ # @return [~Resource::Base]
106
+ #
107
+ def from_hash(hash, options = {})
108
+ instance = new
109
+ instance.client = extract_client!(options)
110
+
111
+ hash.inject(instance) do |instance, (key, value)|
112
+ method = :"#{Util.underscore(key)}="
113
+
114
+ if instance.respond_to?(method)
115
+ instance.send(method, value)
116
+ end
117
+
118
+ instance
119
+ end
120
+ end
121
+
55
122
  #
56
123
  # Get the client (connection) object from the given options. If the
57
124
  # +:client+ key is preset in the hash, it is assumed to contain the
@@ -103,10 +170,6 @@ module Artifactory
103
170
  def url_safe(value)
104
171
  URI.escape(value.to_s)
105
172
  end
106
-
107
- def attributes
108
- @attributes ||= {}
109
- end
110
173
  end
111
174
 
112
175
  attribute :client, ->{ Artifactory.client }
@@ -159,6 +222,55 @@ module Artifactory
159
222
  self.class.url_safe(value)
160
223
  end
161
224
 
225
+ #
226
+ # The hash representation
227
+ #
228
+ # @example An example hash response
229
+ # { 'key' => 'local-repo1', 'includesPattern' => '**/*' }
230
+ #
231
+ # @return [Hash]
232
+ #
233
+ def to_hash
234
+ attributes.inject({}) do |hash, (key, value)|
235
+ unless Resource::Base.has_attribute?(key)
236
+ hash[Util.camelize(key, true)] = value
237
+ end
238
+
239
+ hash
240
+ end
241
+ end
242
+
243
+ #
244
+ # The JSON representation of this object.
245
+ #
246
+ # @see Artifactory::Resource::Base#to_json
247
+ #
248
+ # @return [String]
249
+ #
250
+ def to_json
251
+ JSON.fast_generate(to_hash)
252
+ end
253
+
254
+ #
255
+ # Create URI-escaped string from matrix properties
256
+ #
257
+ # @see http://bit.ly/1qeVYQl
258
+ #
259
+ def to_matrix_properties(hash = {})
260
+ properties = hash.map do |k, v|
261
+ key = URI.escape(k.to_s)
262
+ value = URI.escape(v.to_s)
263
+
264
+ "#{key}=#{value}"
265
+ end
266
+
267
+ if properties.empty?
268
+ nil
269
+ else
270
+ ";#{properties.join(';')}"
271
+ end
272
+ end
273
+
162
274
  # @private
163
275
  def to_s
164
276
  "#<#{short_classname}>"
@@ -167,7 +279,9 @@ module Artifactory
167
279
  # @private
168
280
  def inspect
169
281
  list = attributes.collect do |key, value|
170
- "#{key}: #{value.inspect}" unless key == :client
282
+ unless Resource::Base.has_attribute?(key)
283
+ "#{key}: #{value.inspect}"
284
+ end
171
285
  end.compact
172
286
 
173
287
  "#<#{short_classname} #{list.join(', ')}>"