go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/gcloud/gs/path.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gs
    16  
    17  import (
    18  	"strings"
    19  )
    20  
    21  // Path is a Google Storage path. A full path consists of a Google storage
    22  // bucket and a series of path components.
    23  //
    24  // An example of a Path is:
    25  //
    26  //	gs://test-bucket/path/to/thing.txt
    27  type Path string
    28  
    29  // MakePath constructs a Google Storage path from optional bucket and filename
    30  // components.
    31  //
    32  // Trailing forward slashes will be removed from the bucket name, if present.
    33  func MakePath(bucket string, parts ...string) Path {
    34  	if len(parts) == 0 {
    35  		return makePath(bucket, "")
    36  	} else if len(parts) <= 1 {
    37  		return makePath(bucket, parts[0])
    38  	}
    39  	path := makePath(bucket, parts[0])
    40  	return path.Concat(parts[1], parts[2:]...)
    41  }
    42  
    43  func makePath(bucket, filename string) Path {
    44  	var carr [2]string
    45  
    46  	comps := carr[:0]
    47  	if b := stripTrailingSlashes(bucket); b != "" {
    48  		comps = append(comps, "gs://"+b)
    49  	}
    50  	if filename != "" {
    51  		comps = append(comps, filename)
    52  	}
    53  	return Path(strings.Join(comps, "/"))
    54  }
    55  
    56  // Bucket returns the Google Storage bucket component of the Path. If there is
    57  // no bucket, an empty string will be returned.
    58  func (p Path) Bucket() string {
    59  	b, _ := p.Split()
    60  	return b
    61  }
    62  
    63  // Filename returns the filename component of the Path. If there is no filename
    64  // component, an empty string will be returned.
    65  //
    66  // Leading and trailing slashes will be truncated.
    67  func (p Path) Filename() string {
    68  	_, f := p.Split()
    69  	return f
    70  }
    71  
    72  // Split returns the bucket and filename components of the Path.
    73  //
    74  // If a bucket is not defined (doesn't begin with "gs://"), the remainder will
    75  // be considered to be the filename component. If a filename is not defined,
    76  // an empty string will be returned.
    77  func (p Path) Split() (bucket string, filename string) {
    78  	v, ok := trimPrefix(string(p), "gs://")
    79  	if ok {
    80  		// Has a "gs://" prefix, trim that to get the bucket.
    81  		sidx := strings.IndexRune(v, '/')
    82  		if sidx <= 0 {
    83  			// Only a Google Storage bucket name.
    84  			bucket = v
    85  			return
    86  		}
    87  
    88  		bucket = v[:sidx]
    89  		v = v[sidx+1:]
    90  	}
    91  	filename = v
    92  	return
    93  }
    94  
    95  // IsFullPath returns true if the Path contains both a bucket and file name.
    96  func (p Path) IsFullPath() bool {
    97  	bucket, filename := p.Split()
    98  	return (bucket != "" && filename != "")
    99  }
   100  
   101  // Concat concatenates a filename component to the end of Path.
   102  //
   103  // Multiple components may be specified. In this case, each will be added as a
   104  // "/"-delimited component, and will have any present trailing slashes stripped.
   105  func (p Path) Concat(v string, parts ...string) Path {
   106  	comps := make([]string, 0, len(parts)+2)
   107  	add := func(v string) {
   108  		v = stripTrailingSlashes(v)
   109  		if len(v) > 0 {
   110  			comps = append(comps, v)
   111  		}
   112  	}
   113  
   114  	// Build our components slice.
   115  	b, f := p.Split()
   116  	if cleanBucket := stripTrailingSlashes(b); cleanBucket != "" {
   117  		add("gs://" + cleanBucket)
   118  	}
   119  	add(f)
   120  	add(v)
   121  	for _, p := range parts {
   122  		add(p)
   123  	}
   124  	return Path(strings.Join(comps, "/"))
   125  }
   126  
   127  func trimPrefix(s, prefix string) (string, bool) {
   128  	if strings.HasPrefix(s, prefix) {
   129  		return s[len(prefix):], true
   130  	}
   131  	return s, false
   132  }
   133  
   134  func stripTrailingSlashes(v string) string {
   135  	return strings.TrimRight(v, "/")
   136  }