blob: 95e8d63d777228bfae63c4c0e3200dd4a69640f2 [file] [log] [blame]
xixuan44b55452016-09-06 15:35:56 -07001#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07002# -*- coding: utf-8 -*-
Chris Sosa76e44b92013-01-31 12:11:38 -08003# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Gabe Black3b567202015-09-23 14:07:59 -07007"""Downloaders used to download artifacts and files from a given source."""
8
9from __future__ import print_function
10
Prashanth Ba06d2d22014-03-07 15:35:19 -080011import collections
Gabe Black3b567202015-09-23 14:07:59 -070012import glob
Chris Sosa9164ca32012-03-28 11:04:50 -070013import os
Gabe Black3b567202015-09-23 14:07:59 -070014import re
15import shutil
Gilad Arnold0b8c3f32012-09-19 14:35:44 -070016import threading
Prashanth Ba06d2d22014-03-07 15:35:19 -080017from datetime import datetime
Frank Farzan37761d12011-12-01 14:29:08 -080018
Chris Sosa76e44b92013-01-31 12:11:38 -080019import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -070020import common_util
21import log_util
Frank Farzan37761d12011-12-01 14:29:08 -080022
xixuan44b55452016-09-06 15:35:56 -070023# Make sure that chromite is available to import.
24import setup_chromite # pylint: disable=unused-import
25
26try:
27 from chromite.lib import gs
28except ImportError as e:
29 gs = None
30
Dan Shi72b16132015-10-08 12:10:33 -070031try:
32 import android_build
33except ImportError as e:
34 # Ignore android_build import failure. This is to support devserver running
35 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
36 # do not have google-api-python-client module and they don't need to support
37 # Android updating, therefore, ignore the import failure here.
38 android_build = None
39
Frank Farzan37761d12011-12-01 14:29:08 -080040
Dan Shi6e50c722013-08-19 15:05:06 -070041class DownloaderException(Exception):
42 """Exception that aggregates all exceptions raised during async download.
43
44 Exceptions could be raised in artifact.Process method, and saved to files.
45 When caller calls IsStaged to check the downloading progress, devserver can
46 retrieve the persisted exceptions from the files, wrap them into a
47 DownloaderException, and raise it.
48 """
49 def __init__(self, exceptions):
50 """Initialize a DownloaderException instance with a list of exceptions.
51
Gabe Black3b567202015-09-23 14:07:59 -070052 Args:
53 exceptions: Exceptions raised when downloading artifacts.
Dan Shi6e50c722013-08-19 15:05:06 -070054 """
55 message = 'Exceptions were raised when downloading artifacts.'
56 Exception.__init__(self, message)
57 self.exceptions = exceptions
58
59 def __repr__(self):
60 return self.__str__()
61
62 def __str__(self):
63 """Return a custom exception message with all exceptions merged."""
64 return '--------\n'.join([str(exception) for exception in self.exceptions])
65
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080067 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080068
Gabe Black3b567202015-09-23 14:07:59 -070069 This is the base class for different types of downloaders, including
Dan Shi72b16132015-10-08 12:10:33 -070070 GoogleStorageDownloader, LocalDownloader and AndroidBuildDownloader.
Gabe Black3b567202015-09-23 14:07:59 -070071
Frank Farzan37761d12011-12-01 14:29:08 -080072 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080073 - Caches that build and the given artifacts onto the devserver.
74 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080075
Chris Sosa76e44b92013-01-31 12:11:38 -080076 Private class members:
Chris Sosa76e44b92013-01-31 12:11:38 -080077 static_dir: local filesystem directory to store all artifacts.
78 build_dir: the local filesystem directory to store artifacts for the given
Gabe Black3b567202015-09-23 14:07:59 -070079 build based on the remote source.
80
81 Public methods must be overridden:
82 Wait: Verifies the local artifact exists and returns the appropriate names.
83 Fetch: Downloads artifact from given source to a local directory.
84 DescribeSource: Gets the source of the download, e.g., a url to GS.
Frank Farzan37761d12011-12-01 14:29:08 -080085 """
86
Alex Millera44d5022012-07-27 11:34:16 -070087 # This filename must be kept in sync with clean_staged_images.py
88 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070089
Gabe Black3b567202015-09-23 14:07:59 -070090 def __init__(self, static_dir, build_dir, build):
Chris Sosa76e44b92013-01-31 12:11:38 -080091 super(Downloader, self).__init__()
Frank Farzan37761d12011-12-01 14:29:08 -080092 self._static_dir = static_dir
Gabe Black3b567202015-09-23 14:07:59 -070093 self._build_dir = build_dir
94 self._build = build
Chris Masone816e38c2012-05-02 12:22:36 -070095
Gabe Black3b567202015-09-23 14:07:59 -070096 def GetBuildDir(self):
97 """Returns the path to where the artifacts will be staged."""
98 return self._build_dir
Simran Basi4243a862014-12-12 12:48:33 -080099
Gabe Black3b567202015-09-23 14:07:59 -0700100 def GetBuild(self):
101 """Returns the path to where the artifacts will be staged."""
102 return self._build
Frank Farzan37761d12011-12-01 14:29:08 -0800103
Chris Sosa9164ca32012-03-28 11:04:50 -0700104 @staticmethod
Simran Basief83d6a2014-08-28 14:32:01 -0700105 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 11:34:16 -0700106 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
107 # Easiest python version of |touch file_name|
108 with file(file_name, 'a'):
109 os.utime(file_name, None)
110
Dan Shiba0e6742013-06-26 17:39:05 -0700111 @staticmethod
112 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 11:27:54 -0800113 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-26 17:39:05 -0700114
Gilad Arnold02dc6552013-11-14 11:27:54 -0800115 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
116 is the only file in that directory. The build could be non-existing, and
117 the directory should be removed.
Dan Shiba0e6742013-06-26 17:39:05 -0700118
Gabe Black3b567202015-09-23 14:07:59 -0700119 Args:
120 directory_path: directory used to stage the image.
Dan Shiba0e6742013-06-26 17:39:05 -0700121 """
122 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
123 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
124 os.remove(file_name)
125 os.rmdir(directory_path)
126
Prashanth Ba06d2d22014-03-07 15:35:19 -0800127 def ListBuildDir(self):
128 """List the files in the build directory.
129
130 Only lists files a single level into the build directory. Includes
131 timestamp information in the listing.
132
133 Returns:
134 A string with information about the files in the build directory.
135 None if the build directory doesn't exist.
136
137 Raises:
138 build_artifact.ArtifactDownloadError: If the build_dir path exists
139 but is not a directory.
140 """
141 if not os.path.exists(self._build_dir):
142 return None
143 if not os.path.isdir(self._build_dir):
144 raise build_artifact.ArtifactDownloadError(
145 'Artifacts %s improperly staged to build_dir path %s. The path is '
146 'not a directory.' % (self._archive_url, self._build_dir))
147
148 ls_format = collections.namedtuple(
Gabe Black3b567202015-09-23 14:07:59 -0700149 'ls', ['name', 'accessed', 'modified', 'size'])
Prashanth Ba06d2d22014-03-07 15:35:19 -0800150 output_format = ('Name: %(name)s Accessed: %(accessed)s '
Gabe Black3b567202015-09-23 14:07:59 -0700151 'Modified: %(modified)s Size: %(size)s bytes.\n')
Prashanth Ba06d2d22014-03-07 15:35:19 -0800152
153 build_dir_info = 'Listing contents of :%s \n' % self._build_dir
154 for file_name in os.listdir(self._build_dir):
155 file_path = os.path.join(self._build_dir, file_name)
156 file_info = os.stat(file_path)
157 ls_info = ls_format(file_path,
158 datetime.fromtimestamp(file_info.st_atime),
159 datetime.fromtimestamp(file_info.st_mtime),
160 file_info.st_size)
161 build_dir_info += output_format % ls_info._asdict()
162 return build_dir_info
163
Gabe Black3b567202015-09-23 14:07:59 -0700164 def Download(self, factory, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800165 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700166
Gabe Black3b567202015-09-23 14:07:59 -0700167 Downloads and caches the |artifacts|. Returns once these are present on the
168 devserver. A call to this will attempt to cache non-specified artifacts in
169 the background following the principle of spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700170
Chris Sosa75490802013-09-30 17:21:45 -0700171 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700172 factory: The artifact factory.
Chris Sosa75490802013-09-30 17:21:45 -0700173 async: If True, return without waiting for download to complete.
174
175 Raises:
Gilad Arnold02dc6552013-11-14 11:27:54 -0800176 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700177 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800178 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700179
Chris Sosa76e44b92013-01-31 12:11:38 -0800180 # We are doing some work on this build -- let's touch it to indicate that
181 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 14:32:01 -0700182 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700183
Chris Sosa76e44b92013-01-31 12:11:38 -0800184 # Create factory to create build_artifacts from artifact names.
Chris Sosa76e44b92013-01-31 12:11:38 -0800185 background_artifacts = factory.OptionalArtifacts()
186 if background_artifacts:
187 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700188
Chris Sosa76e44b92013-01-31 12:11:38 -0800189 required_artifacts = factory.RequiredArtifacts()
190 str_repr = [str(a) for a in required_artifacts]
191 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700192
Dan Shi6e50c722013-08-19 15:05:06 -0700193 if async:
194 self._DownloadArtifactsInBackground(required_artifacts)
195 else:
196 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800197
Gabe Black3b567202015-09-23 14:07:59 -0700198 def IsStaged(self, factory):
Dan Shif8eb0d12013-08-01 17:52:06 -0700199 """Check if all artifacts have been downloaded.
200
Gabe Black3b567202015-09-23 14:07:59 -0700201 Args:
202 factory: An instance of BaseArtifactFactory to be used to check if desired
203 artifacts or files are staged.
204
205 Returns:
206 True if all artifacts are staged.
207
208 Raises:
209 DownloaderException: A wrapper for exceptions raised by any artifact when
210 calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700211 """
Dan Shif8eb0d12013-08-01 17:52:06 -0700212 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700213 exceptions = [artifact.GetException() for artifact in required_artifacts if
214 artifact.GetException()]
215 if exceptions:
216 raise DownloaderException(exceptions)
217
Dan Shif8eb0d12013-08-01 17:52:06 -0700218 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
219
Chris Sosa76e44b92013-01-31 12:11:38 -0800220 def _DownloadArtifactsSerially(self, artifacts, no_wait):
221 """Simple function to download all the given artifacts serially.
222
Chris Sosa75490802013-09-30 17:21:45 -0700223 Args:
224 artifacts: A list of build_artifact.BuildArtifact instances to
225 download.
226 no_wait: If True, don't block waiting for artifact to exist if we
227 fail to immediately find it.
228
229 Raises:
230 build_artifact.ArtifactDownloadError: If we failed to download the
231 artifact.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700232 """
Dan Shi6e50c722013-08-19 15:05:06 -0700233 try:
234 for artifact in artifacts:
Gabe Black3b567202015-09-23 14:07:59 -0700235 artifact.Process(self, no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800236 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700237 Downloader._TryRemoveStageDir(self._build_dir)
238 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700239
Chris Sosa76e44b92013-01-31 12:11:38 -0800240 def _DownloadArtifactsInBackground(self, artifacts):
241 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700242
Chris Sosa76e44b92013-01-31 12:11:38 -0800243 Downloads |artifacts| in the background. As these are backgrounded
244 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700245
Chris Sosa76e44b92013-01-31 12:11:38 -0800246 Args:
247 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700248 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800249 self._Log('Invoking background download of artifacts for %r', artifacts)
250 thread = threading.Thread(target=self._DownloadArtifactsSerially,
251 args=(artifacts, False))
252 thread.start()
Gabe Black3b567202015-09-23 14:07:59 -0700253
254 def Wait(self, name, is_regex_name, timeout):
255 """Waits for artifact to exist and returns the appropriate names.
256
257 Args:
258 name: Name to look at.
259 is_regex_name: True if the name is a regex pattern.
260 timeout: How long to wait for the artifact to become available.
261
262 Returns:
263 A list of names that match.
264 """
265 raise NotImplementedError()
266
267 def Fetch(self, remote_name, local_path):
268 """Downloads artifact from given source to a local directory.
269
270 Args:
271 remote_name: Remote name of the file to fetch.
272 local_path: Local path to the folder to store fetched file.
273
274 Returns:
275 The path to fetched file.
276 """
277 raise NotImplementedError()
278
279 def DescribeSource(self):
280 """Gets the source of the download, e.g., a url to GS."""
281 raise NotImplementedError()
282
283
284class GoogleStorageDownloader(Downloader):
285 """Downloader of images to the devserver from Google Storage.
286
287 Given a URL to a build on the archive server:
288 - Caches that build and the given artifacts onto the devserver.
289 - May also initiate caching of related artifacts in the background.
290
291 This is intended to be used with ChromeOS.
292
293 Private class members:
294 archive_url: Google Storage URL to download build artifacts from.
295 """
296
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700297 def __init__(self, static_dir, archive_url, build_id):
298 build = build_id.split('/')[-1]
299 build_dir = os.path.join(static_dir, build_id)
Gabe Black3b567202015-09-23 14:07:59 -0700300
301 super(GoogleStorageDownloader, self).__init__(static_dir, build_dir, build)
302
303 self._archive_url = archive_url
304
xixuan178263c2017-03-22 09:10:25 -0700305 if common_util.IsRunningOnMoblab():
306 self._ctx = gs.GSContext(cache_user='chronos') if gs else None
307 else:
308 self._ctx = gs.GSContext() if gs else None
xixuan44b55452016-09-06 15:35:56 -0700309
Gabe Black3b567202015-09-23 14:07:59 -0700310 def Wait(self, name, is_regex_name, timeout):
311 """Waits for artifact to exist and returns the appropriate names.
312
313 Args:
314 name: Name to look at.
315 is_regex_name: True if the name is a regex pattern.
316 timeout: How long to wait for the artifact to become available.
317
318 Returns:
319 A list of names that match.
320
321 Raises:
322 ArtifactDownloadError: An error occurred when obtaining artifact.
323 """
xixuan44b55452016-09-06 15:35:56 -0700324 names = self._ctx.GetGsNamesWithWait(
325 name, self._archive_url, timeout=timeout,
Gabe Black3b567202015-09-23 14:07:59 -0700326 is_regex_pattern=is_regex_name)
327 if not names:
328 raise build_artifact.ArtifactDownloadError(
329 'Could not find %s in Google Storage at %s' %
330 (name, self._archive_url))
331 return names
332
333 def Fetch(self, remote_name, local_path):
334 """Downloads artifact from Google Storage to a local directory."""
335 install_path = os.path.join(local_path, remote_name)
336 gs_path = '/'.join([self._archive_url, remote_name])
xixuan44b55452016-09-06 15:35:56 -0700337 self._ctx.Copy(gs_path, local_path)
Gabe Black3b567202015-09-23 14:07:59 -0700338 return install_path
339
340 def DescribeSource(self):
341 return self._archive_url
342
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700343 @staticmethod
344 def GetBuildIdFromArchiveURL(archive_url):
345 """Extracts the build ID from the archive URL.
346
347 The archive_url is of the form gs://server/[some_path/target]/...]/build
348 This function discards 'gs://server/' and extracts the [some_path/target]
349 as rel_path and the build as build.
350 """
351 sub_url = archive_url.partition('://')[2]
352 split_sub_url = sub_url.split('/')
353 return '/'.join(split_sub_url[1:])
354
Gabe Black3b567202015-09-23 14:07:59 -0700355
356class LocalDownloader(Downloader):
357 """Downloader of images to the devserver from local storage.
358
359 Given a local path:
360 - Caches that build and the given artifacts onto the devserver.
361 - May also initiate caching of related artifacts in the background.
362
363 Private class members:
364 archive_params: parameters for where to download build artifacts from.
365 """
366
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800367 def __init__(self, static_dir, source_path, delete_source=False):
368 """Initialize us.
369
370 Args:
371 static_dir: The directory where artifacts are to be staged.
372 source_path: The source path to copy artifacts from.
373 delete_source: If True, delete the source files. This mode is faster than
374 actually copying because it allows us to simply move the files.
375 """
Gabe Black3b567202015-09-23 14:07:59 -0700376 # The local path is of the form /{path to static dir}/{rel_path}/{build}.
377 # local_path must be a subpath of the static directory.
378 self.source_path = source_path
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800379 self._move_files = delete_source
Gabe Black3b567202015-09-23 14:07:59 -0700380 rel_path = os.path.basename(os.path.dirname(source_path))
381 build = os.path.basename(source_path)
382 build_dir = os.path.join(static_dir, rel_path, build)
383
384 super(LocalDownloader, self).__init__(static_dir, build_dir, build)
385
386 def Wait(self, name, is_regex_name, timeout):
387 """Verifies the local artifact exists and returns the appropriate names.
388
389 Args:
390 name: Name to look at.
391 is_regex_name: True if the name is a regex pattern.
392 timeout: How long to wait for the artifact to become available.
393
394 Returns:
395 A list of names that match.
396
397 Raises:
398 ArtifactDownloadError: An error occurred when obtaining artifact.
399 """
Gabe Black3b567202015-09-23 14:07:59 -0700400 if is_regex_name:
401 filter_re = re.compile(name)
Prathmesh Prabhubee63be2018-02-09 23:28:24 -0800402 artifacts = [f for f in os.listdir(self.source_path) if
403 filter_re.match(f)]
Gabe Black3b567202015-09-23 14:07:59 -0700404 else:
Prathmesh Prabhubee63be2018-02-09 23:28:24 -0800405 glob_search = glob.glob(os.path.join(self.source_path, name))
406 artifacts = [os.path.basename(g) for g in glob_search]
407
408 if not artifacts:
409 raise build_artifact.ArtifactDownloadError(
410 'Artifact %s not found at %s(regex_match: %s)'
411 % (name, self.source_path, is_regex_name))
412 return artifacts
Gabe Black3b567202015-09-23 14:07:59 -0700413
414 def Fetch(self, remote_name, local_path):
415 """Downloads artifact from Google Storage to a local directory."""
416 install_path = os.path.join(local_path, remote_name)
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800417 src_path = os.path.join(self.source_path, remote_name)
418 if self._move_files:
419 shutil.move(src_path, install_path)
420 else:
421 shutil.copyfile(src_path, install_path)
Gabe Black3b567202015-09-23 14:07:59 -0700422 return install_path
423
424 def DescribeSource(self):
425 return self.source_path
426
427
Dan Shi72b16132015-10-08 12:10:33 -0700428class AndroidBuildDownloader(Downloader):
429 """Downloader of images to the devserver from Android's build server."""
Gabe Black3b567202015-09-23 14:07:59 -0700430
Dan Shi72b16132015-10-08 12:10:33 -0700431 def __init__(self, static_dir, branch, build_id, target):
432 """Initialize AndroidBuildDownloader.
Gabe Black3b567202015-09-23 14:07:59 -0700433
434 Args:
435 static_dir: Root directory to store the build.
Dan Shi72b16132015-10-08 12:10:33 -0700436 branch: Branch for the build. Download will always verify if the given
437 build id is for the branch.
Gabe Black3b567202015-09-23 14:07:59 -0700438 build_id: Build id of the Android build, e.g., 2155602.
439 target: Target of the Android build, e.g., shamu-userdebug.
440 """
Dan Shi72b16132015-10-08 12:10:33 -0700441 build = '%s/%s/%s' % (branch, target, build_id)
Gabe Black3b567202015-09-23 14:07:59 -0700442 build_dir = os.path.join(static_dir, '', build)
443
Dan Shi72b16132015-10-08 12:10:33 -0700444 self.branch = branch
Gabe Black3b567202015-09-23 14:07:59 -0700445 self.build_id = build_id
446 self.target = target
447
Dan Shi72b16132015-10-08 12:10:33 -0700448 super(AndroidBuildDownloader, self).__init__(static_dir, build_dir, build)
Gabe Black3b567202015-09-23 14:07:59 -0700449
450 def Wait(self, name, is_regex_name, timeout):
451 """Verifies the local artifact exists and returns the appropriate names.
452
453 Args:
454 name: Name to look at.
455 is_regex_name: True if the name is a regex pattern.
456 timeout: How long to wait for the artifact to become available.
457
458 Returns:
459 A list of names that match.
460
461 Raises:
462 ArtifactDownloadError: An error occurred when obtaining artifact.
463 """
Dan Shi72b16132015-10-08 12:10:33 -0700464 artifacts = android_build.BuildAccessor.GetArtifacts(
465 branch=self.branch, build_id=self.build_id, target=self.target)
466
467 names = []
468 for artifact_name in [a['name'] for a in artifacts]:
469 match = (re.match(name, artifact_name) if is_regex_name
470 else name == artifact_name)
471 if match:
472 names.append(artifact_name)
473
474 if not names:
475 raise build_artifact.ArtifactDownloadError(
Dan Shi9ee5dc22017-06-27 11:53:07 -0700476 'No artifact found with given name: %s for %s-%s. All available '
477 'artifacts are: %s' %
478 (name, self.target, self.build_id,
479 ','.join([a['name'] for a in artifacts])))
Dan Shi72b16132015-10-08 12:10:33 -0700480
481 return names
Gabe Black3b567202015-09-23 14:07:59 -0700482
483 def Fetch(self, remote_name, local_path):
Dan Shi72b16132015-10-08 12:10:33 -0700484 """Downloads artifact from Android's build server to a local directory."""
485 dest_file = os.path.join(local_path, remote_name)
486 android_build.BuildAccessor.Download(
487 branch=self.branch, build_id=self.build_id, target=self.target,
488 resource_id=remote_name, dest_file=dest_file)
489 return dest_file
Gabe Black3b567202015-09-23 14:07:59 -0700490
491 def DescribeSource(self):
Dan Shi72b16132015-10-08 12:10:33 -0700492 return '%s/%s/%s/%s' % (android_build.DEFAULT_BUILDER, self.branch,
493 self.target, self.build_id)