Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / paygen / gslib.py
1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Common Google Storage interface library."""
6
7 from __future__ import print_function
8
9 import base64
10 import datetime
11 import errno
12 import logging
13 import os
14 import re
15
16 import fixup_path
17 fixup_path.FixupPath()
18
19 from chromite.lib import gs
20 from chromite.lib import osutils
21 from chromite.lib.paygen import filelib
22 from chromite.lib.paygen import utils
23
24
25 PROTOCOL = 'gs'
26 RETRY_ATTEMPTS = 2
27 GS_LS_STATUS_RE = re.compile(r'status=(\d+)')
28
29 # Gsutil is filled in by "FindGsUtil" on first invocation.
30 GSUTIL = None
31
32
33 def FindGsUtil():
34   """Find which gsutil executuable to use.
35
36   This may download and cache the command if needed, and will return the
37   version pinned by chromite for general use. Will cache the result after
38   the first call.
39
40   This function is multi-process safe, but NOT THREAD SAFE. If you need
41   to use gsutil functionality in threads, call this function at least
42   once before creating the threads. That way the value will be safely
43   pre-cached.
44
45   Returns:
46     Full path to the gsutil command to use.
47   """
48   # TODO(dgarrett): This is a hack. Merge chromite and crostools to fix.
49
50   # pylint: disable=W0603
51   global GSUTIL
52   if GSUTIL is None:
53     GSUTIL = gs.GSContext.GetDefaultGSUtilBin()
54
55   return GSUTIL
56
57
58 class GsutilError(Exception):
59   """Base exception for errors where gsutil cannot be used for any reason."""
60
61
62 class GsutilMissingError(GsutilError):
63   """Returned when the gsutil utility is missing from PATH."""
64   def __init__(self, msg='The gsutil utility must be installed.'):
65     GsutilError.__init__(self, msg)
66
67
68 class GSLibError(Exception):
69   """Raised when gsutil command runs but gives an error."""
70
71
72 class CopyFail(GSLibError):
73   """Raised if Copy fails in any way."""
74
75
76 class MoveFail(GSLibError):
77   """Raised if Move fails in any way."""
78
79
80 class RemoveFail(GSLibError):
81   """Raised if Remove fails in any way."""
82
83
84 class AclFail(GSLibError):
85   """Raised if SetAcl fails in any way."""
86
87
88 class CatFail(GSLibError):
89   """Raised if Cat fails in any way."""
90
91
92 class StatFail(GSLibError):
93   """Raised if Stat fails in any way."""
94
95
96 class BucketOperationError(GSLibError):
97   """Raised when a delete or create bucket command fails."""
98
99
100 class URIError(GSLibError):
101   """Raised when URI does not behave as expected."""
102
103
104 class ValidateGsutilFailure(GSLibError):
105   """We are unable to validate that gsutil is working correctly."""
106
107
108 def RetryGSLib(func):
109   """Decorator to retry function calls that throw an exception.
110
111   If the decorated method throws a GSLibError exception, the exception
112   will be thrown away and the function will be run again until all retries
113   are exhausted. On the final attempt, the exception will be thrown normally.
114
115   Three attempts in total will be made to run the function (one more
116   than RETRY_ATTEMPTS).
117
118   @RetryGSLib
119   def MyFunctionHere(): pass
120   """
121   def RetryHandler(*args, **kwargs):
122     """Retry func with given args/kwargs RETRY_ATTEMPTS times."""
123     warning_msgs = []
124     for i in xrange(0, RETRY_ATTEMPTS + 1):
125       try:
126         return func(*args, **kwargs)
127       except GSLibError as ex:
128         # On the last try just pass the exception on up.
129         if i >= RETRY_ATTEMPTS:
130           raise
131
132         error_msg = str(ex)
133         RESUMABLE_ERROR_MESSAGE = (
134             gs.GSContext.RESUMABLE_DOWNLOAD_ERROR,
135             gs.GSContext.RESUMABLE_UPLOAD_ERROR,
136             'ResumableUploadException',
137             'ResumableDownloadException',
138             'ssl.SSLError: The read operation timed out',
139         )
140         if (func.__name__ == 'Copy' and
141             any(x in error_msg for x in RESUMABLE_ERROR_MESSAGE)):
142           logging.info(
143               'Resumable download/upload exception occured for %s', args[1])
144           # Pass the dest_path to get the tracker filename.
145           tracker_filenames = gs.GSContext.GetTrackerFilenames(args[1])
146           # This part of the code is copied from chromite.lib.gs with
147           # slight modifications. This is a temporary solution until
148           # we can deprecate crostools.lib.gslib (crbug.com/322740).
149           logging.info('Potential list of tracker files: %s',
150                        tracker_filenames)
151           for tracker_filename in tracker_filenames:
152             tracker_file_path = os.path.join(
153                 gs.GSContext.DEFAULT_GSUTIL_TRACKER_DIR,
154                 tracker_filename)
155             if os.path.exists(tracker_file_path):
156               logging.info('Deleting gsutil tracker file %s before retrying.',
157                            tracker_file_path)
158               logging.info('The content of the tracker file: %s',
159                            osutils.ReadFile(tracker_file_path))
160               osutils.SafeUnlink(tracker_file_path)
161         else:
162           if 'AccessDeniedException' in str(ex) or 'NoSuchKey' in str(ex):
163             raise
164
165         # Record a warning message to be issued if a retry actually helps.
166         warning_msgs.append('Try %d failed with error message:\n%s' %
167                             (i + 1, ex))
168       else:
169         # If the func succeeded, then log any accumulated warning messages.
170         if warning_msgs:
171           logging.warning('Failed %s %d times before success:\n%s',
172                           func.__name__, len(warning_msgs),
173                           '\n'.join(warning_msgs))
174
175   RetryHandler.__module__ = func.__module__
176   RetryHandler.__name__ = func.__name__
177   RetryHandler.__doc__ = func.__doc__
178   return RetryHandler
179
180
181 def RunGsutilCommand(args,
182                      redirect_stdout=True,
183                      redirect_stderr=True,
184                      failed_exception=GSLibError,
185                      generation=None,
186                      headers=None,
187                      get_headers_from_stdout=False,
188                      **kwargs):
189   """Run gsutil with given args through RunCommand with given options.
190
191   Generally this method is intended for use within this module, see the various
192   command-specific wrappers provided for convenience.  However, it can be called
193   directly if 'gsutil' needs to be called in specific way.
194
195   A few of the options for RunCommand have their default values switched for
196   this function.  Those options are called out explicitly as options here, while
197   addition RunCommand options can be used through extra_run_command_opts.
198
199   Args:
200     args: List of arguments to use with 'gsutil'.
201     redirect_stdout: Boolean option passed directly to RunCommand.
202     redirect_stderr: Boolean option passed directly to RunCommand.
203     failed_exception: Exception class to raise if CommandFailedException is
204       caught.  It should be GSLibError or a subclass.
205     generation: Only run the specified command if the generation matches.
206        (See "Conditional Updates Using Object Versioning" in the gsutil docs.)
207     headers: Fill in this dictionary with header values captured from stderr.
208     get_headers_from_stdout: Whether header information is to be parsed from
209       stdout (default: stderr).
210     kwargs: Additional options to pass directly to RunCommand, beyond the
211       explicit ones above.  See RunCommand itself.
212
213   Returns:
214     Anything that RunCommand returns, which should be a CommandResult object.
215
216   Raises:
217     GsutilMissingError is the gsutil utility cannot be found.
218     GSLibError (or whatever is in failed_exception) if RunCommand failed (and
219       error_ok was not True).
220   """
221   # The -d flag causes gsutil to dump various metadata, including user
222   # credentials.  We therefore don't allow users to pass it in directly.
223   assert '-d' not in args, 'Cannot pass in the -d flag directly'
224
225   gsutil = FindGsUtil()
226
227   if generation is not None:
228     args = ['-h', 'x-goog-if-generation-match:%s' % generation] + args
229   if headers is not None:
230     args.insert(0, '-d')
231     assert redirect_stderr
232   cmd = [gsutil] + args
233   run_opts = {'redirect_stdout': redirect_stdout,
234               'redirect_stderr': redirect_stderr,
235               }
236   run_opts.update(kwargs)
237
238   # Always use RunCommand with return_result on, which will be the default
239   # behavior for RunCommand itself someday.
240   run_opts['return_result'] = True
241
242   try:
243     result = utils.RunCommand(cmd, **run_opts)
244   except OSError as e:
245     if e.errno == errno.ENOENT:
246       raise GsutilMissingError()
247     raise
248   except utils.CommandFailedException as e:
249     # If headers is set, we have to hide the output here because it may contain
250     # credentials that we don't want to show in buildbot logs.
251     raise failed_exception('%r failed' % cmd if headers else e)
252
253   if headers is not None and result is not None:
254     assert (redirect_stdout if get_headers_from_stdout else redirect_stderr)
255     # Parse headers that look like this:
256     # header: x-goog-generation: 1359148994758000
257     # header: x-goog-metageneration: 1
258     headers_source = result.output if get_headers_from_stdout else result.error
259     for line in headers_source.splitlines():
260       if line.startswith('header: '):
261         header, _, value = line.partition(': ')[-1].partition(': ')
262         headers[header.replace('x-goog-', '')] = value
263
264     # Strip out stderr entirely to avoid showing credentials in logs; for
265     # commands that dump credentials to stdout, clobber that as well.
266     result.error = '<stripped>'
267     if get_headers_from_stdout:
268       result.output = '<stripped>'
269
270   return result
271
272
273 def ValidateGsutilWorking(bucket):
274   """Validate that gsutil is working correctly.
275
276   There is a failure mode for gsutil in which all operations fail, and this
277   is indistinguishable from all gsutil ls operations matching nothing. We
278   check that there is at least one file in the root of the bucket.
279
280   Args:
281     bucket: bucket we are about to test.
282
283   Raises:
284     ValidateGsutilFailure: If we are unable to find any files in the bucket.
285   """
286   url = 'gs://%s/' % bucket
287   if not List(url):
288     raise ValidateGsutilFailure('Unable to find anything in: %s' % url)
289
290
291 def GetGsutilVersion():
292   """Return the version string for the installed gsutil utility.
293
294   Returns:
295     The version string.
296
297   Raises:
298     GsutilMissingError if gsutil cannot be found.
299     GSLibError for any other error.
300   """
301   args = ['version']
302
303   # As of version 3.26, a quirk of 'gsutil version' is that if gsutil is
304   # outdated it will ask if you want to update (Y/n) before proceeding... but
305   # do it only the first time (for a particular update?  I'm not exactly sure).
306   # Prepare a 'n' answer just in case.
307   user_input = 'n\n'
308
309   result = RunGsutilCommand(args, error_ok=False, input=user_input)
310
311   output = '\n'.join(o for o in [result.output, result.error] if o)
312
313   if output:
314     match = re.search(r'^\s*gsutil\s+version\s+([\d\.]+)', output,
315                       re.IGNORECASE)
316     if match:
317       return match.group(1)
318     else:
319       logging.error('Unexpected output format from %r:\n%s',
320                     result.cmdstr, output)
321       raise GSLibError('Unexpected output format from %r.' % result.cmdstr)
322
323   else:
324     logging.error('No stdout output from %r.', result.cmdstr)
325     raise GSLibError('No stdout output from %r.', result.cmdstr)
326
327
328 def UpdateGsutil():
329   """Update the gsutil utility to the latest version.
330
331   Returns:
332     The updated version, if updated, otherwise None.
333
334   Raises:
335     GSLibError if any error occurs.
336   """
337   original_version = GetGsutilVersion()
338   updated_version = None
339
340   # If an update is available the 'gsutil update' command will ask
341   # whether to continue.  Reply with 'y'.
342   user_input = 'y\n'
343   args = ['update']
344
345   result = RunGsutilCommand(args, error_ok=True, input=user_input)
346
347   if result.returncode != 0:
348     # Oddly, 'gsutil update' exits with error if no update is needed.
349     # Check the output to see if this is the situation, in which case the
350     # error is harmless (and expected).  Last line in stderr will be:
351     # "You already have the latest gsutil release installed."
352     if not result.error:
353       raise GSLibError('Failed command: %r' % result.cmdstr)
354
355     last_error_line = result.error.splitlines()[-1]
356     if not last_error_line.startswith('You already have'):
357       raise GSLibError(result.error)
358
359   else:
360     current_version = GetGsutilVersion()
361     if current_version != original_version:
362       updated_version = current_version
363
364   return updated_version
365
366
367 @RetryGSLib
368 def MD5Sum(gs_uri):
369   """Read the gsutil md5 sum from etag and gsutil ls -L.
370
371   Note that because this relies on 'gsutil ls -L' it suffers from the
372   eventual consistency issue, meaning this function could fail to find
373   the MD5 value for a recently created file in Google Storage.
374
375   Args:
376     gs_uri: An absolute Google Storage URI that refers directly to an object.
377       No globs are supported.
378
379   Returns:
380     A string that is an md5sum, or None if no object found.
381
382   Raises:
383     GSLibError if the gsutil command fails.  If there is no object at that path
384     that is not considered a failure.
385   """
386   gs_md5_regex = re.compile(r'.*?Hash \(md5\):\s+(.*)', re.IGNORECASE)
387   args = ['ls', '-L', gs_uri]
388
389   result = RunGsutilCommand(args, error_ok=True)
390
391   # If object was not found then output is completely empty.
392   if not result.output:
393     return None
394
395   for line in result.output.splitlines():
396     match = gs_md5_regex.match(line)
397     if match:
398       # gsutil now prints the MD5 sum in base64, but we want it in hex.
399       return base64.b16encode(base64.b64decode(match.group(1))).lower()
400
401   # This means there was some actual failure in the command.
402   raise GSLibError('Unable to determine MD5Sum for %r' % gs_uri)
403
404
405 @RetryGSLib
406 def Cmp(path1, path2):
407   """Return True if paths hold identical files, according to MD5 sum.
408
409   Note that this function relies on MD5Sum, which means it also can only
410   promise eventual consistency.  A recently uploaded file in Google Storage
411   may behave badly in this comparison function.
412
413   If either file is missing then always return False.
414
415   Args:
416     path1: URI to a file.  Local paths also supported.
417     path2: URI to a file.  Local paths also supported.
418
419   Returns:
420     True if files are the same, False otherwise.
421   """
422   md5_1 = MD5Sum(path1) if IsGsURI(path1) else filelib.MD5Sum(path1)
423   if not md5_1:
424     return False
425
426   md5_2 = MD5Sum(path2) if IsGsURI(path2) else filelib.MD5Sum(path2)
427
428   return md5_1 == md5_2
429
430
431 @RetryGSLib
432 def Copy(src_path, dest_path, acl=None, **kwargs):
433   """Run gsutil cp src_path dest_path supporting GS globs.
434
435   e.g.
436   gsutil cp /etc/* gs://etc/ where /etc/* is src_path with a glob and
437   gs://etc is dest_path.
438
439   This assumes that the src or dest path already exist.
440
441   Args:
442     src_path: The src of the path to copy, either a /unix/path or gs:// uri.
443     dest_path: The dest of the path to copy, either a /unix/path or gs:// uri.
444     acl: an ACL argument (predefined name or XML file) to pass to gsutil
445     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
446       explicit ones above.  See RunGsutilCommand itself.
447
448   Raises:
449     CopyFail: If the copy fails for any reason.
450   """
451   args = ['cp']
452   if acl:
453     args += ['-a', acl]
454   args += [src_path, dest_path]
455   RunGsutilCommand(args, failed_exception=CopyFail, **kwargs)
456
457
458 @RetryGSLib
459 def Move(src_path, dest_path, **kwargs):
460   """Run gsutil mv src_path dest_path supporting GS globs.
461
462   Note that the created time is changed to now for the moved object(s).
463
464   Args:
465     src_path: The src of the path to move, either a /unix/path or gs:// uri.
466     dest_path: The dest of the path to move, either a /unix/path or gs:// uri.
467     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
468       explicit ones above.  See RunGsutilCommand itself.
469
470   Raises:
471     MoveFail: If the move fails for any reason.
472   """
473   args = ['mv', src_path, dest_path]
474   RunGsutilCommand(args, failed_exception=MoveFail, **kwargs)
475
476 # pylint: disable-msg=C9011
477
478 @RetryGSLib
479 def Remove(*paths, **kwargs):
480   """Run gsutil rm on path supporting GS globs.
481
482   Args:
483     paths: Local path or gs URI, or list of same.
484     ignore_no_match: If True, then do not complain if anything was not
485       removed because no URI match was found.  Like rm -f.  Defaults to False.
486     recurse: Remove recursively starting at path.  Same as rm -R.  Defaults
487       to False.
488     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
489       explicit ones above.  See RunGsutilCommand itself.
490
491   Raises:
492     RemoveFail: If the remove fails for any reason.
493   """
494   ignore_no_match = kwargs.pop('ignore_no_match', False)
495   recurse = kwargs.pop('recurse', False)
496
497   args = ['rm']
498
499   if recurse:
500     args.append('-R')
501
502   args.extend(paths)
503
504   try:
505     RunGsutilCommand(args, failed_exception=RemoveFail, **kwargs)
506   except RemoveFail as e:
507     if not (ignore_no_match and 'No URLs matched' in str(e.args[0])):
508       raise
509
510
511 def RemoveDirContents(gs_dir_uri):
512   """Remove all contents of a directory.
513
514   Args:
515     gs_dir_uri: directory to delete contents of.
516   """
517   Remove(os.path.join(gs_dir_uri, '**'), ignore_no_match=True)
518
519
520 def CreateWithContents(gs_uri, contents, **kwargs):
521   """Creates the specified file with specified contents.
522
523   Args:
524     gs_uri: The URI of a file on Google Storage.
525     contents: Contents to write to the file.
526     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
527       explicit ones above.  See RunGsutilCommand itself.
528
529   Raises:
530     CopyFail: If it fails for any reason.
531   """
532   with utils.CreateTempFileWithContents(contents) as content_file:
533     Copy(content_file.name, gs_uri, **kwargs)
534
535
536 def Cat(gs_uri, **kwargs):
537   """Return the contents of a file at the given GS URI
538
539   Args:
540     gs_uri: The URI of a file on Google Storage.
541     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
542       explicit ones above.  See RunGsutilCommand itself.
543
544   Raises:
545     CatFail: If the cat fails for any reason.
546   """
547   args = ['cat', gs_uri]
548   result = RunGsutilCommand(args, failed_exception=CatFail, **kwargs)
549   return result.output
550
551
552 def Stat(gs_uri, **kwargs):
553   """Stats a file at the given GS URI (returns nothing).
554
555   Args:
556     gs_uri: The URI of a file on Google Storage.
557     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
558       explicit ones above.  See RunGsutilCommand itself.
559
560   Raises:
561     StatFail: If the stat fails for any reason.
562   """
563   args = ['stat', gs_uri]
564   # IMPORTANT! With stat, header information is dumped to standard output,
565   # rather than standard error, as with other gsutil commands. Hence,
566   # get_headers_from_stdout must be True to ensure both correct parsing of
567   # output and stripping of sensitive information.
568   RunGsutilCommand(args, failed_exception=StatFail,
569                    get_headers_from_stdout=True, **kwargs)
570
571
572 def IsGsURI(path):
573   """Returns true if the path begins with gs://
574
575   Args:
576     path: An absolute Google Storage URI.
577
578   Returns:
579     True if path is really a google storage uri that begins with gs://
580     False otherwise.
581   """
582   return path and path.startswith(PROTOCOL + '://')
583
584
585 def SplitGSUri(gs_uri):
586   """Returns tuple (bucket, uri_remainder) from GS URI.
587
588   Examples: 1) 'gs://foo/hi/there' returns ('foo', 'hi/there')
589             2) 'gs://foo/hi/there/' returns ('foo', 'hi/there/')
590             3) 'gs://foo' returns ('foo', '')
591             4) 'gs://foo/' returns ('foo', '')
592
593   Args:
594     gs_uri: A Google Storage URI.
595
596   Returns:
597     A tuple (bucket, uri_remainder)
598
599   Raises:
600     URIError if URI is not in recognized format
601   """
602   match = re.search(r'^gs://([^/]+)/?(.*)$', gs_uri)
603   if match:
604     return (match.group(1), match.group(2))
605   else:
606     raise URIError('Bad GS URI: %r' % gs_uri)
607
608
609 # TODO(mtennant): Rename this "Size" for consistency.
610 @RetryGSLib
611 def FileSize(gs_uri, **kwargs):
612   """Return the size of the given gsutil file in bytes.
613
614   Args:
615     gs_uri: Google Storage URI (beginning with 'gs://') pointing
616       directly to a single file.
617     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
618       explicit ones above.  See RunGsutilCommand itself.
619
620   Returns:
621     Size of file in bytes.
622
623   Raises:
624     URIError: Raised when URI is unknown to Google Storage or when
625       URI matches more than one file.
626   """
627   headers = {}
628   try:
629     Stat(gs_uri, headers=headers, **kwargs)
630   except StatFail as e:
631     raise URIError('Unable to stat file at URI %r: %s' % (gs_uri, e))
632
633   size_str = headers.get('stored-content-length')
634   if size_str is None:
635     raise URIError('Failed to get size of %r' % gs_uri)
636
637   return int(size_str)
638
639
640 def FileTimestamp(gs_uri, **kwargs):
641   """Return the timestamp of the given gsutil file.
642
643   Args:
644     gs_uri: Google Storage URI (beginning with 'gs://') pointing
645       directly to a single file.
646     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
647       explicit ones above.  See RunGsutilCommand itself.
648
649   Returns:
650     datetime of the files creation, or None
651
652   Raises:
653     URIError: Raised when URI is unknown to Google Storage or when
654       URI matches more than one file.
655   """
656   args = ['ls', '-l', gs_uri]
657   try:
658     result = RunGsutilCommand(args, **kwargs)
659     ls_lines = result.output.splitlines()
660
661     # We expect one line per file and a summary line.
662     if len(ls_lines) != 2:
663       raise URIError('More than one file matched URI %r' % gs_uri)
664
665     # Should have the format:
666     # <filesize> <date> <filepath>
667     return datetime.datetime.strptime(ls_lines[0].split()[1],
668                                       '%Y-%m-%dT%H:%M:%S')
669   except GSLibError:
670     raise URIError('Unable to locate file at URI %r' % gs_uri)
671
672
673 def ExistsLazy(gs_uri, **kwargs):
674   """Return True if object exists at given GS URI.
675
676   Warning: This can return false negatives, because 'gsutil ls' relies on
677   a cache that is only eventually consistent.  But it is faster to run, and
678   it does accept URIs with glob expressions, where Exists does not.
679
680   Args:
681     gs_uri: Google Storage URI
682     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
683       explicit ones above.  See RunGsutilCommand itself.
684
685   Returns:
686     True if object exists and False otherwise.
687
688   Raises:
689     URIError if there is a problem with the URI other than the URI
690       not being found.
691   """
692   args = ['ls', gs_uri]
693   try:
694     RunGsutilCommand(args, **kwargs)
695     return True
696   except GSLibError as e:
697     # If the URI was simply not found, the output should be something like:
698     # CommandException: One or more URLs matched no objects.
699     msg = str(e).strip()
700     if not msg.startswith('CommandException: '):
701       raise URIError(e)
702
703     return False
704
705
706 def Exists(gs_uri, **kwargs):
707   """Return True if object exists at given GS URI.
708
709   Args:
710     gs_uri: Google Storage URI.  Must be a fully-specified URI with
711       no glob expression.  Even if a glob expression matches this
712       method will return False.
713     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
714       explicit ones above.  See RunGsutilCommand itself.
715
716   Returns:
717     True if gs_uri points to an existing object, and False otherwise.
718   """
719   try:
720     Stat(gs_uri, **kwargs)
721   except StatFail:
722     return False
723
724   return True
725
726
727 @RetryGSLib
728 def List(root_uri, recurse=False, filepattern=None, sort=False):
729   """Return list of file and directory paths under given root URI.
730
731   Args:
732     root_uri: e.g. gs://foo/bar
733     recurse: Look in subdirectories, as well
734     filepattern: glob pattern to match against basename of path
735     sort: If True then do a default sort on paths
736
737   Returns:
738     List of GS URIs to paths that matched
739   """
740   gs_uri = root_uri
741   if recurse:
742     # In gs file patterns '**' absorbs any number of directory names,
743     # including none.
744     gs_uri = gs_uri.rstrip('/') + '/**'
745
746   # Now match the filename itself at the end of the URI.
747   if filepattern:
748     gs_uri = gs_uri.rstrip('/') + '/' + filepattern
749
750   args = ['ls', gs_uri]
751
752   try:
753     result = RunGsutilCommand(args)
754     paths = [path for path in result.output.splitlines() if path]
755
756     if sort:
757       paths = sorted(paths)
758
759     return paths
760
761   except GSLibError as e:
762     # The ls command will fail under normal operation if there was just
763     # nothing to be found. That shows up like this to stderr:
764     # CommandException: One or more URLs matched no objects.
765     if 'CommandException: One or more URLs matched no objects.' not in str(e):
766       raise
767
768   # Otherwise, assume a normal error.
769   # TODO(mtennant): It would be more functionally correct to return this
770   # if and only if the error is identified as a "file not found" error.
771   # We simply have to determine how to do that reliably.
772   return []
773
774
775 def ListFiles(root_uri, recurse=False, filepattern=None, sort=False):
776   """Return list of file paths under given root URI.
777
778   Directories are intentionally excluded.
779
780   Args:
781     root_uri: e.g. gs://foo/bar
782     recurse: Look for files in subdirectories, as well
783     filepattern: glob pattern to match against basename of file
784     sort: If True then do a default sort on paths
785
786   Returns:
787     List of GS URIs to files that matched
788   """
789   paths = List(root_uri, recurse=recurse, filepattern=filepattern, sort=sort)
790
791   # Directory paths should be excluded from output, per ListFiles guarantee.
792   return [path for path in paths if not path.endswith('/')]
793
794
795 def ListDirs(root_uri, recurse=False, filepattern=None, sort=False):
796   """Return list of dir paths under given root URI.
797
798   File paths are intentionally excluded.  The root_uri itself is excluded.
799
800   Args:
801     root_uri: e.g. gs://foo/bar
802     recurse: Look for directories in subdirectories, as well
803     filepattern: glob pattern to match against basename of director
804     sort: If True then do a default sort on paths
805
806   Returns:
807     List of GS URIs to directories that matched
808   """
809   paths = List(root_uri, recurse=recurse, filepattern=filepattern, sort=sort)
810
811   # Only include directory paths in output, per ListDirs guarantee.
812   return [path for path in paths if path.endswith('/')]
813
814
815 @RetryGSLib
816 def SetACL(gs_uri, acl_file, **kwargs):
817   """Set the ACLs of a file in Google Storage.
818
819   Args:
820     gs_uri: The GS URI to set the ACL on.
821     acl_file: A Google Storage xml ACL file.
822     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
823       explicit ones above.  See RunGsutilCommand itself.
824
825   Returns:
826     True if the ACL was successfully set
827
828   Raises:
829     AclFail: If SetACL fails for any reason.
830   """
831   args = ['setacl', acl_file, gs_uri]
832   RunGsutilCommand(args, failed_exception=AclFail, **kwargs)
833
834
835 @RetryGSLib
836 def CreateBucket(bucket, **kwargs):
837   """Create a Google Storage bucket using the users default credentials.
838
839   Args:
840     bucket: The name of the bucket to create.
841     kwargs: Additional options to pass directly to RunGsutilCommand, beyond the
842       explicit ones above.  See RunGsutilCommand itself.
843
844   Returns:
845     The GS URI of the bucket created.
846
847   Raises:
848     BucketOperationError if the bucket is not created properly.
849   """
850   gs_uri = 'gs://%s' % bucket
851   args = ['mb', gs_uri]
852   try:
853     RunGsutilCommand(args, **kwargs)
854   except GSLibError as e:
855     raise BucketOperationError('Error creating bucket %s.\n%s' % (bucket, e))
856
857   return gs_uri
858
859
860 @RetryGSLib
861 def DeleteBucket(bucket):
862   """Delete a Google Storage bucket using the users default credentials.
863
864   Warning: All contents will be deleted.
865
866   Args:
867     bucket: The name of the bucket to create.
868
869   Raises:
870     BucketOperationError if the bucket is not created properly.
871   """
872   bucket = bucket.strip('/')
873   gs_uri = 'gs://%s' % bucket
874   try:
875     RunGsutilCommand(['rm', '%s/*' % gs_uri], error_ok=True)
876     RunGsutilCommand(['rb', gs_uri])
877
878   except GSLibError as e:
879     raise BucketOperationError('Error deleting bucket %s.\n%s' % (bucket, e))