package main

import (
	"errors"
	"fmt"
	"io"
	"maps"
	"net/url"
	"os"
	"path"
	"sort"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"go.yaml.in/yaml/v4"

	incus "github.com/lxc/incus/v7/client"
	"github.com/lxc/incus/v7/cmd/incus/color"
	u "github.com/lxc/incus/v7/cmd/incus/usage"
	"github.com/lxc/incus/v7/internal/i18n"
	"github.com/lxc/incus/v7/shared/api"
	"github.com/lxc/incus/v7/shared/archive"
	cli "github.com/lxc/incus/v7/shared/cmd"
	"github.com/lxc/incus/v7/shared/ioprogress"
	"github.com/lxc/incus/v7/shared/logger"
	"github.com/lxc/incus/v7/shared/termios"
	"github.com/lxc/incus/v7/shared/units"
	"github.com/lxc/incus/v7/shared/util"
)

type cmdStorageBucket struct {
	global     *cmdGlobal
	flagTarget string
}

func (c *cmdStorageBucket) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("bucket")
	cmd.Short = i18n.G("Manage storage buckets")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Manage storage buckets.`))

	// Create.
	storageBucketCreateCmd := cmdStorageBucketCreate{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketCreateCmd.command())

	// Delete.
	storageBucketDeleteCmd := cmdStorageBucketDelete{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketDeleteCmd.command())

	// Edit.
	storageBucketEditCmd := cmdStorageBucketEdit{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketEditCmd.command())

	// Get.
	storageBucketGetCmd := cmdStorageBucketGet{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketGetCmd.command())

	// List.
	storageBucketListCmd := cmdStorageBucketList{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketListCmd.command())

	// Set.
	storageBucketSetCmd := cmdStorageBucketSet{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketSetCmd.command())

	// Show.
	storageBucketShowCmd := cmdStorageBucketShow{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketShowCmd.command())

	// Unset.
	storageBucketUnsetCmd := cmdStorageBucketUnset{global: c.global, storageBucket: c, storageBucketSet: &storageBucketSetCmd}
	cmd.AddCommand(storageBucketUnsetCmd.command())

	// Key.
	storageBucketKeyCmd := cmdStorageBucketKey{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketKeyCmd.command())

	// Export.
	storageBucketExportCmd := cmdStorageBucketExport{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketExportCmd.command())

	// Import.
	storageBucketImporttCmd := cmdStorageBucketImport{global: c.global, storageBucket: c}
	cmd.AddCommand(storageBucketImporttCmd.command())

	// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
	cmd.Args = cobra.NoArgs
	cmd.Run = func(cmd *cobra.Command, _ []string) { _ = cmd.Usage() }
	return cmd
}

// Create.
type cmdStorageBucketCreate struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket

	flagDescription string
}

var cmdStorageBucketCreateUsage = u.Usage{u.Pool.Remote(), u.NewName(u.Bucket), u.KV.List(0)}

func (c *cmdStorageBucketCreate) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("create", cmdStorageBucketCreateUsage...)
	cmd.Aliases = []string{"add"}
	cmd.Short = i18n.G("Create new custom storage buckets")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Create new custom storage buckets`))
	cmd.Example = cli.FormatSection("", i18n.G(`incus storage bucket create p1 b01
	Create a new storage bucket named b01 in storage pool p1

incus storage bucket create p1 b01 < config.yaml
	Create a new storage bucket named b01 in storage pool p1 using the content of config.yaml`))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddStringFlag(cmd.Flags(), &c.flagDescription, "description", "", "", i18n.G("Bucket description"))

	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketCreate) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketCreateUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	keys, err := kvToMap(parsed[2])
	if err != nil {
		return err
	}

	// If stdin isn't a terminal, read yaml from it.
	var bucketPut api.StorageBucketPut
	if !termios.IsTerminal(getStdinFd()) {
		loader, err := yaml.NewLoader(os.Stdin, yaml.WithKnownFields())
		if err != nil {
			return err
		}

		err = loader.Load(&bucketPut)
		if err != nil && !errors.Is(err, io.EOF) {
			return err
		}
	}

	if bucketPut.Config == nil {
		bucketPut.Config = map[string]string{}
	}

	maps.Copy(bucketPut.Config, keys)

	// Create the storage bucket.
	bucket := api.StorageBucketsPost{
		Name:             bucketName,
		StorageBucketPut: bucketPut,
	}

	if c.flagDescription != "" {
		bucket.Description = c.flagDescription
	}

	// If a target was specified, create the bucket on the given member.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	adminKey, err := d.CreateStoragePoolBucket(poolName, bucket)
	if err != nil {
		return err
	}

	if !c.global.flagQuiet {
		fmt.Printf(i18n.G("Storage bucket %q created")+"\n", bucketName)

		if adminKey != nil {
			fmt.Printf(i18n.G("Admin access key: %s")+"\n", adminKey.AccessKey)
			fmt.Printf(i18n.G("Admin secret key: %s")+"\n", adminKey.SecretKey)
		}
	}

	return nil
}

// Delete.
type cmdStorageBucketDelete struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket
}

var cmdStorageBucketDeleteUsage = u.Usage{u.Pool.Remote(), u.Bucket}

func (c *cmdStorageBucketDelete) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("delete", cmdStorageBucketDeleteUsage...)
	cmd.Aliases = []string{"rm", "remove"}
	cmd.Short = i18n.G("Delete storage buckets")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Delete storage buckets`))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketDelete) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketDeleteUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String

	// If a target was specified, delete the bucket on the given member.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	// Delete the bucket.
	err = d.DeleteStoragePoolBucket(poolName, bucketName)
	if err != nil {
		return err
	}

	if !c.global.flagQuiet {
		fmt.Printf(i18n.G("Storage bucket %q deleted")+"\n", bucketName)
	}

	return nil
}

// Edit.
type cmdStorageBucketEdit struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket
}

var cmdStorageBucketEditUsage = u.Usage{u.Pool.Remote(), u.Bucket}

func (c *cmdStorageBucketEdit) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("edit", cmdStorageBucketEditUsage...)
	cmd.Short = i18n.G("Edit storage bucket configurations as YAML")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Edit storage bucket configurations as YAML`))
	cmd.Example = cli.FormatSection("", i18n.G(`incus storage bucket edit [<remote>:]<pool> <bucket> < bucket.yaml
    Update a storage bucket using the content of bucket.yaml.`))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketEdit) helpTemplate() string {
	return i18n.G(
		`### This is a YAML representation of a storage bucket.
### Any line starting with a '# will be ignored.
###
### A storage bucket consists of a set of configuration items.
###
### name: bucket1
### used_by: []
### config:
###   size: "61203283968"`,
	)
}

func (c *cmdStorageBucketEdit) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketEditUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String

	// If stdin isn't a terminal, read text from it
	if !termios.IsTerminal(getStdinFd()) {
		loader, err := yaml.NewLoader(os.Stdin)
		if err != nil {
			return err
		}

		// Allow output of `incus storage bucket show` command to be passed in here, but only take the
		// contents of the StorageBucketPut fields when updating.
		// The other fields are silently discarded.
		newdata := api.StorageBucketPut{}
		err = loader.Load(&newdata)
		if err != nil && !errors.Is(err, io.EOF) {
			return err
		}

		return d.UpdateStoragePoolBucket(poolName, bucketName, newdata, "")
	}

	// If a target was specified, edit the bucket on the given member.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	// Get the current config.
	bucket, etag, err := d.GetStoragePoolBucket(poolName, bucketName)
	if err != nil {
		return err
	}

	data, err := yaml.Dump(&bucket, yaml.WithV2Defaults())
	if err != nil {
		return err
	}

	// Spawn the editor.
	content, err := cli.TextEditor("", []byte(c.helpTemplate()+"\n\n"+string(data)))
	if err != nil {
		return err
	}

	for {
		// Parse the text received from the editor
		newdata := api.StorageBucket{}
		err = yaml.Load(content, &newdata)
		if err == nil {
			err = d.UpdateStoragePoolBucket(poolName, bucketName, newdata.Writable(), etag)
		}

		// Respawn the editor
		if err != nil {
			fmt.Fprintf(os.Stderr, i18n.G("Config parsing error: %s")+"\n", err)
			fmt.Println(i18n.G("Press enter to open the editor again or ctrl+c to abort change"))

			_, err := os.Stdin.Read(make([]byte, 1))
			if err != nil {
				return err
			}

			content, err = cli.TextEditor("", content)
			if err != nil {
				return err
			}

			continue
		}

		break
	}

	return nil
}

// Get.
type cmdStorageBucketGet struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket

	flagIsProperty bool
}

var cmdStorageBucketGetUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.Key}

func (c *cmdStorageBucketGet) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("get", cmdStorageBucketGetUsage...)
	cmd.Short = i18n.G("Get values for storage bucket configuration keys")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Get values for storage bucket configuration keys`))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagIsProperty, "property|p", i18n.G("Get the key as a storage bucket property"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketGet) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketGetUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	key := parsed[2].String

	// If a target was specified, use the bucket on the given member.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	// Get the storage bucket entry.
	resp, _, err := d.GetStoragePoolBucket(poolName, bucketName)
	if err != nil {
		return err
	}

	if c.flagIsProperty {
		w := resp.Writable()
		res, err := getFieldByJSONTag(&w, key)
		if err != nil {
			return fmt.Errorf(i18n.G("The property %q does not exist on the storage bucket %q: %v"), key, bucketName, err)
		}

		fmt.Printf("%v\n", res)
	} else {
		v, ok := resp.Config[key]
		if ok {
			fmt.Println(v)
		}
	}

	return nil
}

// List.
type cmdStorageBucketList struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket

	flagFormat      string
	flagAllProjects bool
	flagColumns     string
}

var cmdStorageBucketListUsage = u.Usage{u.Pool.Remote(), u.Filter.List(0)}

type storageBucketColumn struct {
	Name string
	Data func(api.StorageBucket) string
}

func (c *cmdStorageBucketList) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("list", cmdStorageBucketListUsage...)
	cmd.Aliases = []string{"ls"}
	cmd.Short = i18n.G("List storage buckets")

	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
		`List storage buckets

Default column layout: ndL

== Columns ==
The -c option takes a comma separated list of arguments that control
which storage bucket attributes to output when displaying in table or csv
format.

Column arguments are either pre-defined shorthand chars (see below),
or (extended) config keys.

Commas between consecutive shorthand chars are optional.

Pre-defined column shorthand chars:
  e - Project name
  n - Name
  d - Description
  L - Location of the storage bucket (e.g. its cluster member)`,
	))

	cli.AddStringFlag(cmd.Flags(), &c.flagFormat, "format|f", c.global.defaultListFormat(), "", i18n.G(`Format (csv|json|table|yaml|compact|markdown), use suffix ",noheader" to disable headers and ",header" to enable it if missing, e.g. csv,header`))
	cli.AddBoolFlag(cmd.Flags(), &c.flagAllProjects, "all-projects", i18n.G("Display storage pool buckets from all projects"))
	cli.AddStringFlag(cmd.Flags(), &c.flagColumns, "columns|c", defaultStorageBucketColumns, "", i18n.G("Columns"))

	cmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
		return cli.ValidateFlagFormatForListOutput(cmd.Flag("format").Value.String())
	}

	cmd.RunE = c.run

	return cmd
}

const defaultStorageBucketColumns = "nd" // codespell:ignore nd

func (c *cmdStorageBucketList) parseColumns(clustered bool) ([]storageBucketColumn, error) {
	columnsShorthandMap := map[rune]storageBucketColumn{
		'e': {i18n.G("PROJECT"), c.projectColumnData},
		'n': {i18n.G("NAME"), c.nameColumnData},
		'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData},
		'L': {i18n.G("LOCATION"), c.locationColumnData},
	}

	columnList := strings.Split(c.flagColumns, ",")
	columns := []storageBucketColumn{}

	if c.flagColumns == defaultStorageBucketColumns && clustered {
		columnList = append(columnList, "L")
	}

	if c.flagColumns == defaultStorageBucketColumns && c.flagAllProjects {
		columnList = append([]string{"e"}, columnList...)
	}

	for _, columnEntry := range columnList {
		if columnEntry == "" {
			return nil, fmt.Errorf(i18n.G("Empty column entry (redundant, leading or trailing command) in '%s'"), c.flagColumns)
		}

		for _, columnRune := range columnEntry {
			column, ok := columnsShorthandMap[columnRune]
			if !ok {
				return nil, fmt.Errorf(i18n.G("Unknown column shorthand char '%c' in '%s'"), columnRune, columnEntry)
			}

			columns = append(columns, column)
		}
	}

	return columns, nil
}

func (c *cmdStorageBucketList) nameColumnData(bucket api.StorageBucket) string {
	return bucket.Name
}

func (c *cmdStorageBucketList) descriptionColumnData(bucket api.StorageBucket) string {
	return bucket.Description
}

func (c *cmdStorageBucketList) locationColumnData(bucket api.StorageBucket) string {
	return bucket.Location
}

func (c *cmdStorageBucketList) projectColumnData(bucket api.StorageBucket) string {
	return bucket.Project
}

func (c *cmdStorageBucketList) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketListUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	filters := prepareStorageBucketFilters(parsed[1].StringList)

	var buckets []api.StorageBucket
	if c.flagAllProjects {
		buckets, err = d.GetStoragePoolBucketsWithFilterAllProjects(poolName, filters)
		if err != nil {
			return err
		}
	} else {
		buckets, err = d.GetStoragePoolBucketsWithFilter(poolName, filters)
		if err != nil {
			return err
		}
	}

	// Parse column flags.
	columns, err := c.parseColumns(d.IsClustered())
	if err != nil {
		return err
	}

	data := make([][]string, 0, len(buckets))
	for _, bucket := range buckets {
		line := []string{}
		for _, column := range columns {
			line = append(line, column.Data(bucket))
		}

		data = append(data, line)
	}

	sort.Sort(cli.SortColumnsNaturally(data))

	header := []string{}
	for _, column := range columns {
		header = append(header, column.Name)
	}

	return cli.RenderTable(os.Stdout, c.flagFormat, header, data, buckets)
}

// Set.
type cmdStorageBucketSet struct {
	global *cmdGlobal

	storageBucket *cmdStorageBucket

	flagIsProperty bool
}

var cmdStorageBucketSetUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.LegacyKV.List(1)}

func (c *cmdStorageBucketSet) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("set", cmdStorageBucketSetUsage...)
	cmd.Short = i18n.G("Set storage bucket configuration keys")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
		`Set storage bucket configuration keys

For backward compatibility, a single configuration key may still be set with:
    incus storage bucket set [<remote>:]<pool> <bucket> <key> <value>`,
	))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagIsProperty, "property|p", i18n.G("Set the key as a storage bucket property"))
	cmd.RunE = c.run

	return cmd
}

// prepareStorageBucketFilters processes and formats filter criteria
// for storage buckets, ensuring they are in a format that the server can interpret.
func prepareStorageBucketFilters(filters []string) []string {
	formatedFilters := []string{}

	for _, filter := range filters {
		membs := strings.SplitN(filter, "=", 2)
		key := membs[0]

		if len(membs) == 1 {
			regexpValue := key
			if !strings.Contains(key, "^") && !strings.Contains(key, "$") {
				regexpValue = "^" + regexpValue + "$"
			}

			filter = fmt.Sprintf("name=(%s|^%s.*)", regexpValue, key)
		}

		formatedFilters = append(formatedFilters, filter)
	}

	return formatedFilters
}

// set runs the post-parsing command logic.
func (c *cmdStorageBucketSet) set(cmd *cobra.Command, parsed []*u.Parsed) error {
	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	keys, err := kvToMap(parsed[2])
	if err != nil {
		return err
	}

	// If a target was specified, use the bucket on the given member.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	// Get the storage bucket entry.
	bucket, etag, err := d.GetStoragePoolBucket(poolName, bucketName)
	if err != nil {
		return err
	}

	writable := bucket.Writable()
	if c.flagIsProperty {
		if cmd.Name() == "unset" {
			for k := range keys {
				err := unsetFieldByJSONTag(&writable, k)
				if err != nil {
					return fmt.Errorf(i18n.G("Error unsetting property: %v"), err)
				}
			}
		} else {
			err := unpackKVToWritable(&writable, keys)
			if err != nil {
				return fmt.Errorf(i18n.G("Error setting properties: %v"), err)
			}
		}
	} else {
		maps.Copy(writable.Config, keys)
	}

	err = d.UpdateStoragePoolBucket(poolName, bucketName, writable, etag)
	if err != nil {
		return err
	}

	return nil
}

func (c *cmdStorageBucketSet) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketSetUsage, cmd, args)
	if err != nil {
		return err
	}

	return c.set(cmd, parsed)
}

// Show.
type cmdStorageBucketShow struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket
}

var cmdStorageBucketShowUsage = u.Usage{u.Pool.Remote(), u.Bucket}

func (c *cmdStorageBucketShow) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("show", cmdStorageBucketShowUsage...)
	cmd.Short = i18n.G("Show storage bucket configurations")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Show storage bucket configurations`))
	cmd.Example = cli.FormatSection("", i18n.G(
		`incus storage bucket show default data
    Will show the properties of a bucket called "data" in the "default" pool.`,
	))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketShow) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketShowUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String

	// If a target member was specified, get the bucket with the matching name on that member, if any.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	bucket, _, err := d.GetStoragePoolBucket(poolName, bucketName)
	if err != nil {
		return err
	}

	data, err := yaml.Dump(&bucket, yaml.WithV2Defaults())
	if err != nil {
		return err
	}

	fmt.Printf("%s", data)

	return nil
}

// Unset.
type cmdStorageBucketUnset struct {
	global           *cmdGlobal
	storageBucket    *cmdStorageBucket
	storageBucketSet *cmdStorageBucketSet

	flagIsProperty bool
}

var cmdStorageBucketUnsetUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.Key.List(1)}

func (c *cmdStorageBucketUnset) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("unset", cmdStorageBucketUnsetUsage...)
	cmd.Short = i18n.G("Unset storage bucket configuration keys")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Unset storage bucket configuration keys`))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagIsProperty, "property|p", i18n.G("Unset the keys as storage bucket properties"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketUnset) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketUnsetUsage, cmd, args)
	if err != nil {
		return err
	}

	c.storageBucketSet.flagIsProperty = c.flagIsProperty
	return unsetKey(c.storageBucketSet, cmd, parsed)
}

// Key commands.
type cmdStorageBucketKey struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket

	flagTarget string
}

func (c *cmdStorageBucketKey) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("key")
	cmd.Short = i18n.G("Manage storage bucket keys")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Manage storage bucket keys.`))

	// Create.
	storageBucketKeyCreateCmd := cmdStorageBucketKeyCreate{global: c.global, storageBucketKey: c}
	cmd.AddCommand(storageBucketKeyCreateCmd.command())

	// Delete.
	storageBucketKeyDeleteCmd := cmdStorageBucketKeyDelete{global: c.global, storageBucketKey: c}
	cmd.AddCommand(storageBucketKeyDeleteCmd.command())

	// Edit.
	storageBucketKeyEditCmd := cmdStorageBucketKeyEdit{global: c.global, storageBucketKey: c}
	cmd.AddCommand(storageBucketKeyEditCmd.command())

	// List.
	storageBucketKeyListCmd := cmdStorageBucketKeyList{global: c.global, storageBucketKey: c}
	cmd.AddCommand(storageBucketKeyListCmd.command())

	// Show.
	storageBucketKeyShowCmd := cmdStorageBucketKeyShow{global: c.global, storageBucketKey: c}
	cmd.AddCommand(storageBucketKeyShowCmd.command())

	// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
	cmd.Args = cobra.NoArgs
	cmd.Run = func(cmd *cobra.Command, _ []string) { _ = cmd.Usage() }
	return cmd
}

// List Keys.
type cmdStorageBucketKeyList struct {
	global           *cmdGlobal
	storageBucketKey *cmdStorageBucketKey
	flagFormat       string
	flagColumns      string
}

type storageBucketKeyListColumns struct {
	Name string
	Data func(api.StorageBucketKey) string
}

var cmdStorageBucketKeyListUsage = u.Usage{u.Pool.Remote(), u.Bucket}

func (c *cmdStorageBucketKeyList) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("list", cmdStorageBucketKeyListUsage...)
	cmd.Aliases = []string{"ls"}
	cmd.Short = i18n.G("List storage bucket keys")

	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
		`List storage bucket keys

Default column layout: ndr

== Columns ==
The -c option takes a comma separated list of arguments that control
which storage bucket keys attributes to output when displaying in table or csv
format.

Column arguments are either pre-defined shorthand chars (see below),
or (extended) config keys.

Commas between consecutive shorthand chars are optional.

Pre-defined column shorthand chars:
  n - Name
  d - Description
  r - Role`,
	))
	cli.AddStringFlag(cmd.Flags(), &c.flagFormat, "format|f", c.global.defaultListFormat(), "", i18n.G(`Format (csv|json|table|yaml|compact|markdown), use suffix ",noheader" to disable headers and ",header" to enable it if missing, e.g. csv,header`))
	cli.AddStringFlag(cmd.Flags(), &c.storageBucketKey.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddStringFlag(cmd.Flags(), &c.flagColumns, "columns|c", defaultStorageBucketKeyColumns, "", i18n.G("Columns"))

	cmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
		return cli.ValidateFlagFormatForListOutput(cmd.Flag("format").Value.String())
	}

	cmd.RunE = c.run

	return cmd
}

const defaultStorageBucketKeyColumns = "ndr"

func (c *cmdStorageBucketKeyList) parseColumns() ([]storageBucketKeyListColumns, error) {
	columnsShorthandMap := map[rune]storageBucketKeyListColumns{
		'n': {i18n.G("NAME"), c.nameColumnData},
		'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData},
		'r': {i18n.G("ROLE"), c.roleColumnData},
	}

	columnList := strings.Split(c.flagColumns, ",")
	columns := []storageBucketKeyListColumns{}

	for _, columnEntry := range columnList {
		if columnEntry == "" {
			return nil, fmt.Errorf(i18n.G("Empty column entry (redundant, leading or trailing command) in '%s'"), c.flagColumns)
		}

		for _, columnRune := range columnEntry {
			column, ok := columnsShorthandMap[columnRune]
			if !ok {
				return nil, fmt.Errorf(i18n.G("Unknown column shorthand char '%c' in '%s'"), columnRune, columnEntry)
			}

			columns = append(columns, column)
		}
	}

	return columns, nil
}

func (c *cmdStorageBucketKeyList) nameColumnData(buckKey api.StorageBucketKey) string {
	return buckKey.Name
}

func (c *cmdStorageBucketKeyList) descriptionColumnData(buckKey api.StorageBucketKey) string {
	return buckKey.Description
}

func (c *cmdStorageBucketKeyList) roleColumnData(buckKey api.StorageBucketKey) string {
	return buckKey.Role
}

func (c *cmdStorageBucketKeyList) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketKeyListUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String

	// If a target member was specified, get the bucket with the matching name on that member, if any.
	if c.storageBucketKey.flagTarget != "" {
		d = d.UseTarget(c.storageBucketKey.flagTarget)
	}

	bucketKeys, err := d.GetStoragePoolBucketKeys(poolName, bucketName)
	if err != nil {
		return err
	}

	// Parse column flags.
	columns, err := c.parseColumns()
	if err != nil {
		return err
	}

	data := make([][]string, 0, len(bucketKeys))
	for _, bucketKey := range bucketKeys {
		line := []string{}
		for _, column := range columns {
			line = append(line, column.Data(bucketKey))
		}

		data = append(data, line)
	}

	sort.Sort(cli.SortColumnsNaturally(data))

	header := []string{}
	for _, column := range columns {
		header = append(header, column.Name)
	}

	return cli.RenderTable(os.Stdout, c.flagFormat, header, data, bucketKeys)
}

// Create Key.
type cmdStorageBucketKeyCreate struct {
	global           *cmdGlobal
	storageBucketKey *cmdStorageBucketKey
	flagRole         string
	flagAccessKey    string
	flagSecretKey    string
	flagDescription  string
}

var cmdStorageBucketKeyCreateUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.NewName(u.Key)}

func (c *cmdStorageBucketKeyCreate) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("create", cmdStorageBucketKeyCreateUsage...)
	cmd.Aliases = []string{"add"}
	cmd.Short = i18n.G("Create key for a storage bucket")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G("Create key for a storage bucket"))
	cmd.Example = cli.FormatSection("", i18n.G(`incus storage bucket key create p1 b01 k1
	Create a key called k1 for the bucket b01 in the pool p1.

incus storage bucket key create p1 b01 k1 < config.yaml
	Create a key called k1 for the bucket b01 in the pool p1 using the content of config.yaml.`))

	cmd.RunE = c.runAdd

	cli.AddStringFlag(cmd.Flags(), &c.storageBucketKey.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddStringFlag(cmd.Flags(), &c.flagRole, "role", "read-only", "", i18n.G("Role (admin or read-only)"))
	cli.AddStringFlag(cmd.Flags(), &c.flagAccessKey, "access-key", "", "", i18n.G("Access key (auto-generated if empty)"))
	cli.AddStringFlag(cmd.Flags(), &c.flagSecretKey, "secret-key", "", "", i18n.G("Secret key (auto-generated if empty)"))
	cli.AddStringFlag(cmd.Flags(), &c.flagDescription, "description", "", "", i18n.G("Key description"))

	return cmd
}

func (c *cmdStorageBucketKeyCreate) runAdd(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketKeyCreateUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	keyName := parsed[2].String

	// If a target member was specified, get the bucket with the matching name on that member, if any.
	if c.storageBucketKey.flagTarget != "" {
		d = d.UseTarget(c.storageBucketKey.flagTarget)
	}

	// If stdin isn't a terminal, read yaml from it.
	var bucketKeyPut api.StorageBucketKeyPut
	if !termios.IsTerminal(getStdinFd()) {
		loader, err := yaml.NewLoader(os.Stdin, yaml.WithKnownFields())
		if err != nil {
			return err
		}

		err = loader.Load(&bucketKeyPut)
		if err != nil && !errors.Is(err, io.EOF) {
			return err
		}
	}

	req := api.StorageBucketKeysPost{
		Name:                keyName,
		StorageBucketKeyPut: bucketKeyPut,
	}

	if c.flagRole != "" {
		req.Role = c.flagRole
	}

	if c.flagAccessKey != "" {
		req.AccessKey = c.flagAccessKey
	}

	if c.flagSecretKey != "" {
		req.SecretKey = c.flagSecretKey
	}

	if c.flagDescription != "" {
		req.Description = c.flagDescription
	}

	key, err := d.CreateStoragePoolBucketKey(poolName, bucketName, req)
	if err != nil {
		return err
	}

	if !c.global.flagQuiet {
		fmt.Printf(i18n.G("Storage bucket key %q added")+"\n", key.Name)
		fmt.Printf(i18n.G("Access key: %s")+"\n", key.AccessKey)
		fmt.Printf(i18n.G("Secret key: %s")+"\n", key.SecretKey)
	}

	return nil
}

// Delete Key.
type cmdStorageBucketKeyDelete struct {
	global           *cmdGlobal
	storageBucketKey *cmdStorageBucketKey
}

var cmdStorageBucketKeyDeleteUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.Key}

func (c *cmdStorageBucketKeyDelete) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("delete", cmdStorageBucketKeyDeleteUsage...)
	cmd.Aliases = []string{"rm", "remove"}
	cmd.Short = i18n.G("Delete key from a storage bucket")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G("Delete key from a storage bucket"))
	cmd.RunE = c.runRemove

	cli.AddStringFlag(cmd.Flags(), &c.storageBucketKey.flagTarget, "target", "", "", i18n.G("Cluster member name"))

	return cmd
}

func (c *cmdStorageBucketKeyDelete) runRemove(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketKeyDeleteUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	keyName := parsed[2].String

	// If a target member was specified, get the bucket with the matching name on that member, if any.
	if c.storageBucketKey.flagTarget != "" {
		d = d.UseTarget(c.storageBucketKey.flagTarget)
	}

	err = d.DeleteStoragePoolBucketKey(poolName, bucketName, keyName)
	if err != nil {
		return err
	}

	if !c.global.flagQuiet {
		fmt.Printf(i18n.G("Storage bucket key %q removed")+"\n", keyName)
	}

	return nil
}

// Edit Key.
type cmdStorageBucketKeyEdit struct {
	global           *cmdGlobal
	storageBucketKey *cmdStorageBucketKey
}

var cmdStorageBucketKeyEditUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.Key}

func (c *cmdStorageBucketKeyEdit) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("edit", cmdStorageBucketKeyEditUsage...)
	cmd.Short = i18n.G("Edit storage bucket key as YAML")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Edit storage bucket key as YAML`))
	cmd.Example = cli.FormatSection("", i18n.G(`incus storage bucket edit [<remote>:]<pool> <bucket> <key> < key.yaml
    Update a storage bucket key using the content of key.yaml.`))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucketKey.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketKeyEdit) helpTemplate() string {
	return i18n.G(
		`### This is a YAML representation of a storage bucket.
### Any line starting with a '# will be ignored.
###
### A storage bucket consists of a set of configuration items.
###
### name: bucket1
### used_by: []
### config:
###   size: "61203283968"`,
	)
}

func (c *cmdStorageBucketKeyEdit) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketKeyEditUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	keyName := parsed[2].String

	// If stdin isn't a terminal, read text from it
	if !termios.IsTerminal(getStdinFd()) {
		loader, err := yaml.NewLoader(os.Stdin)
		if err != nil {
			return err
		}

		// Allow output of `incus storage bucket key show` command to be passed in here, but only take the
		// contents of the StorageBucketPut fields when updating.
		// The other fields are silently discarded.
		newdata := api.StorageBucketKeyPut{}
		err = loader.Load(&newdata)
		if err != nil && !errors.Is(err, io.EOF) {
			return err
		}

		return d.UpdateStoragePoolBucketKey(poolName, bucketName, keyName, newdata, "")
	}

	// If a target was specified, edit the bucket on the given member.
	if c.storageBucketKey.flagTarget != "" {
		d = d.UseTarget(c.storageBucketKey.flagTarget)
	}

	// Get the current config.
	bucket, etag, err := d.GetStoragePoolBucketKey(poolName, bucketName, keyName)
	if err != nil {
		return err
	}

	data, err := yaml.Dump(&bucket, yaml.WithV2Defaults())
	if err != nil {
		return err
	}

	// Spawn the editor.
	content, err := cli.TextEditor("", []byte(c.helpTemplate()+"\n\n"+string(data)))
	if err != nil {
		return err
	}

	for {
		// Parse the text received from the editor
		newdata := api.StorageBucketKey{}
		err = yaml.Load(content, &newdata)
		if err == nil {
			err = d.UpdateStoragePoolBucketKey(poolName, bucketName, keyName, newdata.Writable(), etag)
		}

		// Respawn the editor
		if err != nil {
			fmt.Fprintf(os.Stderr, i18n.G("Config parsing error: %s")+"\n", err)
			fmt.Println(i18n.G("Press enter to open the editor again or ctrl+c to abort change"))

			_, err := os.Stdin.Read(make([]byte, 1))
			if err != nil {
				return err
			}

			content, err = cli.TextEditor("", content)
			if err != nil {
				return err
			}

			continue
		}

		break
	}

	return nil
}

// Show Key.
type cmdStorageBucketKeyShow struct {
	global           *cmdGlobal
	storageBucketKey *cmdStorageBucketKey
}

var cmdStorageBucketKeyShowUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.Key}

func (c *cmdStorageBucketKeyShow) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("show", cmdStorageBucketKeyShowUsage...)
	cmd.Short = i18n.G("Show storage bucket key configurations")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Show storage bucket key configurations`))
	cmd.Example = cli.FormatSection("", i18n.G(
		`incus storage bucket key show default data foo
    Will show the properties of a bucket key called "foo" for a bucket called "data" in the "default" pool.`,
	))

	cli.AddStringFlag(cmd.Flags(), &c.storageBucketKey.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketKeyShow) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketKeyShowUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	keyName := parsed[2].String

	// If a target member was specified, get the bucket with the matching name on that member, if any.
	if c.storageBucketKey.flagTarget != "" {
		d = d.UseTarget(c.storageBucketKey.flagTarget)
	}

	bucket, _, err := d.GetStoragePoolBucketKey(poolName, bucketName, keyName)
	if err != nil {
		return err
	}

	data, err := yaml.Dump(&bucket, yaml.WithV2Defaults())
	if err != nil {
		return err
	}

	fmt.Printf("%s", data)

	return nil
}

type cmdStorageBucketExport struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket

	flagCompressionAlgorithm string
	flagForce                bool
}

var cmdStorageBucketExportUsage = u.Usage{u.Pool.Remote(), u.Bucket, u.Target(u.File).Optional()}

func (c *cmdStorageBucketExport) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("export", cmdStorageBucketExportUsage...)
	cmd.Short = i18n.G("Export storage bucket")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
		`Export storage buckets as tarball.`,
	))
	cmd.Example = cli.FormatSection("", i18n.G(
		`incus storage bucket export default b1
    Download a backup tarball of the b1 storage bucket from the default pool.`,
	))

	cli.AddStringFlag(cmd.Flags(), &c.flagCompressionAlgorithm, "compression", "", "", i18n.G("Define a compression algorithm: for backup or none"))
	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagForce, "force|f", i18n.G("Force overwriting existing backup file"))

	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketExport) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketExportUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	bucketName := parsed[1].String
	hasTarget := !parsed[2].Skipped
	targetName := parsed[2].Get("." + bucketName + ".backup")

	// If a target was specified, use the bucket on the given member.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	if isStdout(targetName) {
		// If outputting to stdout, quiesce the output.
		c.global.flagQuiet = true
	} else if hasTarget && !c.flagForce && util.PathExists(targetName) {
		// Check if the target path already exists.
		return fmt.Errorf(i18n.G("Target path %q already exists"), targetName)
	}

	req := api.StorageBucketBackupsPost{
		Name:                 "",
		ExpiresAt:            time.Now().Add(23 * time.Hour),
		CompressionAlgorithm: c.flagCompressionAlgorithm,
	}

	var getter func(backupReq *incus.BackupFileRequest) error

	if d.HasExtension("direct_backup") {
		getter = func(backupReq *incus.BackupFileRequest) error {
			return d.CreateStoragePoolBucketBackupStream(poolName, bucketName, req, backupReq)
		}
	} else {
		op, err := d.CreateStoragePoolBucketBackup(poolName, bucketName, req)
		if err != nil {
			return fmt.Errorf(i18n.G("Failed to create backup: %v"), err)
		}

		// Watch the background operation.
		progress := cli.ProgressRenderer{
			Format: i18n.G("Backing up storage bucket: %s"),
			Quiet:  c.global.flagQuiet,
		}

		_, err = op.AddHandler(progress.UpdateOp)
		if err != nil {
			progress.Done("")
			return err
		}

		// Wait until backup is done.
		err = cli.CancelableWait(op, &progress)
		if err != nil {
			progress.Done("")
			return err
		}

		progress.Done("")

		err = op.Wait()
		if err != nil {
			return err
		}

		// Get name of backup.
		utStr := op.Get().Resources["backups"][0]
		uri, err := url.Parse(utStr)
		if err != nil {
			return fmt.Errorf(i18n.G("Invalid URL %q: %w"), utStr, err)
		}

		backupName, err := url.PathUnescape(path.Base(uri.EscapedPath()))
		if err != nil {
			return fmt.Errorf(i18n.G("Invalid backup name segment in path %q: %w"), uri.EscapedPath(), err)
		}

		defer func() {
			// Delete backup after we're done.
			op, err := d.DeleteStoragePoolBucketBackup(poolName, bucketName, backupName)
			if err == nil {
				_ = op.Wait()
			}
		}()

		getter = func(backupReq *incus.BackupFileRequest) error {
			_, err := d.GetStoragePoolBucketBackupFile(poolName, bucketName, backupName, backupReq)
			return err
		}
	}

	var target *os.File
	if isStdout(targetName) {
		target = os.Stdout
	} else {
		target, err = os.Create(targetName)
		if err != nil {
			return err
		}

		defer logger.WarnOnErrorExcept(target.Close, []error{os.ErrClosed}, "Failed to close target file")
	}

	// Prepare the download request.
	progress := cli.ProgressRenderer{
		Format: i18n.G("Exporting backup of storage bucket: %s"),
		Quiet:  c.global.flagForceLocal,
	}

	backupFileRequest := incus.BackupFileRequest{
		BackupFile:      io.WriteSeeker(target),
		ProgressHandler: progress.UpdateProgress,
	}

	// Export tarball.
	err = getter(&backupFileRequest)
	if err != nil {
		_ = os.Remove(targetName)
		progress.Done("")
		return fmt.Errorf(i18n.G("Failed to fetch storage bucket backup: %w"), err)
	}

	// Detect backup file type and rename file accordingly.
	if !hasTarget {
		_, err := target.Seek(0, io.SeekStart)
		if err != nil {
			return err
		}

		_, ext, _, err := archive.DetectCompressionFile(target)
		if err != nil {
			return err
		}

		err = os.Rename(targetName, bucketName+ext)
		if err != nil {
			return fmt.Errorf(i18n.G("Failed to rename export file: %w"), err)
		}
	}

	err = target.Close()
	if err != nil {
		return fmt.Errorf(i18n.G("Failed to close export file: %w"), err)
	}

	progress.Done(i18n.G("Backup exported successfully!"))

	return nil
}

// Import.
type cmdStorageBucketImport struct {
	global        *cmdGlobal
	storageBucket *cmdStorageBucket
}

var cmdStorageBucketImportUsage = u.Usage{u.Pool.Remote(), u.BackupFile, u.Bucket.Optional()}

func (c *cmdStorageBucketImport) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("import", cmdStorageBucketImportUsage...)
	cmd.Short = i18n.G("Import storage bucket")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
		`Import backups of storage buckets.`,
	))
	cmd.Example = cli.FormatSection("", i18n.G(
		`incus storage bucket import default backup0.tar.gz
		Create a new storage bucket using backup0.tar.gz as the source.`,
	))
	cli.AddStringFlag(cmd.Flags(), &c.storageBucket.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cmd.RunE = c.run

	return cmd
}

func (c *cmdStorageBucketImport) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdStorageBucketImportUsage, cmd, args)
	if err != nil {
		return err
	}

	d := parsed[0].RemoteServer
	poolName := parsed[0].RemoteObject.String
	backupFile := parsed[1].String
	bucketName := parsed[2].String

	// Use the provided target.
	if c.storageBucket.flagTarget != "" {
		d = d.UseTarget(c.storageBucket.flagTarget)
	}

	var file *os.File
	usePercentage := true
	if isStdin(backupFile) {
		file = os.Stdin
		usePercentage = false
	} else {
		file, err = os.Open(backupFile)
		if err != nil {
			return err
		}

		// The HTTP transport closes the request body, so only warn on unexpected errors.
		defer logger.WarnOnErrorExcept(file.Close, []error{os.ErrClosed}, "Failed to close file")
	}

	fstat, err := file.Stat()
	if err != nil {
		return err
	}

	progress := cli.ProgressRenderer{
		Format: i18n.G("Importing bucket: %s"),
		Quiet:  c.global.flagQuiet,
	}

	createArgs := incus.StoragePoolBucketBackupArgs{
		BackupFile: &ioprogress.ProgressReader{
			ReadCloser: file,
			Tracker: &ioprogress.ProgressTracker{
				Length: fstat.Size(),
				Handler: func(v int64, speed int64) {
					if usePercentage {
						progress.UpdateProgress(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", v, units.GetByteSizeString(speed, 2))})
					} else {
						progress.UpdateProgress(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(v, 2), units.GetByteSizeString(speed, 2))})
					}
				},
			},
		},
		Name: bucketName,
	}

	op, err := d.CreateStoragePoolBucketFromBackup(poolName, createArgs)
	if err != nil {
		return err
	}

	err = cli.CancelableWait(op, &progress)
	if err != nil {
		progress.Done("")
		return err
	}

	progress.Done("")

	return nil
}
