Packages

macula

0.37.7
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_content_system macula_content.erl
Raw

src/macula_content_system/macula_content.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% Public API facade for Macula content-addressed storage system.
%%%
%%% Provides a simple, high-level interface for content operations:
%%% - Publishing: Store content and announce to mesh
%%% - Fetching: Download content from mesh peers
%%% - Verification: Ensure content integrity via Merkle proofs
%%%
%%% == Quick Start ==
%%% ```
%%% %% Publish content (returns manifest MCID)
%%% {ok, MCID} = macula_content:publish("/path/to/release.tar.gz").
%%%
%%% %% Fetch content by MCID
%%% {ok, FilePath} = macula_content:fetch(MCID, #{output_dir => "/tmp"}).
%%%
%%% %% Check if content is available locally
%%% true = macula_content:is_local(MCID).
%%% '''
%%%
%%% == MCID Format ==
%%% MCIDs are 34-byte content identifiers:
%%% - Version (1 byte): Protocol version
%%% - Codec (1 byte): Content type (0x55 = raw, 0x56 = manifest)
%%% - Hash (32 bytes): BLAKE3 or SHA256 content hash
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_content).
%% MCID Operations
-export([
mcid/1,
mcid/2,
mcid_to_string/1,
mcid_from_string/1
]).
%% Manifest Operations
-export([
create_manifest/1,
create_manifest/2,
verify_manifest/2,
get_chunks/1,
missing_chunks/1
]).
%% Content Operations
-export([
store/1,
store/2,
fetch/1,
fetch/2,
is_local/1
]).
%% Status
-export([
status/0
]).
%% Types
-type mcid() :: <<_:272>>. %% 34 bytes
-type manifest() :: map().
-type chunk_info() :: map().
-export_type([mcid/0, manifest/0, chunk_info/0]).
%% Constants
-define(VERSION, 1).
-define(CODEC_RAW, 16#55).
-define(CODEC_MANIFEST, 16#56).
%%%===================================================================
%%% MCID Operations
%%%===================================================================
%% @doc Create MCID from binary data using default algorithm (BLAKE3).
-spec mcid(binary()) -> mcid().
mcid(Data) ->
mcid(Data, #{}).
%% @doc Create MCID from binary data with options.
%% Options:
%% - hash_algorithm: blake3 | sha256 (default: blake3)
-spec mcid(binary(), map()) -> mcid().
mcid(Data, Opts) ->
Algorithm = maps:get(hash_algorithm, Opts, blake3),
Hash = macula_content_hasher:hash(Algorithm, Data),
Codec = ?CODEC_RAW,
<<?VERSION:8, Codec:8, Hash/binary>>.
%% @doc Convert MCID to human-readable string.
%% Format: mcid1-{codec}-{algorithm}-{hash_hex}
-spec mcid_to_string(mcid()) -> binary().
mcid_to_string(MCID) ->
macula_content_manifest:mcid_to_string(MCID).
%% @doc Parse MCID from string representation.
-spec mcid_from_string(binary()) -> {ok, mcid()} | {error, invalid_mcid}.
mcid_from_string(Str) ->
macula_content_manifest:mcid_from_string(Str).
%%%===================================================================
%%% Manifest Operations
%%%===================================================================
%% @doc Create manifest from binary data.
-spec create_manifest(binary()) -> {ok, manifest()}.
create_manifest(Data) ->
create_manifest(Data, #{}).
%% @doc Create manifest from binary data with options.
%% Options:
%% - name: Content name (default: "unnamed")
%% - chunk_size: Chunk size in bytes (default: 262144 / 256KB)
%% - hash_algorithm: blake3 | sha256 (default: blake3)
-spec create_manifest(binary(), map()) -> {ok, manifest()}.
create_manifest(Data, Opts) ->
%% macula_content_manifest:create returns {ok, Map}
macula_content_manifest:create(Data, Opts).
%% @doc Verify data against manifest.
%% Returns true if data matches manifest's root hash.
-spec verify_manifest(manifest(), binary()) -> boolean().
verify_manifest(Manifest, Data) when is_map(Manifest) ->
case macula_content_manifest:verify(Manifest, Data) of
ok -> true;
{error, _} -> false
end.
%% @doc Get list of chunks from manifest.
-spec get_chunks(manifest()) -> [chunk_info()].
get_chunks(#{chunks := Chunks}) ->
Chunks;
get_chunks(_) ->
[].
%% @doc Get list of chunks not yet stored locally.
-spec missing_chunks(manifest()) -> [chunk_info()].
missing_chunks(Manifest) ->
Chunks = get_chunks(Manifest),
Algorithm = maps:get(hash_algorithm, Manifest, blake3),
lists:filter(fun(Chunk) ->
ChunkHash = maps:get(hash, Chunk),
ChunkMCID = make_chunk_mcid(ChunkHash, Algorithm),
not macula_content_store:has_block(ChunkMCID)
end, Chunks).
%% @private Create chunk MCID from hash.
make_chunk_mcid(Hash, _Algorithm) ->
<<?VERSION:8, ?CODEC_RAW:8, Hash/binary>>.
%%%===================================================================
%%% Content Operations
%%%===================================================================
%% @doc Store binary content locally.
%% Creates manifest, chunks data, stores blocks.
-spec store(binary()) -> {ok, manifest()} | {error, term()}.
store(Data) ->
store(Data, #{}).
%% @doc Store binary content with options.
-spec store(binary(), map()) -> {ok, manifest()} | {error, term()}.
store(Data, Opts) ->
{ok, Manifest} = create_manifest(Data, Opts),
%% Store each chunk as a block
ChunkSize = maps:get(chunk_size, Manifest, 262144),
{ok, Chunks} = macula_content_chunker:chunk(Data, ChunkSize),
Algorithm = maps:get(hash_algorithm, Manifest, blake3),
lists:foreach(fun(ChunkData) ->
Hash = macula_content_hasher:hash(Algorithm, ChunkData),
ChunkMCID = <<?VERSION:8, ?CODEC_RAW:8, Hash/binary>>,
macula_content_store:put_block(ChunkMCID, ChunkData)
end, Chunks),
%% Store manifest (manifest is already a map)
macula_content_store:put_manifest(Manifest),
{ok, Manifest}.
%% @doc Fetch content by MCID.
%% If available locally, returns immediately.
%% Otherwise, discovers providers and downloads.
-spec fetch(mcid()) -> {ok, binary()} | {error, term()}.
fetch(MCID) ->
fetch(MCID, #{}).
%% @doc Fetch content with options.
%% Options:
%% - verify: Verify Merkle root after download (default: true)
%% - timeout: Download timeout in ms (default: 300000 / 5 min)
-spec fetch(mcid(), map()) -> {ok, binary()} | {error, term()}.
fetch(MCID, _Opts) ->
%% Try local first
case macula_content_store:get_manifest(MCID) of
{ok, Manifest} ->
fetch_from_local(Manifest);
{error, not_found} ->
%% Would discover from DHT and download from peers
%% For now, return error
{error, not_found}
end.
%% @doc Check if content is available locally.
-spec is_local(mcid()) -> boolean().
is_local(MCID) ->
case macula_content_store:get_manifest(MCID) of
{ok, _Manifest} ->
true;
{error, _} ->
%% Also check if it's a block MCID
macula_content_store:has_block(MCID)
end.
%%%===================================================================
%%% Status
%%%===================================================================
%% @doc Get content system status.
-spec status() -> map().
status() ->
handle_status_result(catch macula_content_store:stats()).
%% @private Handle status result
handle_status_result({'EXIT', _}) ->
#{enabled => false, error => store_unavailable};
handle_status_result(Stats) when is_map(Stats) ->
Stats#{enabled => true};
handle_status_result(_) ->
#{enabled => false, error => store_unavailable}.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @private Fetch content from local storage using manifest map.
fetch_from_local(#{chunks := Chunks, hash_algorithm := Algorithm}) ->
ChunkDataList = lists:map(fun(#{hash := Hash}) ->
ChunkMCID = make_chunk_mcid(Hash, Algorithm),
case macula_content_store:get_block(ChunkMCID) of
{ok, Data} -> Data;
{error, _} -> <<>>
end
end, Chunks),
Data = macula_content_chunker:reassemble(ChunkDataList),
{ok, Data};
fetch_from_local(#{chunks := Chunks}) ->
%% Fallback without algorithm - try raw hash extraction
ChunkDataList = lists:map(fun(#{hash := Hash}) ->
ChunkMCID = make_chunk_mcid(Hash, blake3),
case macula_content_store:get_block(ChunkMCID) of
{ok, Data} -> Data;
{error, _} -> <<>>
end
end, Chunks),
Data = macula_content_chunker:reassemble(ChunkDataList),
{ok, Data}.