gsutil.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env python3
  2. # Copyright 2014 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Run a pinned gsutil."""
  6. import argparse
  7. import base64
  8. import contextlib
  9. import hashlib
  10. import json
  11. import os
  12. import shutil
  13. import subprocess
  14. import sys
  15. import tempfile
  16. import time
  17. try:
  18. import urllib2 as urllib
  19. except ImportError: # For Py3 compatibility
  20. import urllib.request as urllib
  21. import zipfile
  22. GSUTIL_URL = 'https://storage.googleapis.com/pub/'
  23. API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
  24. THIS_DIR = os.path.dirname(os.path.abspath(__file__))
  25. DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
  26. IS_WINDOWS = os.name == 'nt'
  27. VERSION = '4.68'
  28. class InvalidGsutilError(Exception):
  29. pass
  30. def download_gsutil(version, target_dir):
  31. """Downloads gsutil into the target_dir."""
  32. filename = 'gsutil_%s.zip' % version
  33. target_filename = os.path.join(target_dir, filename)
  34. # Check if the target exists already.
  35. if os.path.exists(target_filename):
  36. md5_calc = hashlib.md5()
  37. with open(target_filename, 'rb') as f:
  38. while True:
  39. buf = f.read(4096)
  40. if not buf:
  41. break
  42. md5_calc.update(buf)
  43. local_md5 = md5_calc.hexdigest()
  44. metadata_url = '%s%s' % (API_URL, filename)
  45. metadata = json.load(urllib.urlopen(metadata_url))
  46. remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
  47. if local_md5 == remote_md5:
  48. return target_filename
  49. os.remove(target_filename)
  50. # Do the download.
  51. url = '%s%s' % (GSUTIL_URL, filename)
  52. u = urllib.urlopen(url)
  53. with open(target_filename, 'wb') as f:
  54. while True:
  55. buf = u.read(4096)
  56. if not buf:
  57. break
  58. f.write(buf)
  59. return target_filename
  60. @contextlib.contextmanager
  61. def temporary_directory(base):
  62. tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
  63. try:
  64. yield tmpdir
  65. finally:
  66. if os.path.isdir(tmpdir):
  67. shutil.rmtree(tmpdir)
  68. def ensure_gsutil(version, target, clean):
  69. bin_dir = os.path.join(target, 'gsutil_%s' % version)
  70. gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
  71. gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
  72. # We assume that if gsutil_flag exists, then we have a good version
  73. # of the gsutil package.
  74. if not clean and os.path.isfile(gsutil_flag):
  75. # Everything is awesome! we're all done here.
  76. return gsutil_bin
  77. if not os.path.exists(target):
  78. try:
  79. os.makedirs(target)
  80. except FileExistsError:
  81. # Another process is prepping workspace, so let's check if gsutil_bin is
  82. # present. If after several checks it's still not, continue with
  83. # downloading gsutil.
  84. delay = 2 # base delay, in seconds
  85. for _ in range(3): # make N attempts
  86. # sleep first as it's not expected to have file ready just yet.
  87. time.sleep(delay)
  88. delay *= 1.5 # next delay increased by that factor
  89. if os.path.isfile(gsutil_bin):
  90. return gsutil_bin
  91. with temporary_directory(target) as instance_dir:
  92. # Clean up if we're redownloading a corrupted gsutil.
  93. cleanup_path = os.path.join(instance_dir, 'clean')
  94. try:
  95. os.rename(bin_dir, cleanup_path)
  96. except (OSError, IOError):
  97. cleanup_path = None
  98. if cleanup_path:
  99. shutil.rmtree(cleanup_path)
  100. download_dir = os.path.join(instance_dir, 'd')
  101. target_zip_filename = download_gsutil(version, instance_dir)
  102. with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
  103. target_zip.extractall(download_dir)
  104. shutil.move(download_dir, bin_dir)
  105. # Final check that the gsutil bin exists. This should never fail.
  106. if not os.path.isfile(gsutil_bin):
  107. raise InvalidGsutilError()
  108. # Drop a flag file.
  109. with open(gsutil_flag, 'w') as f:
  110. f.write('This flag file is dropped by gsutil.py')
  111. return gsutil_bin
  112. def run_gsutil(target, args, clean=False):
  113. gsutil_bin = ensure_gsutil(VERSION, target, clean)
  114. args_opt = ['-o', 'GSUtil:software_update_check_period=0']
  115. if sys.platform == 'darwin':
  116. # We are experiencing problems with multiprocessing on MacOS where gsutil.py
  117. # may hang.
  118. # This behavior is documented in gsutil codebase, and recommendation is to
  119. # set GSUtil:parallel_process_count=1.
  120. # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
  121. # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
  122. args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
  123. if sys.platform == 'cygwin':
  124. # This script requires Windows Python, so invoke with depot_tools'
  125. # Python.
  126. def winpath(path):
  127. stdout = subprocess.check_output(['cygpath', '-w', path])
  128. return stdout.strip().decode('utf-8', 'replace')
  129. cmd = ['python.bat', winpath(__file__)]
  130. cmd.extend(args)
  131. sys.exit(subprocess.call(cmd))
  132. assert sys.platform != 'cygwin'
  133. cmd = [
  134. 'vpython3',
  135. '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython3'),
  136. '--',
  137. gsutil_bin
  138. ] + args_opt + args
  139. return subprocess.call(cmd, shell=IS_WINDOWS)
  140. def parse_args():
  141. bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
  142. # Help is disabled as it conflicts with gsutil -h, which controls headers.
  143. parser = argparse.ArgumentParser(add_help=False)
  144. parser.add_argument('--clean', action='store_true',
  145. help='Clear any existing gsutil package, forcing a new download.')
  146. parser.add_argument('--target', default=bin_dir,
  147. help='The target directory to download/store a gsutil version in. '
  148. '(default is %(default)s).')
  149. # These two args exist for backwards-compatibility but are no-ops.
  150. parser.add_argument('--force-version', default=VERSION,
  151. help='(deprecated, this flag has no effect)')
  152. parser.add_argument('--fallback',
  153. help='(deprecated, this flag has no effect)')
  154. parser.add_argument('args', nargs=argparse.REMAINDER)
  155. args, extras = parser.parse_known_args()
  156. if args.args and args.args[0] == '--':
  157. args.args.pop(0)
  158. if extras:
  159. args.args = extras + args.args
  160. return args
  161. def main():
  162. args = parse_args()
  163. return run_gsutil(args.target, args.args, clean=args.clean)
  164. if __name__ == '__main__':
  165. sys.exit(main())