Skip to main content

lib/ex_storage_service_cli.ex

defmodule ExStorageServiceCli do
  @moduledoc """
  CLI tool for ExStorageService S3-compatible object storage.

  Provides an `ess` command-line interface for managing buckets and objects.

  ## Usage

      ess <command> [options]

  ## Commands

      configure         Set up access credentials and endpoint
      mb <bucket>       Make (create) a bucket
      rb <bucket>       Remove (delete) a bucket
      ls [bucket[/prefix]]  List buckets or objects
      tree <bucket[/prefix]>  Display objects as a directory tree
      cp <src> <dst>    Copy files (upload/download)
      rm <target>       Remove an object
      mv <src> <dst>    Move an object (copy + delete)
      presign <target>  Generate a presigned URL
      info              Show server health info
      version           Print CLI version

  ## Global Options

      --endpoint <url>     S3 endpoint (default: http://localhost:9000)
      --profile <name>     Use a named profile
      --access-key <id>    Override access key ID
      --secret-key <key>   Override secret access key
      --region <region>    AWS region (default: us-east-1)
      --json               Output in JSON format
      --no-color           Disable colored output
      -h, --help           Show help
  """

  @version Mix.Project.config()[:version]

  alias ExStorageServiceCli.Config
  alias ExStorageServiceCli.Output
  alias ExStorageServiceCli.Commands

  @commands %{
    "configure" => Commands.Configure,
    "mb" => Commands.Bucket,
    "rb" => Commands.Bucket,
    "ls" => Commands.Ls,
    "tree" => Commands.Tree,
    "cp" => Commands.Cp,
    "rm" => Commands.Rm,
    "mv" => Commands.Mv,
    "presign" => Commands.Presign,
    "info" => Commands.Info,
    "version" => Commands.Version
  }

  def main(args) do
    {global_opts, rest, _invalid} =
      OptionParser.parse_head(args,
        strict: [
          endpoint: :string,
          profile: :string,
          access_key: :string,
          secret_key: :string,
          region: :string,
          json: :boolean,
          no_color: :boolean,
          help: :boolean
        ],
        aliases: [h: :help]
      )

    if global_opts[:no_color] do
      Application.put_env(:elixir, :ansi_enabled, false)
    end

    ctx = build_context(global_opts)

    case rest do
      [] ->
        if global_opts[:help] do
          print_help()
        else
          print_help()
        end

      [command | cmd_args] ->
        if global_opts[:help] do
          dispatch_help(command)
        else
          dispatch(command, cmd_args, ctx)
        end
    end
  end

  @doc """
  Returns the current CLI version.
  """
  def version, do: @version

  defp build_context(opts) do
    profile_name = opts[:profile] || "default"
    profile = Config.load_profile(profile_name)

    %{
      endpoint: opts[:endpoint] || profile[:endpoint] || "http://localhost:9000",
      access_key_id: opts[:access_key] || profile[:access_key_id],
      secret_access_key: opts[:secret_key] || profile[:secret_access_key],
      region: opts[:region] || profile[:region] || "us-east-1",
      json: opts[:json] || false,
      profile: profile_name
    }
  end

  defp dispatch(command, args, ctx) do
    case Map.get(@commands, command) do
      nil ->
        Output.error("Unknown command: #{command}")
        print_help()
        System.halt(1)

      module ->
        try do
          module.run(command, args, ctx)
        rescue
          e ->
            Output.error("#{Exception.message(e)}")
            System.halt(1)
        end
    end
  end

  defp dispatch_help(command) do
    case Map.get(@commands, command) do
      nil ->
        Output.error("Unknown command: #{command}")
        print_help()

      module ->
        module.help(command)
    end
  end

  defp print_help do
    IO.puts("""
    #{IO.ANSI.bright()}ess#{IO.ANSI.reset()} — ExStorageService CLI v#{@version}

    #{IO.ANSI.bright()}USAGE#{IO.ANSI.reset()}
        ess <command> [options]

    #{IO.ANSI.bright()}COMMANDS#{IO.ANSI.reset()}
        configure             Set up access credentials and endpoint
        mb <bucket>           Make (create) a bucket
        rb <bucket>           Remove (delete) a bucket
        ls [bucket[/prefix]]  List buckets or objects
        tree <bucket[/prefix]>  Display objects as a directory tree
        cp <src> <dst>        Copy files (upload/download)
        rm s3://<bucket>/<key>  Remove an object
        mv <src> <dst>        Move an object (copy + delete)
        presign s3://<b>/<k>  Generate a presigned URL
        info                  Show server health info
        version               Print CLI version

    #{IO.ANSI.bright()}GLOBAL OPTIONS#{IO.ANSI.reset()}
        --endpoint <url>      S3 endpoint (default: http://localhost:9000)
        --profile <name>      Use a named profile
        --access-key <id>     Override access key ID
        --secret-key <key>    Override secret access key
        --region <region>     AWS region (default: us-east-1)
        --json                Output in JSON format
        --no-color            Disable colored output
        -h, --help            Show help

    #{IO.ANSI.bright()}EXAMPLES#{IO.ANSI.reset()}
        ess configure
        ess mb my-bucket
        ess ls
        ess tree my-bucket
        ess cp ./file.txt s3://my-bucket/file.txt
        ess cp s3://my-bucket/file.txt ./downloaded.txt
        ess ls my-bucket --json
        ess presign s3://my-bucket/file.txt --expires 3600
    """)
  end
end