// Copyright 2018 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package blob_test

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"os"

	"cloud.google.com/go/storage"
	"github.com/aws/smithy-go"
	"gocloud.dev/blob"
	"gocloud.dev/blob/fileblob"

	_ "gocloud.dev/blob/gcsblob"
	_ "gocloud.dev/blob/s3blob"
)

func ExampleBucket_NewReader() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	ctx := context.Background()
	var bucket *blob.Bucket

	// Open the key "foo.txt" for reading with the default options.
	r, err := bucket.NewReader(ctx, "foo.txt", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()
	// Readers also have a limited view of the blob's metadata.
	fmt.Println("Content-Type:", r.ContentType())
	fmt.Println()
	// Copy from the reader to stdout.
	if _, err := io.Copy(os.Stdout, r); err != nil {
		log.Fatal(err)
	}
}

func ExampleBucket_NewRangeReader() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	ctx := context.Background()
	var bucket *blob.Bucket

	// Open the key "foo.txt" for reading at offset 1024 and read up to 4096 bytes.
	r, err := bucket.NewRangeReader(ctx, "foo.txt", 1024, 4096, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()
	// Copy from the read range to stdout.
	if _, err := io.Copy(os.Stdout, r); err != nil {
		log.Fatal(err)
	}
}

func ExampleBucket_NewWriter() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	ctx := context.Background()
	var bucket *blob.Bucket

	// Open the key "foo.txt" for writing with the default options.
	w, err := bucket.NewWriter(ctx, "foo.txt", nil)
	if err != nil {
		log.Fatal(err)
	}
	_, writeErr := fmt.Fprintln(w, "Hello, World!")
	// Always check the return value of Close when writing.
	closeErr := w.Close()
	if writeErr != nil {
		log.Fatal(writeErr)
	}
	if closeErr != nil {
		log.Fatal(closeErr)
	}
}

func ExampleBucket_NewWriter_cancel() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	ctx := context.Background()
	var bucket *blob.Bucket

	// Create a cancelable context from the existing context.
	writeCtx, cancelWrite := context.WithCancel(ctx)
	defer cancelWrite()

	// Open the key "foo.txt" for writing with the default options.
	w, err := bucket.NewWriter(writeCtx, "foo.txt", nil)
	if err != nil {
		log.Fatal(err)
	}

	// Assume some writes happened and we encountered an error.
	// Now we want to abort the write.

	if err != nil {
		// First cancel the context.
		cancelWrite()
		// You must still close the writer to avoid leaking resources.
		w.Close()
	}
}

func ExampleBucket_Delete() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	ctx := context.Background()
	var bucket *blob.Bucket

	if err := bucket.Delete(ctx, "foo.txt"); err != nil {
		log.Fatal(err)
	}
}

func Example() {
	// Connect to a bucket when your program starts up.
	// This example uses the file-based implementation in fileblob, and creates
	// a temporary directory to use as the root directory.
	dir, cleanup := newTempDir()
	defer cleanup()
	bucket, err := fileblob.OpenBucket(dir, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	// We now have a *blob.Bucket! We can write our application using the
	// *blob.Bucket type, and have the freedom to change the initialization code
	// above to choose a different service-specific driver later.

	// In this example, we'll write a blob and then read it.
	ctx := context.Background()
	if err := bucket.WriteAll(ctx, "foo.txt", []byte("Go Cloud Development Kit"), nil); err != nil {
		log.Fatal(err)
	}
	b, err := bucket.ReadAll(ctx, "foo.txt")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))

	// Output:
	// Go Cloud Development Kit
}

func ExampleBucket_ErrorAs() {
	// This example is specific to the s3blob implementation; it demonstrates
	// access to the underlying smithy.APIError type.
	// The types exposed for ErrorAs by s3blob are documented in
	// https://godoc.org/gocloud.dev/blob/s3blob#hdr-As

	ctx := context.Background()

	b, err := blob.OpenBucket(ctx, "s3://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	_, err = b.ReadAll(ctx, "nosuchfile")
	if err != nil {
		var awsErr smithy.APIError
		if b.ErrorAs(err, &awsErr) {
			fmt.Println(awsErr.ErrorCode())
		}
		// Alternatively, using the Go built-in errors package:
		if errors.As(err, &awsErr) {
			fmt.Println(awsErr.ErrorCode())
		}
	}
}

func ExampleBucket_List() {
	// Connect to a bucket when your program starts up.
	// This example uses the file-based implementation.
	dir, cleanup := newTempDir()
	defer cleanup()

	// Create the file-based bucket.
	bucket, err := fileblob.OpenBucket(dir, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	// Create some blob objects for listing: "foo[0..4].txt".
	ctx := context.Background()
	for i := range 5 {
		if err := bucket.WriteAll(ctx, fmt.Sprintf("foo%d.txt", i), []byte("Go Cloud Development Kit"), nil); err != nil {
			log.Fatal(err)
		}
	}

	// Iterate over them.
	// This will list the blobs created above because fileblob is strongly
	// consistent, but is not guaranteed to work on all services.
	li := bucket.List(nil)
	for {
		obj, err := li.Next(ctx)
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(obj.Key)
	}

	// Alternatively, use All to iterate (and optionally download):
	fmt.Println()
	fmt.Println("Now, using an iterator:")
	li = bucket.List(nil)
	iter, errFn := li.All(ctx)
	for obj, download := range iter {
		var buf bytes.Buffer
		if err := download(&buf, nil /* default ReaderOptions */); err != nil {
			log.Fatalf("download of %q failed: %v", obj.Key, err)
		}
		fmt.Printf("%s: %s\n", obj.Key, string(buf.Bytes()))
	}
	if err := errFn(); err != nil {
		log.Fatalf("iteration failed: %v", err)
	}

	// Output:
	// foo0.txt
	// foo1.txt
	// foo2.txt
	// foo3.txt
	// foo4.txt
	//
	// Now, using an iterator:
	// foo0.txt: Go Cloud Development Kit
	// foo1.txt: Go Cloud Development Kit
	// foo2.txt: Go Cloud Development Kit
	// foo3.txt: Go Cloud Development Kit
	// foo4.txt: Go Cloud Development Kit
}

func ExampleBucket_List_withDelimiter() {
	// Connect to a bucket when your program starts up.
	// This example uses the file-based implementation.
	dir, cleanup := newTempDir()
	defer cleanup()

	// Create the file-based bucket.
	bucket, err := fileblob.OpenBucket(dir, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	// Create some blob objects in a hierarchy.
	ctx := context.Background()
	for _, key := range []string{
		"dir1/subdir/a.txt",
		"dir1/subdir/b.txt",
		"dir2/c.txt",
		"d.txt",
	} {
		if err := bucket.WriteAll(ctx, key, []byte("Go Cloud Development Kit"), nil); err != nil {
			log.Fatal(err)
		}
	}

	// list lists files in b starting with prefix. It uses the delimiter "/",
	// and recurses into "directories", adding 2 spaces to indent each time.
	// It will list the blobs created above because fileblob is strongly
	// consistent, but is not guaranteed to work on all services.
	var list func(context.Context, *blob.Bucket, string, string)
	list = func(ctx context.Context, b *blob.Bucket, prefix, indent string) {
		iter := b.List(&blob.ListOptions{
			Delimiter: "/",
			Prefix:    prefix,
		})
		for {
			obj, err := iter.Next(ctx)
			if err == io.EOF {
				break
			}
			if err != nil {
				log.Fatal(err)
			}
			fmt.Printf("%s%s\n", indent, obj.Key)
			if obj.IsDir {
				list(ctx, b, obj.Key, indent+"  ")
			}
		}
	}
	list(ctx, bucket, "", "")

	// Output:
	// d.txt
	// dir1/
	//   dir1/subdir/
	//     dir1/subdir/a.txt
	//     dir1/subdir/b.txt
	// dir2/
	//   dir2/c.txt
}

func ExampleBucket_ListPage() {
	// Connect to a bucket when your program starts up.
	// This example uses the file-based implementation.
	dir, cleanup := newTempDir()
	defer cleanup()

	// Create the file-based bucket.
	bucket, err := fileblob.OpenBucket(dir, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	// Create some blob objects for listing: "foo[0..7].txt".
	ctx := context.Background()
	for i := range 8 {
		if err := bucket.WriteAll(ctx, fmt.Sprintf("foo%d.txt", i), []byte("Go Cloud Development Kit"), nil); err != nil {
			log.Fatal(err)
		}
	}

	// Iterate over them in pages.
	// This will list the blobs created above because fileblob is strongly
	// consistent, but is not guaranteed to work on all services.

	// The first page of 3 results.
	objs, token, err := bucket.ListPage(ctx, blob.FirstPageToken, 3, nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, obj := range objs {
		fmt.Println(obj.Key)
	}
	fmt.Println("END OF PAGE 1")

	// The second page of 3 results.
	objs, token, err = bucket.ListPage(ctx, token, 3, nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, obj := range objs {
		fmt.Println(obj.Key)
	}
	fmt.Println("END OF PAGE 2")

	// The third page with the last 2 results.
	objs, token, err = bucket.ListPage(ctx, token, 3, nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, obj := range objs {
		fmt.Println(obj.Key)
	}
	fmt.Println("END OF PAGE 3")

	// There are no more pages, so token is now nil. Calling ListPage again will return io.EOF.
	if token != nil {
		fmt.Println("Token was not nil.")
	}

	// Output:
	// foo0.txt
	// foo1.txt
	// foo2.txt
	// END OF PAGE 1
	// foo3.txt
	// foo4.txt
	// foo5.txt
	// END OF PAGE 2
	// foo6.txt
	// foo7.txt
	// END OF PAGE 3
}

func ExampleBucket_As() {
	// This example is specific to the gcsblob implementation; it demonstrates
	// access to the underlying cloud.google.com/go/storage.Client type.
	// The types exposed for As by gcsblob are documented in
	// https://godoc.org/gocloud.dev/blob/gcsblob#hdr-As

	// This URL will open the bucket "my-bucket" using default credentials.
	ctx := context.Background()
	b, err := blob.OpenBucket(ctx, "gs://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	// Access storage.Client fields via gcsClient here.
	var gcsClient *storage.Client
	if b.As(&gcsClient) {
		email, err := gcsClient.ServiceAccount(ctx, "project-name")
		if err != nil {
			log.Fatal(err)
		}
		_ = email
	} else {
		log.Println("Unable to access storage.Client through Bucket.As")
	}
}

func ExampleWriterOptions() {
	// This example is specific to the gcsblob implementation; it demonstrates
	// access to the underlying cloud.google.com/go/storage.Writer type.
	// The types exposed for As by gcsblob are documented in
	// https://godoc.org/gocloud.dev/blob/gcsblob#hdr-As

	ctx := context.Background()

	b, err := blob.OpenBucket(ctx, "gs://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	beforeWrite := func(as func(any) bool) error {
		var sw *storage.Writer
		if as(&sw) {
			fmt.Println(sw.ChunkSize)
		}
		return nil
	}

	options := blob.WriterOptions{BeforeWrite: beforeWrite}
	if err := b.WriteAll(ctx, "newfile.txt", []byte("hello\n"), &options); err != nil {
		log.Fatal(err)
	}
}

func ExampleListObject_As() {
	// This example is specific to the gcsblob implementation; it demonstrates
	// access to the underlying cloud.google.com/go/storage.ObjectAttrs type.
	// The types exposed for As by gcsblob are documented in
	// https://godoc.org/gocloud.dev/blob/gcsblob#hdr-As

	ctx := context.Background()

	b, err := blob.OpenBucket(ctx, "gs://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	iter := b.List(nil)
	for {
		obj, err := iter.Next(ctx)
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		// Access storage.ObjectAttrs via oa here.
		var oa storage.ObjectAttrs
		if obj.As(&oa) {
			_ = oa.Owner
		}
	}
}

func ExampleListOptions() {
	// This example is specific to the gcsblob implementation; it demonstrates
	// access to the underlying cloud.google.com/go/storage.Query type.
	// The types exposed for As by gcsblob are documented in
	// https://godoc.org/gocloud.dev/blob/gcsblob#hdr-As

	ctx := context.Background()

	b, err := blob.OpenBucket(ctx, "gs://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	beforeList := func(as func(any) bool) error {
		// Access storage.Query via q here.
		var q *storage.Query
		if as(&q) {
			_ = q.Delimiter
		}
		return nil
	}

	iter := b.List(&blob.ListOptions{Prefix: "", Delimiter: "/", BeforeList: beforeList})
	for {
		obj, err := iter.Next(ctx)
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		_ = obj
	}
}

func ExamplePrefixedBucket() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	var bucket *blob.Bucket

	// Wrap the bucket using blob.PrefixedBucket.
	// The prefix should end with "/", so that the resulting bucket operates
	// in a subfolder.
	bucket = blob.PrefixedBucket(bucket, "a/subfolder/")

	// The original bucket is no longer usable; it has been closed.
	// The wrapped bucket should be closed when done.
	defer bucket.Close()

	// Bucket operations on <key> will be translated to "a/subfolder/<key>".
}

func ExampleSingleKeyBucket() {
	// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
	// PRAGMA: On gocloud.dev, hide lines until the next blank line.
	var bucket *blob.Bucket

	// Wrap the bucket using blob.SingleKeyBucket.
	// The bucket always references the provided key.
	bucket = blob.SingleKeyBucket(bucket, "foo.txt")

	// The original bucket is no longer usable; it has been closed.
	// The wrapped bucket should be closed when done.
	defer bucket.Close()

	// Bucket operations will ignore the passed-in key and always reference foo.txt.
}

func ExampleReader_As() {
	// This example is specific to the gcsblob implementation; it demonstrates
	// access to the underlying cloud.google.com/go/storage.Reader type.
	// The types exposed for As by gcsblob are documented in
	// https://godoc.org/gocloud.dev/blob/gcsblob#hdr-As

	ctx := context.Background()

	b, err := blob.OpenBucket(ctx, "gs://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	r, err := b.NewReader(ctx, "gopher.png", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()

	// Access storage.Reader via sr here.
	var sr *storage.Reader
	if r.As(&sr) {
		_ = sr.Attrs
	}
}

func ExampleAttributes_As() {
	// This example is specific to the gcsblob implementation; it demonstrates
	// access to the underlying cloud.google.com/go/storage.ObjectAttrs type.
	// The types exposed for As by gcsblob are documented in
	// https://godoc.org/gocloud.dev/blob/gcsblob#hdr-As
	ctx := context.Background()

	b, err := blob.OpenBucket(ctx, "gs://my-bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer b.Close()

	attrs, err := b.Attributes(ctx, "gopher.png")
	if err != nil {
		log.Fatal(err)
	}

	var oa storage.ObjectAttrs
	if attrs.As(&oa) {
		fmt.Println(oa.Owner)
	}
}

func newTempDir() (string, func()) {
	dir, err := os.MkdirTemp("", "go-cloud-blob-example")
	if err != nil {
		panic(err)
	}
	return dir, func() { os.RemoveAll(dir) }
}
