lib/mix/tasks/hf.upload.ex

defmodule Mix.Tasks.Hf.Upload do
  @shortdoc "Upload a file or folder to the HuggingFace Hub"
  @moduledoc """
  Uploads a file or folder to a HuggingFace Hub repository.

      $ mix hf.upload my-org/my-model ./model.safetensors
      $ mix hf.upload my-org/my-model ./outputs/ --path-in-repo models/v2/
      $ mix hf.upload --type dataset my-org/my-dataset ./data/

  ## Options

    * `--type` / `-t` — repo type: `model`, `dataset`, `space` (default: `model`)
    * `--path-in-repo` — destination path in the repo (default: same as local name)
    * `--commit-message` / `-m` — commit message
    * `--revision` / `-r` — branch to push to (default: `main`)
    * `--token` — HF API token
  """

  use Mix.Task

  @impl Mix.Task
  def run(args) do
    {opts, argv, _} =
      OptionParser.parse(args,
        aliases: [t: :type, m: :commit_message, r: :revision],
        strict: [
          type: :string,
          path_in_repo: :string,
          commit_message: :string,
          revision: :string,
          token: :string
        ]
      )

    [repo_id, local_path | _] =
      case argv do
        [_, _ | _] = v -> v
        _ -> Mix.raise("Usage: mix hf.upload REPO_ID LOCAL_PATH")
      end

    token = opts[:token] || HuggingfaceClient.Config.token()
    unless token, do: Mix.raise("Not logged in. Run: mix hf.login --token hf_xxx")

    case File.stat!(local_path).type do
      :directory -> upload_folder(repo_id, local_path, opts, token)
      _ -> upload_file(repo_id, local_path, opts, token)
    end
  end

  defp upload_folder(repo_id, local_path, opts, token) do
    Mix.shell().info("Uploading folder #{local_path} to #{repo_id}...")

    up_opts = [
      folder_path: local_path,
      path_in_repo: opts[:path_in_repo] || "",
      commit_title: opts[:commit_message] || "Upload via mix hf.upload",
      revision: opts[:revision] || "main",
      type: String.to_existing_atom(opts[:type] || "model"),
      access_token: token
    ]

    case HuggingfaceClient.Hub.Repos.upload_folder(repo_id, up_opts) do
      {:ok, commit} -> Mix.shell().info("✓ Uploaded. Commit: #{commit["commitId"] || commit["id"]}")
      {:error, err} -> Mix.raise("Upload failed: #{Exception.message(err)}")
    end
  end

  defp upload_file(repo_id, local_path, opts, token) do
    dest = opts[:path_in_repo] || Path.basename(local_path)
    message = opts[:commit_message] || "Upload #{dest} via mix hf.upload"
    Mix.shell().info("Uploading #{local_path}#{repo_id}/#{dest}...")

    up_opts = [
      path: dest,
      content: File.read!(local_path),
      commit_title: message,
      revision: opts[:revision] || "main",
      type: String.to_existing_atom(opts[:type] || "model"),
      access_token: token
    ]

    case HuggingfaceClient.Hub.Commits.upload_file(repo_id, up_opts) do
      {:ok, _} -> Mix.shell().info("✓ Uploaded to #{repo_id}/#{dest}")
      {:error, err} -> Mix.raise("Upload failed: #{Exception.message(err)}")
    end
  end
end