module Artifactory
class Resource::Artifact < Resource::Base
class << self
#
# Search for an artifact by the full or partial filename.
#
# @example Search for all repositories with the name "artifact"
# Artifact.search(name: 'artifact')
#
# @example Search for all artifacts named "artifact" in a specific repo
# Artifact.search(name: 'artifact', repos: 'libs-release-local')
#
# @param [Hash] options
# the list of options to search with
#
# @option options [Artifactory::Client] :client
# the client object to make the request with
# @option options [String] :name
# the name of the artifact to search (it can be a regular expression)
# @option options [String, Array<String>] :repos
# the list of repos to search
#
# @return [Array<Resource::Artifact>]
# a list of artifacts that match the query
#
def search(options = {})
client = extract_client!(options)
params = Util.slice(options, :name, :repos)
format_repos!(params)
client.get('/api/search/artifact', params)['results'].map do |artifact|
from_url(artifact['uri'], client: client)
end
end
#
# Search for an artifact by Maven coordinates: +Group ID+, +Artifact ID+,
# +Version+ and +Classifier+.
#
# @example Search for all repositories with the given gavc
# Artifact.gavc_search(
# group: 'org.acme',
# name: 'artifact',
# version: '1.0',
# classifier: 'sources',
# )
#
# @example Search for all artifacts with the given gavc in a specific repo
# Artifact.gavc_search(
# group: 'org.acme',
# name: 'artifact',
# version: '1.0',
# classifier: 'sources',
# repos: 'libs-release-local',
# )
#
# @param [Hash] options
# the list of options to search with
#
# @option options [Artifactory::Client] :client
# the client object to make the request with
# @option options [String] :group
# the group id to search for
# @option options [String] :name
# the artifact id to search for
# @option options [String] :version
# the version of the artifact to search for
# @option options [String] :classifier
# the classifer to search for
# @option options [String, Array<String>] :repos
# the list of repos to search
#
# @return [Array<Resource::Artifact>]
# a list of artifacts that match the query
#
def gavc_search(options = {})
client = extract_client!(options)
options = Util.rename_keys(options,
:group => :g,
:name => :a,
:version => :v,
:classifier => :c,
)
params = Util.slice(options, :g, :a, :v, :c, :repos)
format_repos!(params)
client.get('/api/search/gavc', params)['results'].map do |artifact|
from_url(artifact['uri'], client: client)
end
end
#
# Search for an artifact by the given properties. These are arbitrary
# properties defined by the user on artifact, so the search uses a free-
# form schema.
#
# @example Search for all repositories with the given properties
# Artifact.property_search(
# branch: 'master',
# author: 'sethvargo',
# )
#
# @example Search for all artifacts with the given gavc in a specific repo
# Artifact.property_search(
# branch: 'master',
# author: 'sethvargo',
# repos: 'libs-release-local',
# )
#
# @param [Hash] options
# the free-form list of options to search with
#
# @option options [Artifactory::Client] :client
# the client object to make the request with
# @option options [String, Array<String>] :repos
# the list of repos to search
#
# @return [Array<Resource::Artifact>]
# a list of artifacts that match the query
#
def property_search(options = {})
client = extract_client!(options)
params = options.dup
format_repos!(params)
client.get('/api/search/prop', params)['results'].map do |artifact|
from_url(artifact['uri'], client: client)
end
end
#
# Search for an artifact by its checksum
#
# @example Search for all repositories with the given MD5 checksum
# Artifact.checksum_search(
# md5: 'abcd1234...',
# )
#
# @example Search for all artifacts with the given SHA1 checksum in a repo
# Artifact.checksum_search(
# sha1: 'abcdef123456....',
# repos: 'libs-release-local',
# )
#
# @param [Hash] options
# the list of options to search with
#
# @option options [Artifactory::Client] :client
# the client object to make the request with
# @option options [String] :md5
# the MD5 checksum of the artifact to search for
# @option options [String] :sha1
# the SHA1 checksum of the artifact to search for
# @option options [String, Array<String>] :repos
# the list of repos to search
#
# @return [Array<Resource::Artifact>]
# a list of artifacts that match the query
#
def checksum_search(options = {})
client = extract_client!(options)
params = Util.slice(options, :md5, :sha1, :repos)
format_repos!(params)
client.get('/api/search/checksum', params)['results'].map do |artifact|
from_url(artifact['uri'], client: client)
end
end
#
# Get all versions of an artifact.
#
# @example Get all versions of a given artifact
# Artifact.versions(name: 'artifact')
# @example Get all versions of a given artifact in a specific repo
# Artifact.versions(name: 'artifact', repos: 'libs-release-local')
#
# @param [Hash] options
# the list of options to search with
#
# @option options [Artifactory::Client] :client
# the client object to make the request with
# @option options [String] :group
# the
# @option options [String] :sha1
# the SHA1 checksum of the artifact to search for
# @option options [String, Array<String>] :repos
# the list of repos to search
#
def versions(options = {})
client = extract_client!(options)
options = Util.rename_keys(options,
:group => :g,
:name => :a,
:version => :v,
)
params = Util.slice(options, :g, :a, :v, :repos)
format_repos!(params)
client.get('/api/search/versions', params)['results']
rescue Error::HTTPError => e
raise unless e.code == 404
[]
end
#
# Get the latest version of an artifact.
#
# @example Find the latest version of an artifact
# Artifact.latest_version(name: 'artifact')
# @example Find the latest version of an artifact in a repo
# Artifact.latest_version(
# name: 'artifact',
# repo: 'libs-release-local',
# )
# @example Find the latest snapshot version of an artifact
# Artifact.latest_version(name: 'artifact', version: '1.0-SNAPSHOT')
# @example Find the latest version of an artifact in a group
# Artifact.latest_version(name: 'artifact', group: 'org.acme')
#
# @param [Hash] options
# the list of options to search with
#
# @option options [Artifactory::Client] :client
# the client object to make the request with
# @option options [String] :group
# the group id to search for
# @option options [String] :name
# the artifact id to search for
# @option options [String] :version
# the version of the artifact to search for
# @option options [Boolean] :remote
# search remote repos (default: +false+)
# @option options [String, Array<String>] :repos
# the list of repos to search
#
# @return [String, nil]
# the latest version as a string (e.g. +1.0-201203131455-2+), or +nil+
# if no artifact matches the given query
#
def latest_version(options = {})
client = extract_client!(options)
options = Util.rename_keys(options,
:group => :g,
:name => :a,
:version => :v,
)
params = Util.slice(options, :g, :a, :v, :repos, :remote)
format_repos!(params)
# For whatever reason, Artifactory won't accept "true" - they want a
# literal "1"...
params[:remote] = 1 if options[:remote]
client.get('/api/search/latestVersion', params)
rescue Error::HTTPError => e
raise unless e.code == 404
nil
end
#
# @see Artifactory::Resource::Base.from_hash
#
def from_hash(hash, options = {})
super.tap do |instance|
instance.created = Time.parse(instance.created) rescue nil
instance.last_modified = Time.parse(instance.last_modified) rescue nil
instance.last_updated = Time.parse(instance.last_updated) rescue nil
instance.size = instance.size.to_i
end
end
end
attribute :uri, ->{ raise 'API path missing!' }
attribute :checksums
attribute :created
attribute :download_uri, ->{ raise 'Download URI missing!' }
attribute :key
attribute :last_modified
attribute :last_updated
attribute :local_path, ->{ raise 'Local destination missing!' }
attribute :mime_type
attribute :repo
attribute :size
#
# The SHA of this artifact.
#
# @return [String]
#
def sha1
checksums && checksums['sha1']
end
#
# The MD5 of this artifact.
#
# @return [String]
#
def md5
checksums && checksums['md5']
end
#
# @see Artifact#copy_or_move
#
def copy(destination, options = {})
copy_or_move(:copy, destination, options)
end
#
# Delete this artifact from repository, suppressing any +ResourceNotFound+
# exceptions might occur.
#
# @return [Boolean]
# true if the object was deleted successfully, false otherwise
#
def delete
!!client.delete(download_uri)
rescue Error::HTTPError
false
end
#
# @see {Artifact#copy_or_move}
#
def move(destination, options = {})
copy_or_move(:move, destination, options)
end
#
# The list of properties for this object.
#
# @example List all properties for an artifact
# artifact.properties #=> { 'artifactory.licenses'=>['Apache-2.0'] }
#
# @return [Hash<String, Object>]
# the list of properties
#
def properties
@properties ||= client.get(uri, properties: nil)['properties']
end
#
# Get compliance info for a given artifact path. The result includes
# license and vulnerabilities, if any.
#
# **This requires the Black Duck addon to be enabled!**
#
# @example Get compliance info for an artifact
# artifact.compliance #=> { 'licenses' => [{ 'name' => 'LGPL v3' }] }
#
# @return [Hash<String, Array<Hash>>]
#
def compliance
@compliance ||= client.get(File.join('/api/compliance', relative_path))
end
#
# Download the artifact onto the local disk.
#
# @example Download an artifact
# artifact.download #=> /tmp/cache/000adad0-bac/artifact.deb
#
# @example Download a remote artifact into a specific target
# artifact.download('~/Desktop') #=> ~/Desktop/artifact.deb
#
# @param [String] target
# the target directory where the artifact should be downloaded to
# (defaults to a temporary directory). **It is the user's responsibility
# to cleanup the temporary directory when finished!**
# @param [Hash] options
# @option options [String] filename
# the name of the file when downloaded to disk (defaults to the basename
# of the file on the server)
#
# @return [String]
# the path where the file was downloaded on disk
#
def download(target = Dir.mktmpdir, options = {})
target = File.expand_path(target)
# Make the directory if it doesn't yet exist
FileUtils.mkdir_p(target) unless File.exists?(target)
# Use the server artifact's filename if one wasn't given
filename = options[:filename] || File.basename(download_uri)
# Construct the full path for the file
destination = File.join(target, filename)
File.open(destination, 'wb') do |file|
file.write(client.get(download_uri))
end
destination
end
#
# Upload an artifact into the repository. If the first parameter is a File
# object, that file descriptor is passed to the uploader. If the first
# parameter is a string, it is assumed to be the path to a local file on
# disk. This method will automatically construct the File object from the
# given path.
#
# @see bit.ly/1dhJRMO Artifactory Matrix Properties
#
# @example Upload an artifact from a File instance
# artifact = Artifact.new(local_path: '/local/path/to/file.deb')
# artifact.upload('libs-release-local', '/remote/path')
#
# @example Upload an artifact with matrix properties
# artifact = Artifact.new(local_path: '/local/path/to/file.deb')
# artifact.upload('libs-release-local', '/remote/path', {
# status: 'DEV',
# rating: 5,
# branch: 'master'
# })
#
# @param [String] repo
# the key of the repository to which to upload the file
# @param [String] remote_path
# the path where this resource will live in the remote artifactory
# repository, relative to the repository key
# @param [Hash] headers
# the list of headers to send with the request
# @param [Hash] properties
# a list of matrix properties
#
# @return [Resource::Artifact]
#
def upload(repo, remote_path, properties = {}, headers = {})
file = File.new(File.expand_path(local_path))
matrix = to_matrix_properties(properties)
endpoint = File.join("#{url_safe(repo)}#{matrix}", remote_path)
response = client.put(endpoint, file, headers)
# Upload checksums if they were given
upload_checksum(repo, remote_path, :md5, md5) if md5
upload_checksum(repo, remote_path, :sha1, sha1) if sha1
self.class.from_hash(response)
end
#
# Upload the checksum for this artifact. **The artifact must already be
# uploaded or Artifactory will throw an exception!**. This is both a public
# and private API. It is automatically called in {upload} if the SHA
# values are set. You may also call it manually.
#
# @example Set an artifact's md5
# artifact = Artifact.new(local_path: '/local/path/to/file.deb')
# artifact.upload_checksum('libs-release-local', '/remote/path', :md5, 'ABCD1234')
#
# @param (see Artifact#upload)
# @param [Symbol] type
# the type of checksum to write (+md5+ or +sha1+)
# @param [String] value
# the actual checksum
#
# @return [true]
#
def upload_checksum(repo, remote_path, type, value)
file = Tempfile.new("checksum.#{type}")
file.write(value)
file.rewind
endpoint = File.join(url_safe(repo), "#{remote_path}.#{type}")
client.put(endpoint, file)
true
ensure
if file
file.close
file.unlink
end
end
#
# Upload an artifact with the given SHA checksum. Consult the artifactory
# documentation for the possible responses when the checksums fail to
# match.
#
# @see Artifact#upload More syntax examples
#
# @example Upload an artifact with a checksum
# artifact = Artifact.new(local_path: '/local/path/to/file.deb')
# artifact.upload_with_checksum('libs-release-local', /remote/path', 'ABCD1234')
#
# @param (see Artifact#upload)
# @param [String] checksum
# the SHA1 checksum of the artifact to upload
#
def upload_with_checksum(repo, remote_path, checksum, properties = {})
upload(repo, remote_path, properties,
'X-Checksum-Deploy' => true,
'X-Checksum-Sha1' => checksum,
)
end
#
# Upload an artifact with the given archive. Consult the artifactory
# documentation for the format of the archive to upload.
#
# @see Artifact#upload More syntax examples
#
# @example Upload an artifact with a checksum
# artifact = Artifact.new(local_path: '/local/path/to/file.deb')
# artifact.upload_from_archive('/remote/path')
#
# @param (see Repository#upload)
#
def upload_from_archive(repo, remote_path, properties = {})
upload(repo, remote_path, properties,
'X-Explode-Archive' => true,
)
end
private
#
# Helper method for extracting the relative (repo) path, since it's not
# returned as part of the API.
#
# @example Get the relative URI from the resource
# /libs-release-local/org/acme/artifact.deb
#
# @return [String]
#
def relative_path
@relative_path ||= uri.split('/api/storage', 2).last
end
#
# Copy or move current artifact to a new destination.
#
# @example Move the current artifact to +ext-releases-local+
# artifact.move(to: '/ext-releaes-local/org/acme')
# @example Copy the current artifact to +ext-releases-local+
# artifact.move(to: '/ext-releaes-local/org/acme')
#
# @param [Symbol] action
# the action (+:move+ or +:copy+)
# @param [String] destination
# the server-side destination to move or copy the artifact
# @param [Hash] options
# the list of options to pass
#
# @option options [Boolean] :fail_fast (default: +false+)
# fail on the first failure
# @option options [Boolean] :suppress_layouts (default: +false+)
# suppress cross-layout module path translation during copying or moving
# @option options [Boolean] :dry_run (default: +false+)
# pretend to do the copy or move
#
# @return [Hash]
# the parsed JSON response from the server
#
def copy_or_move(action, destination, options = {})
params = {}.tap do |param|
param[:to] = destination
param[:failFast] = 1 if options[:fail_fast]
param[:suppressLayouts] = 1 if options[:suppress_layouts]
param[:dry] = 1 if options[:dry_run]
end
# Okay, seriously, WTF Artifactory? Are you fucking serious? You want me
# to make a POST request, but you don't actually read the contents of the
# POST request, you read the URL-params. Sigh, whoever claimed this was a
# RESTful API should seriously consider a new occupation.
params = params.map do |k, v|
key = URI.escape(k.to_s)
value = URI.escape(v.to_s)
"#{key}=#{value}"
end
endpoint = File.join('/api', action.to_s, relative_path) + '?' + params.join('&')
client.post(endpoint)
end
end
end