Skip to main content

src/barrel_att.erl

%%%-------------------------------------------------------------------
%%% @doc Attachment API for barrel_docdb
%%%
%%% Provides a higher-level API for attachment operations.
%%% Uses barrel_att_store for storage with BlobDB.
%%% @end
%%%-------------------------------------------------------------------
-module(barrel_att).

-include("barrel_docdb.hrl").

%% API
-export([
    put_attachment/5,
    get_attachment/4,
    delete_attachment/4,
    delete_doc_attachments/3,
    list_attachments/3,
    attachment_exists/4
]).

%% Utilities
-export([
    make_att_info/3,
    validate_att_name/1
]).

%%====================================================================
%% API
%%====================================================================

%% @doc Store an attachment
-spec put_attachment(barrel_att_store:att_ref(), db_name(), docid(), binary(), binary()) ->
    {ok, att_info()} | {error, term()}.
put_attachment(AttRef, DbName, DocId, AttName, Data) ->
    case validate_att_name(AttName) of
        ok ->
            barrel_att_store:put(AttRef, DbName, DocId, AttName, Data);
        {error, _} = Error ->
            Error
    end.

%% @doc Retrieve an attachment
-spec get_attachment(barrel_att_store:att_ref(), db_name(), docid(), binary()) ->
    {ok, binary()} | {error, not_found} | {error, term()}.
get_attachment(AttRef, DbName, DocId, AttName) ->
    case barrel_att_store:get(AttRef, DbName, DocId, AttName) of
        {ok, Data} -> {ok, Data};
        not_found -> {error, not_found};
        {error, _} = Error -> Error
    end.

%% @doc Delete an attachment
-spec delete_attachment(barrel_att_store:att_ref(), db_name(), docid(), binary()) ->
    ok | {error, term()}.
delete_attachment(AttRef, DbName, DocId, AttName) ->
    barrel_att_store:delete(AttRef, DbName, DocId, AttName).

%% @doc Delete all attachments for a document
-spec delete_doc_attachments(barrel_att_store:att_ref(), db_name(), docid()) ->
    ok | {error, term()}.
delete_doc_attachments(AttRef, DbName, DocId) ->
    barrel_att_store:delete_all(AttRef, DbName, DocId).

%% @doc List all attachment names for a document
-spec list_attachments(barrel_att_store:att_ref(), db_name(), docid()) -> [binary()].
list_attachments(AttRef, DbName, DocId) ->
    barrel_att_store:fold(AttRef, DbName, DocId,
        fun(Name, _Data, Acc) -> {ok, [Name | Acc]} end,
        []).

%% @doc Check if an attachment exists
-spec attachment_exists(barrel_att_store:att_ref(), db_name(), docid(), binary()) -> boolean().
attachment_exists(AttRef, DbName, DocId, AttName) ->
    case barrel_att_store:get(AttRef, DbName, DocId, AttName) of
        {ok, _} -> true;
        not_found -> false;
        {error, _} -> false
    end.

%%====================================================================
%% Utilities
%%====================================================================

%% @doc Create attachment info from data
-spec make_att_info(binary(), binary(), binary()) -> att_info().
make_att_info(AttName, Data, ContentType) ->
    Digest = compute_digest(Data),
    #{
        name => AttName,
        content_type => ContentType,
        length => byte_size(Data),
        digest => Digest
    }.

%% @doc Validate attachment name
-spec validate_att_name(binary()) -> ok | {error, invalid_att_name}.
validate_att_name(<<>>) ->
    {error, invalid_att_name};
validate_att_name(Name) when is_binary(Name) ->
    %% Check for invalid characters (control chars, path separators)
    case binary:match(Name, [<<0>>, <<"/">>, <<"\\">>]) of
        nomatch -> ok;
        _ -> {error, invalid_att_name}
    end;
validate_att_name(_) ->
    {error, invalid_att_name}.

%%====================================================================
%% Internal Functions
%%====================================================================

%% Compute SHA-256 digest of data
compute_digest(Data) ->
    Digest = crypto:hash(sha256, Data),
    <<"sha256-", (to_hex(Digest))/binary>>.

%% Convert binary to hex string
to_hex(Bin) ->
    << <<(hex_char(N))>> || <<N:4>> <= Bin >>.

hex_char(N) when N < 10 -> $0 + N;
hex_char(N) -> $a + N - 10.