blob: 8afdda97443b06dddac69759d90bcd1230f503c2 [file] [log] [blame]
xixuan44b55452016-09-06 22:35:561#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 19:56:302# -*- coding: utf-8 -*-
Chris Sosa76e44b92013-01-31 20:11:383# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 22:29:084# 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 21:07:597"""Downloaders used to download artifacts and files from a given source."""
8
9from __future__ import print_function
10
Prashanth Ba06d2d22014-03-07 23:35:1911import collections
Eric Carusoe76d1872019-02-21 19:17:4512import errno
Gabe Black3b567202015-09-23 21:07:5913import glob
Chris Sosa9164ca32012-03-28 18:04:5014import os
Gabe Black3b567202015-09-23 21:07:5915import re
16import shutil
Eric Carusoe76d1872019-02-21 19:17:4517import subprocess
Gilad Arnold0b8c3f32012-09-19 21:35:4418import threading
Prashanth Ba06d2d22014-03-07 23:35:1919from datetime import datetime
Frank Farzan37761d12011-12-01 22:29:0820
Chris Sosa76e44b92013-01-31 20:11:3821import build_artifact
Gilad Arnoldc65330c2012-09-20 22:17:4822import common_util
23import log_util
Frank Farzan37761d12011-12-01 22:29:0824
xixuan44b55452016-09-06 22:35:5625# Make sure that chromite is available to import.
26import setup_chromite # pylint: disable=unused-import
27
28try:
29 from chromite.lib import gs
30except ImportError as e:
31 gs = None
32
Dan Shi72b16132015-10-08 19:10:3333try:
34 import android_build
35except ImportError as e:
36 # Ignore android_build import failure. This is to support devserver running
37 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
38 # do not have google-api-python-client module and they don't need to support
39 # Android updating, therefore, ignore the import failure here.
40 android_build = None
41
Frank Farzan37761d12011-12-01 22:29:0842
Dan Shi6e50c722013-08-19 22:05:0643class DownloaderException(Exception):
44 """Exception that aggregates all exceptions raised during async download.
45
46 Exceptions could be raised in artifact.Process method, and saved to files.
47 When caller calls IsStaged to check the downloading progress, devserver can
48 retrieve the persisted exceptions from the files, wrap them into a
49 DownloaderException, and raise it.
50 """
51 def __init__(self, exceptions):
52 """Initialize a DownloaderException instance with a list of exceptions.
53
Gabe Black3b567202015-09-23 21:07:5954 Args:
55 exceptions: Exceptions raised when downloading artifacts.
Dan Shi6e50c722013-08-19 22:05:0656 """
57 message = 'Exceptions were raised when downloading artifacts.'
58 Exception.__init__(self, message)
59 self.exceptions = exceptions
60
61 def __repr__(self):
62 return self.__str__()
63
64 def __str__(self):
65 """Return a custom exception message with all exceptions merged."""
66 return '--------\n'.join([str(exception) for exception in self.exceptions])
67
Gilad Arnoldc65330c2012-09-20 22:17:4868class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 20:11:3869 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 22:29:0870
Gabe Black3b567202015-09-23 21:07:5971 This is the base class for different types of downloaders, including
Dan Shi72b16132015-10-08 19:10:3372 GoogleStorageDownloader, LocalDownloader and AndroidBuildDownloader.
Gabe Black3b567202015-09-23 21:07:5973
Frank Farzan37761d12011-12-01 22:29:0874 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 20:11:3875 - Caches that build and the given artifacts onto the devserver.
76 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 22:29:0877
Chris Sosa76e44b92013-01-31 20:11:3878 Private class members:
Chris Sosa76e44b92013-01-31 20:11:3879 static_dir: local filesystem directory to store all artifacts.
80 build_dir: the local filesystem directory to store artifacts for the given
Gabe Black3b567202015-09-23 21:07:5981 build based on the remote source.
82
83 Public methods must be overridden:
84 Wait: Verifies the local artifact exists and returns the appropriate names.
85 Fetch: Downloads artifact from given source to a local directory.
86 DescribeSource: Gets the source of the download, e.g., a url to GS.
Frank Farzan37761d12011-12-01 22:29:0887 """
88
Alex Millera44d5022012-07-27 18:34:1689 # This filename must be kept in sync with clean_staged_images.py
90 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 19:38:5191
Gabe Black3b567202015-09-23 21:07:5992 def __init__(self, static_dir, build_dir, build):
Chris Sosa76e44b92013-01-31 20:11:3893 super(Downloader, self).__init__()
Frank Farzan37761d12011-12-01 22:29:0894 self._static_dir = static_dir
Gabe Black3b567202015-09-23 21:07:5995 self._build_dir = build_dir
96 self._build = build
Chris Masone816e38c2012-05-02 19:22:3697
Gabe Black3b567202015-09-23 21:07:5998 def GetBuildDir(self):
99 """Returns the path to where the artifacts will be staged."""
100 return self._build_dir
Simran Basi4243a862014-12-12 20:48:33101
Gabe Black3b567202015-09-23 21:07:59102 def GetBuild(self):
103 """Returns the path to where the artifacts will be staged."""
104 return self._build
Frank Farzan37761d12011-12-01 22:29:08105
Chris Sosa9164ca32012-03-28 18:04:50106 @staticmethod
Simran Basief83d6a2014-08-28 21:32:01107 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 18:34:16108 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
109 # Easiest python version of |touch file_name|
110 with file(file_name, 'a'):
111 os.utime(file_name, None)
112
Dan Shiba0e6742013-06-27 00:39:05113 @staticmethod
114 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 19:27:54115 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-27 00:39:05116
Gilad Arnold02dc6552013-11-14 19:27:54117 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
118 is the only file in that directory. The build could be non-existing, and
119 the directory should be removed.
Dan Shiba0e6742013-06-27 00:39:05120
Gabe Black3b567202015-09-23 21:07:59121 Args:
122 directory_path: directory used to stage the image.
Dan Shiba0e6742013-06-27 00:39:05123 """
124 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
125 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
126 os.remove(file_name)
127 os.rmdir(directory_path)
128
Prashanth Ba06d2d22014-03-07 23:35:19129 def ListBuildDir(self):
130 """List the files in the build directory.
131
132 Only lists files a single level into the build directory. Includes
133 timestamp information in the listing.
134
135 Returns:
136 A string with information about the files in the build directory.
137 None if the build directory doesn't exist.
138
139 Raises:
140 build_artifact.ArtifactDownloadError: If the build_dir path exists
141 but is not a directory.
142 """
143 if not os.path.exists(self._build_dir):
144 return None
145 if not os.path.isdir(self._build_dir):
146 raise build_artifact.ArtifactDownloadError(
147 'Artifacts %s improperly staged to build_dir path %s. The path is '
148 'not a directory.' % (self._archive_url, self._build_dir))
149
150 ls_format = collections.namedtuple(
Gabe Black3b567202015-09-23 21:07:59151 'ls', ['name', 'accessed', 'modified', 'size'])
Prashanth Ba06d2d22014-03-07 23:35:19152 output_format = ('Name: %(name)s Accessed: %(accessed)s '
Gabe Black3b567202015-09-23 21:07:59153 'Modified: %(modified)s Size: %(size)s bytes.\n')
Prashanth Ba06d2d22014-03-07 23:35:19154
155 build_dir_info = 'Listing contents of :%s \n' % self._build_dir
156 for file_name in os.listdir(self._build_dir):
157 file_path = os.path.join(self._build_dir, file_name)
158 file_info = os.stat(file_path)
159 ls_info = ls_format(file_path,
160 datetime.fromtimestamp(file_info.st_atime),
161 datetime.fromtimestamp(file_info.st_mtime),
162 file_info.st_size)
163 build_dir_info += output_format % ls_info._asdict()
164 return build_dir_info
165
Gabe Black3b567202015-09-23 21:07:59166 def Download(self, factory, async=False):
Chris Sosa76e44b92013-01-31 20:11:38167 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 18:04:50168
Gabe Black3b567202015-09-23 21:07:59169 Downloads and caches the |artifacts|. Returns once these are present on the
170 devserver. A call to this will attempt to cache non-specified artifacts in
171 the background following the principle of spatial locality.
Gilad Arnold6f99b982012-09-12 17:49:40172
Chris Sosa75490802013-10-01 00:21:45173 Args:
Eric Carusoe76d1872019-02-21 19:17:45174 factory: The artifact factory.
175 async: If True, return without waiting for download to complete.
Chris Sosa75490802013-10-01 00:21:45176
177 Raises:
Gilad Arnold02dc6552013-11-14 19:27:54178 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Gilad Arnold6f99b982012-09-12 17:49:40179 """
Eric Carusoe76d1872019-02-21 19:17:45180 try:
181 common_util.MkDirP(self._build_dir)
182 except OSError as e:
183 if e.errno != errno.EACCES:
184 raise
185 self._Log('Could not create build dir due to permissions issue. '
186 'Attempting to fix permissions.')
187 subprocess.Popen(['sudo', 'chown', '-R',
188 '%s:%s' % (os.getuid(), os.getgid()),
189 self._static_dir]).wait()
190 # Then try to create the build dir again.
191 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 17:49:40192
Chris Sosa76e44b92013-01-31 20:11:38193 # We are doing some work on this build -- let's touch it to indicate that
194 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 21:32:01195 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 17:49:40196
Chris Sosa76e44b92013-01-31 20:11:38197 # Create factory to create build_artifacts from artifact names.
Chris Sosa76e44b92013-01-31 20:11:38198 background_artifacts = factory.OptionalArtifacts()
199 if background_artifacts:
200 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 17:49:40201
Chris Sosa76e44b92013-01-31 20:11:38202 required_artifacts = factory.RequiredArtifacts()
203 str_repr = [str(a) for a in required_artifacts]
204 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 23:10:29205
Dan Shi6e50c722013-08-19 22:05:06206 if async:
207 self._DownloadArtifactsInBackground(required_artifacts)
208 else:
209 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 20:11:38210
Gabe Black3b567202015-09-23 21:07:59211 def IsStaged(self, factory):
Dan Shif8eb0d12013-08-02 00:52:06212 """Check if all artifacts have been downloaded.
213
Gabe Black3b567202015-09-23 21:07:59214 Args:
215 factory: An instance of BaseArtifactFactory to be used to check if desired
216 artifacts or files are staged.
217
218 Returns:
219 True if all artifacts are staged.
220
221 Raises:
222 DownloaderException: A wrapper for exceptions raised by any artifact when
223 calling Process.
Dan Shif8eb0d12013-08-02 00:52:06224 """
Dan Shif8eb0d12013-08-02 00:52:06225 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 22:05:06226 exceptions = [artifact.GetException() for artifact in required_artifacts if
227 artifact.GetException()]
228 if exceptions:
229 raise DownloaderException(exceptions)
230
Dan Shif8eb0d12013-08-02 00:52:06231 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
232
Chris Sosa76e44b92013-01-31 20:11:38233 def _DownloadArtifactsSerially(self, artifacts, no_wait):
234 """Simple function to download all the given artifacts serially.
235
Chris Sosa75490802013-10-01 00:21:45236 Args:
237 artifacts: A list of build_artifact.BuildArtifact instances to
238 download.
239 no_wait: If True, don't block waiting for artifact to exist if we
240 fail to immediately find it.
241
242 Raises:
243 build_artifact.ArtifactDownloadError: If we failed to download the
244 artifact.
Gilad Arnold6f99b982012-09-12 17:49:40245 """
Dan Shi6e50c722013-08-19 22:05:06246 try:
247 for artifact in artifacts:
Gabe Black3b567202015-09-23 21:07:59248 artifact.Process(self, no_wait)
Gilad Arnold02dc6552013-11-14 19:27:54249 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 22:05:06250 Downloader._TryRemoveStageDir(self._build_dir)
251 raise
Gilad Arnold6f99b982012-09-12 17:49:40252
Chris Sosa76e44b92013-01-31 20:11:38253 def _DownloadArtifactsInBackground(self, artifacts):
254 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 17:49:40255
Chris Sosa76e44b92013-01-31 20:11:38256 Downloads |artifacts| in the background. As these are backgrounded
257 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 17:49:40258
Chris Sosa76e44b92013-01-31 20:11:38259 Args:
260 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 17:49:40261 """
Chris Sosa76e44b92013-01-31 20:11:38262 self._Log('Invoking background download of artifacts for %r', artifacts)
263 thread = threading.Thread(target=self._DownloadArtifactsSerially,
264 args=(artifacts, False))
265 thread.start()
Gabe Black3b567202015-09-23 21:07:59266
267 def Wait(self, name, is_regex_name, timeout):
268 """Waits for artifact to exist and returns the appropriate names.
269
270 Args:
271 name: Name to look at.
272 is_regex_name: True if the name is a regex pattern.
273 timeout: How long to wait for the artifact to become available.
274
275 Returns:
276 A list of names that match.
277 """
278 raise NotImplementedError()
279
280 def Fetch(self, remote_name, local_path):
281 """Downloads artifact from given source to a local directory.
282
283 Args:
284 remote_name: Remote name of the file to fetch.
285 local_path: Local path to the folder to store fetched file.
286
287 Returns:
288 The path to fetched file.
289 """
290 raise NotImplementedError()
291
292 def DescribeSource(self):
293 """Gets the source of the download, e.g., a url to GS."""
294 raise NotImplementedError()
295
296
297class GoogleStorageDownloader(Downloader):
298 """Downloader of images to the devserver from Google Storage.
299
300 Given a URL to a build on the archive server:
301 - Caches that build and the given artifacts onto the devserver.
302 - May also initiate caching of related artifacts in the background.
303
304 This is intended to be used with ChromeOS.
305
306 Private class members:
307 archive_url: Google Storage URL to download build artifacts from.
308 """
309
Luis Hector Chavezdca9dd72018-06-12 19:56:30310 def __init__(self, static_dir, archive_url, build_id):
311 build = build_id.split('/')[-1]
312 build_dir = os.path.join(static_dir, build_id)
Gabe Black3b567202015-09-23 21:07:59313
314 super(GoogleStorageDownloader, self).__init__(static_dir, build_dir, build)
315
316 self._archive_url = archive_url
317
xixuan178263c2017-03-22 16:10:25318 if common_util.IsRunningOnMoblab():
319 self._ctx = gs.GSContext(cache_user='chronos') if gs else None
320 else:
321 self._ctx = gs.GSContext() if gs else None
xixuan44b55452016-09-06 22:35:56322
Gabe Black3b567202015-09-23 21:07:59323 def Wait(self, name, is_regex_name, timeout):
324 """Waits for artifact to exist and returns the appropriate names.
325
326 Args:
327 name: Name to look at.
328 is_regex_name: True if the name is a regex pattern.
329 timeout: How long to wait for the artifact to become available.
330
331 Returns:
332 A list of names that match.
333
334 Raises:
335 ArtifactDownloadError: An error occurred when obtaining artifact.
336 """
xixuan44b55452016-09-06 22:35:56337 names = self._ctx.GetGsNamesWithWait(
338 name, self._archive_url, timeout=timeout,
Gabe Black3b567202015-09-23 21:07:59339 is_regex_pattern=is_regex_name)
340 if not names:
341 raise build_artifact.ArtifactDownloadError(
342 'Could not find %s in Google Storage at %s' %
343 (name, self._archive_url))
344 return names
345
346 def Fetch(self, remote_name, local_path):
347 """Downloads artifact from Google Storage to a local directory."""
348 install_path = os.path.join(local_path, remote_name)
349 gs_path = '/'.join([self._archive_url, remote_name])
xixuan44b55452016-09-06 22:35:56350 self._ctx.Copy(gs_path, local_path)
Gabe Black3b567202015-09-23 21:07:59351 return install_path
352
353 def DescribeSource(self):
354 return self._archive_url
355
Luis Hector Chavezdca9dd72018-06-12 19:56:30356 @staticmethod
357 def GetBuildIdFromArchiveURL(archive_url):
358 """Extracts the build ID from the archive URL.
359
360 The archive_url is of the form gs://server/[some_path/target]/...]/build
361 This function discards 'gs://server/' and extracts the [some_path/target]
362 as rel_path and the build as build.
363 """
364 sub_url = archive_url.partition('://')[2]
365 split_sub_url = sub_url.split('/')
366 return '/'.join(split_sub_url[1:])
367
Gabe Black3b567202015-09-23 21:07:59368
369class LocalDownloader(Downloader):
370 """Downloader of images to the devserver from local storage.
371
372 Given a local path:
373 - Caches that build and the given artifacts onto the devserver.
374 - May also initiate caching of related artifacts in the background.
375
376 Private class members:
377 archive_params: parameters for where to download build artifacts from.
378 """
379
Prathmesh Prabhu58d08932018-01-19 23:08:19380 def __init__(self, static_dir, source_path, delete_source=False):
381 """Initialize us.
382
383 Args:
384 static_dir: The directory where artifacts are to be staged.
385 source_path: The source path to copy artifacts from.
386 delete_source: If True, delete the source files. This mode is faster than
387 actually copying because it allows us to simply move the files.
388 """
Gabe Black3b567202015-09-23 21:07:59389 # The local path is of the form /{path to static dir}/{rel_path}/{build}.
390 # local_path must be a subpath of the static directory.
391 self.source_path = source_path
Prathmesh Prabhu58d08932018-01-19 23:08:19392 self._move_files = delete_source
Gabe Black3b567202015-09-23 21:07:59393 rel_path = os.path.basename(os.path.dirname(source_path))
394 build = os.path.basename(source_path)
395 build_dir = os.path.join(static_dir, rel_path, build)
396
397 super(LocalDownloader, self).__init__(static_dir, build_dir, build)
398
399 def Wait(self, name, is_regex_name, timeout):
400 """Verifies the local artifact exists and returns the appropriate names.
401
402 Args:
403 name: Name to look at.
404 is_regex_name: True if the name is a regex pattern.
405 timeout: How long to wait for the artifact to become available.
406
407 Returns:
408 A list of names that match.
409
410 Raises:
411 ArtifactDownloadError: An error occurred when obtaining artifact.
412 """
Gabe Black3b567202015-09-23 21:07:59413 if is_regex_name:
414 filter_re = re.compile(name)
Prathmesh Prabhubee63be2018-02-10 07:28:24415 artifacts = [f for f in os.listdir(self.source_path) if
416 filter_re.match(f)]
Gabe Black3b567202015-09-23 21:07:59417 else:
Prathmesh Prabhubee63be2018-02-10 07:28:24418 glob_search = glob.glob(os.path.join(self.source_path, name))
419 artifacts = [os.path.basename(g) for g in glob_search]
420
421 if not artifacts:
422 raise build_artifact.ArtifactDownloadError(
423 'Artifact %s not found at %s(regex_match: %s)'
424 % (name, self.source_path, is_regex_name))
425 return artifacts
Gabe Black3b567202015-09-23 21:07:59426
427 def Fetch(self, remote_name, local_path):
428 """Downloads artifact from Google Storage to a local directory."""
429 install_path = os.path.join(local_path, remote_name)
Prathmesh Prabhu58d08932018-01-19 23:08:19430 src_path = os.path.join(self.source_path, remote_name)
431 if self._move_files:
432 shutil.move(src_path, install_path)
433 else:
434 shutil.copyfile(src_path, install_path)
Gabe Black3b567202015-09-23 21:07:59435 return install_path
436
437 def DescribeSource(self):
438 return self.source_path
439
440
Dan Shi72b16132015-10-08 19:10:33441class AndroidBuildDownloader(Downloader):
442 """Downloader of images to the devserver from Android's build server."""
Gabe Black3b567202015-09-23 21:07:59443
Dan Shi72b16132015-10-08 19:10:33444 def __init__(self, static_dir, branch, build_id, target):
445 """Initialize AndroidBuildDownloader.
Gabe Black3b567202015-09-23 21:07:59446
447 Args:
448 static_dir: Root directory to store the build.
Dan Shi72b16132015-10-08 19:10:33449 branch: Branch for the build. Download will always verify if the given
450 build id is for the branch.
Gabe Black3b567202015-09-23 21:07:59451 build_id: Build id of the Android build, e.g., 2155602.
452 target: Target of the Android build, e.g., shamu-userdebug.
453 """
Dan Shi72b16132015-10-08 19:10:33454 build = '%s/%s/%s' % (branch, target, build_id)
Gabe Black3b567202015-09-23 21:07:59455 build_dir = os.path.join(static_dir, '', build)
456
Dan Shi72b16132015-10-08 19:10:33457 self.branch = branch
Gabe Black3b567202015-09-23 21:07:59458 self.build_id = build_id
459 self.target = target
460
Dan Shi72b16132015-10-08 19:10:33461 super(AndroidBuildDownloader, self).__init__(static_dir, build_dir, build)
Gabe Black3b567202015-09-23 21:07:59462
463 def Wait(self, name, is_regex_name, timeout):
464 """Verifies the local artifact exists and returns the appropriate names.
465
466 Args:
467 name: Name to look at.
468 is_regex_name: True if the name is a regex pattern.
469 timeout: How long to wait for the artifact to become available.
470
471 Returns:
472 A list of names that match.
473
474 Raises:
475 ArtifactDownloadError: An error occurred when obtaining artifact.
476 """
Dan Shi72b16132015-10-08 19:10:33477 artifacts = android_build.BuildAccessor.GetArtifacts(
478 branch=self.branch, build_id=self.build_id, target=self.target)
479
480 names = []
481 for artifact_name in [a['name'] for a in artifacts]:
482 match = (re.match(name, artifact_name) if is_regex_name
483 else name == artifact_name)
484 if match:
485 names.append(artifact_name)
486
487 if not names:
488 raise build_artifact.ArtifactDownloadError(
Dan Shi9ee5dc22017-06-27 18:53:07489 'No artifact found with given name: %s for %s-%s. All available '
490 'artifacts are: %s' %
491 (name, self.target, self.build_id,
492 ','.join([a['name'] for a in artifacts])))
Dan Shi72b16132015-10-08 19:10:33493
494 return names
Gabe Black3b567202015-09-23 21:07:59495
496 def Fetch(self, remote_name, local_path):
Dan Shi72b16132015-10-08 19:10:33497 """Downloads artifact from Android's build server to a local directory."""
498 dest_file = os.path.join(local_path, remote_name)
499 android_build.BuildAccessor.Download(
500 branch=self.branch, build_id=self.build_id, target=self.target,
501 resource_id=remote_name, dest_file=dest_file)
502 return dest_file
Gabe Black3b567202015-09-23 21:07:59503
504 def DescribeSource(self):
Dan Shi72b16132015-10-08 19:10:33505 return '%s/%s/%s/%s' % (android_build.DEFAULT_BUILDER, self.branch,
506 self.target, self.build_id)