View Source ExAttr (ExAttr v2.0.0)

Simple utility library that performs native xattr operations using rustler and the xattr crate created by Steven Allen

Rational

I was disappointed to see that there was no native interface within Elixir or Erlang's standard libraries for managing extended attributes. While this can technically be worked around by simply wrapping the setfattr and getfattr commands, I wasn't happy with the performance of this approach.

Additionally, while there are similar libraries that handle xattr operations for Elixir, I'm picky about how I handle serialization and was looking for something less opinionated and "dumb" that I could easily wrap my application specific logic around.

Since I couldn't find anything that fit this criteria I figured I'd just do it myself, and so here we are!

Namespacing Advise

If you want to add some custom-application-specific xattrs to a file, it's generally a good idea to do this all under a common namespace specific to your application. Since POSIX extended attributes already has some built-in namespacing that differentiates between trusted, system, user, etc, it is highly likely you will need to make your namespace a child of the user namespace (ex: "user.<my_namespace>") unless you are doing SELinux stuff.

This is most easily abstracted away by simply wrapping the functions of this library to auto handle all that for you.

Here is an example of what I mean:

defmodule MyApp.XAttr do
  @namespace "user.my_apps_namespace"

  def set(path, name, value) do
    name = "#{@namespace}.#{name}"
    ExAttr.set(path, name, value)
  end

  def get(path, name) do
    name = "#{@namespace}.#{name}"
    ExAttr.get(path, name)
  end

  def list(path) do
    case ExAttr.list(path) do
      {:ok, list} ->
        list
        |> Enum.filter(&match?("#{@namespace}." <> _, &1))
        |> Enum.map(fn "#{@namespace}." <> name -> name end)
        |> then(fn list ->
          {:ok, list}
        end)

      {:error, reason} ->
        {:error, reason}
    end
  end

  # And so on...
end

Error Handling

When possible, POSIX errors are normalized as atoms to be compatible with the error semantics of the File & :file modules. If it's either not a POSIX error, or one I never anticipated, then it will fallback to being a generic string error instead.

Since these align with existing semantics of erlang, you can use :file.format_error/1 to get the error string for any atoms returned here. Alternativey you can get them directly via :erl_posix_msg.message/1 which :file.format_error/1 uses under the hood.

Below is a table that lists all possible POSIX errors and what could trigger them, the description used might not align with what :file.format_error/1 returns.

ErrorDescription
:e2bigAttribute value is too big
:eaccesPermission denied
:einvalInvalid argument
:eioGeneric I/O Error
:enodataAttribute does not exist (aliased with enoattr)
:enoentDoes not exist
:enomemOut of memory issue, rare
:enospcNo space on device
:epermOperation not permitted
:erofsRead-only filesystem
:enotsupFilesystem or platform not supported

Summary

Functions

Dumps map of extended attributes for the specified file.

Dumps map of extended attributes for the specified file, raises on error.

Get an extended attribute for the specified file.

Get an extended attribute for the specified file, raises on error

List extended attributes attached to the specified file.

List extended attributes attached to the specified file, raises on error.

Remove an extended attribute from the specified file.

Remove an extended attribute from the specified file, raises on error.

Set an extended attribute on the specified file. Passing a nil value will remove the attribute.

Set an extended attribute on the specified file. Passing a nil value will remove the attribute, raises on error.

Forwards the result of the xattr:SUPPORTED_PLATFORM constant from the xattr crate this library wraps.

Types

@type error_reason() ::
  String.t()
  | :e2big
  | :eacces
  | :einval
  | :eio
  | :enodata
  | :enoent
  | :enomem
  | :enospc
  | :eperm
  | :erofs
  | :enotsup
@type name() :: String.t()
@type result() :: :ok | {:error, error_reason()}
@type result(t) :: {:ok, t} | {:error, error_reason()}
@type value() :: String.t() | nil

Functions

@spec dump(Path.t()) :: result(%{required(name()) => value()})

Dumps map of extended attributes for the specified file.

Note

This may not list all attributes. Speficially, it definitely won’t list any trusted attributes unless you are root and it may not list system attributes.

Examples

iex> :ok = ExAttr.set("test.txt", "user.foo", "bar")
iex> :ok = ExAttr.set("test.txt", "user.bar", "foo")
iex> :ok = ExAttr.set("test.txt", "user.test", "example")
iex> ExAttr.dump("test.txt")
{:ok, %{"user.bar" => "foo", "user.foo" => "bar", "user.test" => "example"}}
@spec dump!(Path.t()) :: %{required(name()) => value()}

Dumps map of extended attributes for the specified file, raises on error.

Note

This may not list all attributes. Speficially, it definitely won’t list any trusted attributes unless you are root and it may not list system attributes.

@spec get(Path.t(), name()) :: result(value())

Get an extended attribute for the specified file.

Examples

iex> ExAttr.get("test.txt", "user.foo")
{:ok, nil}
iex> ExAttr.set("test.txt", "user.foo", "123")
:ok
iex> ExAttr.get("test.txt", "user.foo")
{:ok, "123"}
@spec get!(Path.t(), name()) :: value()

Get an extended attribute for the specified file, raises on error

@spec list(Path.t()) :: result([name()])

List extended attributes attached to the specified file.

Note

This may not list all attributes. Speficially, it definitely won’t list any trusted attributes unless you are root and it may not list system attributes.

Examples

iex> :ok = ExAttr.set("test.txt", "user.foo", "bar")
iex> :ok = ExAttr.set("test.txt", "user.bar", "foo")
iex> :ok = ExAttr.set("test.txt", "user.test", "example")
iex> ExAttr.list("test.txt")
{:ok, ["user.test", "user.bar", "user.foo"]}
@spec list!(Path.t()) :: [name()]

List extended attributes attached to the specified file, raises on error.

Note

This may not list all attributes. Speficially, it definitely won’t list any trusted attributes unless you are root and it may not list system attributes.

@spec remove(Path.t(), name()) :: result()

Remove an extended attribute from the specified file.

Examples

iex> ExAttr.set("test.txt", "user.foo", "123")
:ok
iex> ExAttr.get("test.txt", "user.foo")
{:ok, "123"}
iex> ExAttr.remove("test.txt", "user.foo")
:ok
iex> ExAttr.get("test.txt", "user.foo")
{:ok, nil}
iex> ExAttr.remove("test.txt", "user.foo")
{:error, "No data available (os error 61)"}
@spec remove!(Path.t(), name()) :: :ok

Remove an extended attribute from the specified file, raises on error.

@spec set(Path.t(), name(), value()) :: result()

Set an extended attribute on the specified file. Passing a nil value will remove the attribute.

Examples

iex> ExAttr.set("test.txt", "user.foo", "123")
:ok
iex> ExAttr.get("test.txt", "user.foo")
{:ok, "123"}
iex> ExAttr.set("test.txt", "user.foo", nil)
:ok
iex> ExAttr.get("test.txt", "user.foo")
{:ok, nil}
@spec set!(Path.t(), name(), value()) :: :ok

Set an extended attribute on the specified file. Passing a nil value will remove the attribute, raises on error.

@spec supported_platform() :: boolean()

Forwards the result of the xattr:SUPPORTED_PLATFORM constant from the xattr crate this library wraps.

Returns true if platform is supported.