blob: bcbf95ac4bf4a1fa2fe94bebb456f5eb37d3d5b8 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package artifacts
import (
"context"
"fmt"
"io"
"path"
"strings"
"cloud.google.com/go/storage"
"go.fuchsia.dev/fuchsia/tools/lib/osmisc"
"google.golang.org/api/iterator"
)
// The default name of a test's output file. We only store a single file containing a
// test's combined stdout and stderr streams today. This will change in the future.
const DefaultTestOutputName = "stdout-stderr.txt"
// Directory is an interface for interacting with a Cloud Storage "directory".
type Directory interface {
Object(string) *storage.ObjectHandle
CopyFile(ctx context.Context, src, dest string) error
List(context.Context, string) ([]string, error)
}
// BuildDirectory represents a Fuchsia CI build's artifact directory. Refer to the
// layout in doc.go for the layout of this directory. When amending the layout, prefer
// adding convenience methods on this type to encourages all clients to create objects
// within this BuildDirectory using the same layout.
type BuildDirectory struct {
*directory
}
// NewTestOutputObject creates a new ObjectHandle to hold the output of the given test
// execution in this BuildDirectory. testName is the name of the test, envName is the
// canonical name of the test environment. Both are normalized according to
// normalizePathSegment.
func (d BuildDirectory) NewTestOutputObject(ctx context.Context, testName, envName string) *storage.ObjectHandle {
return d.cd("tests").cd(testName).cd(envName).Object(DefaultTestOutputName)
}
// directory is a handle to a Cloud Storage "directory". It provides a minimal
// filesystem-like interface for a Cloud Storage object hierarchy where "/" is used as the
// path separator. Any methods added to this struct are forward to other directory types.
type directory struct {
bucket *storage.BucketHandle
root string
}
// Object returns a handle to the given object within this directory. objPath is the path to
// the object relative to this directory.
func (d *directory) Object(objPath string) *storage.ObjectHandle {
objPath = path.Join(d.root, objPath)
return d.bucket.Object(objPath)
}
// CopyFile copies the object from src to dst, creating all the parent directories
// of dest if they don't exist.
func (d *directory) CopyFile(ctx context.Context, src, dest string) error {
obj := d.Object(src)
input, err := obj.NewReader(ctx)
if err != nil {
return err
}
output, err := osmisc.CreateFile(dest)
if err != nil {
return err
}
_, err = io.Copy(output, input)
return err
}
// List lists all of the objects in this directory with the given prefix. If prefix == "",
// it will list everything in this directory.
func (d *directory) List(ctx context.Context, prefix string) ([]string, error) {
prefix = path.Join(d.root, prefix)
iter := d.bucket.Objects(ctx, &storage.Query{
Prefix: prefix,
})
var items []string
for {
attrs, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
items = append(items, strings.TrimPrefix(attrs.Name, d.root+"/"))
}
return items, nil
}
// CD returns a handle to some child directory of this directory. directChild should be a
// direct child of the current directory, not a grandchild or any entry deeper in the
// tree. The child's name is normalized according to normalizePathSegment, so using a
// nested path may result in an unexpected file tree.
func (d *directory) cd(directChild string) *directory {
return &directory{
bucket: d.bucket,
root: fmt.Sprintf("%s/%s", d.root, normalizePathSegment(directChild)),
}
}