gsutil.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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. DEFAULT_FALLBACK_GSUTIL = os.path.join(
  27. THIS_DIR, 'third_party', 'gsutil', 'gsutil')
  28. IS_WINDOWS = os.name == 'nt'
  29. class InvalidGsutilError(Exception):
  30. pass
  31. def download_gsutil(version, target_dir):
  32. """Downloads gsutil into the target_dir."""
  33. filename = 'gsutil_%s.zip' % version
  34. target_filename = os.path.join(target_dir, filename)
  35. # Check if the target exists already.
  36. if os.path.exists(target_filename):
  37. md5_calc = hashlib.md5()
  38. with open(target_filename, 'rb') as f:
  39. while True:
  40. buf = f.read(4096)
  41. if not buf:
  42. break
  43. md5_calc.update(buf)
  44. local_md5 = md5_calc.hexdigest()
  45. metadata_url = '%s%s' % (API_URL, filename)
  46. metadata = json.load(urllib.urlopen(metadata_url))
  47. remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
  48. if local_md5 == remote_md5:
  49. return target_filename
  50. os.remove(target_filename)
  51. # Do the download.
  52. url = '%s%s' % (GSUTIL_URL, filename)
  53. u = urllib.urlopen(url)
  54. with open(target_filename, 'wb') as f:
  55. while True:
  56. buf = u.read(4096)
  57. if not buf:
  58. break
  59. f.write(buf)
  60. return target_filename
  61. @contextlib.contextmanager
  62. def temporary_directory(base):
  63. tmpdir = tempfile.mkdtemp(prefix='gsutil_py', dir=base)
  64. try:
  65. yield tmpdir
  66. finally:
  67. if os.path.isdir(tmpdir):
  68. shutil.rmtree(tmpdir)
  69. def ensure_gsutil(version, target, clean):
  70. bin_dir = os.path.join(target, 'gsutil_%s' % version)
  71. gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
  72. gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
  73. # We assume that if gsutil_flag exists, then we have a good version
  74. # of the gsutil package.
  75. if not clean and os.path.isfile(gsutil_flag):
  76. # Everything is awesome! we're all done here.
  77. return gsutil_bin
  78. if not os.path.exists(target):
  79. os.makedirs(target)
  80. with temporary_directory(target) as instance_dir:
  81. # Clean up if we're redownloading a corrupted gsutil.
  82. cleanup_path = os.path.join(instance_dir, 'clean')
  83. try:
  84. os.rename(bin_dir, cleanup_path)
  85. except (OSError, IOError):
  86. cleanup_path = None
  87. if cleanup_path:
  88. shutil.rmtree(cleanup_path)
  89. download_dir = os.path.join(instance_dir, 'download')
  90. target_zip_filename = download_gsutil(version, instance_dir)
  91. with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
  92. target_zip.extractall(download_dir)
  93. try:
  94. os.rename(download_dir, bin_dir)
  95. except (OSError, IOError):
  96. # Something else did this in parallel.
  97. pass
  98. # Final check that the gsutil bin exists. This should never fail.
  99. if not os.path.isfile(gsutil_bin):
  100. raise InvalidGsutilError()
  101. # Drop a flag file.
  102. with open(gsutil_flag, 'w') as f:
  103. f.write('This flag file is dropped by gsutil.py')
  104. return gsutil_bin
  105. def run_gsutil(force_version, fallback, target, args, clean=False):
  106. if force_version:
  107. gsutil_bin = ensure_gsutil(force_version, target, clean)
  108. else:
  109. gsutil_bin = fallback
  110. disable_update = ['-o', 'GSUtil:software_update_check_period=0']
  111. if sys.platform == 'cygwin':
  112. # This script requires Windows Python, so invoke with depot_tools'
  113. # Python.
  114. def winpath(path):
  115. stdout = subprocess.check_output(['cygpath', '-w', path])
  116. return stdout.strip().decode('utf-8', 'replace')
  117. cmd = ['python.bat', winpath(__file__)]
  118. cmd.extend(args)
  119. sys.exit(subprocess.call(cmd))
  120. assert sys.platform != 'cygwin'
  121. # Run "gsutil" through "vpython". We need to do this because on GCE instances,
  122. # expectations are made about Python having access to "google-compute-engine"
  123. # and "boto" packages that are not met with non-system Python (e.g., bundles).
  124. cmd = [
  125. 'vpython',
  126. '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython'),
  127. '--',
  128. gsutil_bin
  129. ] + disable_update + args
  130. return subprocess.call(cmd, shell=IS_WINDOWS)
  131. def parse_args():
  132. bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
  133. parser = argparse.ArgumentParser()
  134. parser.add_argument('--force-version', default='4.30')
  135. parser.add_argument('--clean', action='store_true',
  136. help='Clear any existing gsutil package, forcing a new download.')
  137. parser.add_argument('--fallback', default=DEFAULT_FALLBACK_GSUTIL)
  138. parser.add_argument('--target', default=bin_dir,
  139. help='The target directory to download/store a gsutil version in. '
  140. '(default is %(default)s).')
  141. parser.add_argument('args', nargs=argparse.REMAINDER)
  142. args, extras = parser.parse_known_args()
  143. if args.args and args.args[0] == '--':
  144. args.args.pop(0)
  145. if extras:
  146. args.args = extras + args.args
  147. return args
  148. def main():
  149. args = parse_args()
  150. return run_gsutil(args.force_version, args.fallback, args.target, args.args,
  151. clean=args.clean)
  152. if __name__ == '__main__':
  153. sys.exit(main())