summaryrefslogtreecommitdiff
path: root/src/rebar_pkg_resource.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/rebar_pkg_resource.erl')
-rw-r--r--src/rebar_pkg_resource.erl99
1 files changed, 93 insertions, 6 deletions
diff --git a/src/rebar_pkg_resource.erl b/src/rebar_pkg_resource.erl
index 3b44fc8..59ce0dc 100644
--- a/src/rebar_pkg_resource.erl
+++ b/src/rebar_pkg_resource.erl
@@ -23,12 +23,99 @@ needs_update(Dir, {pkg, _Name, Vsn}) ->
true
end.
-download(Dir, {pkg, Name, Vsn}, State) ->
- TmpFile = filename:join(Dir, "package.tar.gz"),
- CDN = rebar_state:get(State, rebar_packages_cdn, "https://s3.amazonaws.com/s3.hex.pm/tarballs"),
- Url = string:join([CDN, binary_to_list(<<Name/binary, "-", Vsn/binary, ".tar">>)], "/"),
- {ok, saved_to_file} = httpc:request(get, {Url, []}, [], [{stream, TmpFile}]),
- {tarball, TmpFile}.
+download(TmpDir, Pkg={pkg, Name, Vsn}, State) ->
+ CDN = rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN),
+ PackageDir = rebar_packages:package_dir(State),
+ Package = binary_to_list(<<Name/binary, "-", Vsn/binary, ".tar">>),
+ CachePath = filename:join(PackageDir, Package),
+ Url = string:join([CDN, Package], "/"),
+ cached_download(TmpDir, CachePath, Pkg, Url, etag(CachePath), State).
+
+cached_download(TmpDir, CachePath, Pkg, Url, ETag, State) ->
+ case request(Url, ETag) of
+ {ok, cached} ->
+ serve_from_cache(TmpDir, CachePath, Pkg, State);
+ {ok, Body, NewETag} ->
+ serve_from_download(TmpDir, CachePath, Pkg, NewETag, Body, State);
+ error when ETag =/= false ->
+ ?DEBUG("Download ~s error, using ~s from cache", [Url, CachePath]),
+ serve_from_cache(TmpDir, CachePath, Pkg, State);
+ error ->
+ request_failed
+ end.
+
+serve_from_cache(TmpDir, CachePath, Pkg, State) ->
+ {Files, Contents, Version, Meta} = extract(TmpDir, CachePath),
+ case checksums(Pkg, Files, Contents, Version, Meta, State) of
+ {Chk, Chk, Chk} ->
+ ok = erl_tar:extract({binary, Contents}, [{cwd, TmpDir}, compressed]),
+ {ok, true};
+ {_Bin, Chk, Chk} ->
+ ?DEBUG("Checksums: registry: ~p, pkg: ~p", [Chk, _Bin]),
+ {failed_extract, CachePath};
+ {Chk, _Reg, Chk} ->
+ ?DEBUG("Checksums: registry: ~p, pkg: ~p", [_Reg, Chk]),
+ {bad_registry_checksum, CachePath};
+ {_Bin, _Reg, _Tar} ->
+ ?DEBUG("Checksums: registry: ~p, pkg: ~p, meta: ~p", [_Reg, _Bin, _Tar]),
+ {bad_checksum, CachePath}
+ end.
+
+serve_from_download(TmpDir, CachePath, Package, ETag, Binary, State) ->
+ ?DEBUG("Writing ~p to cache at ~s", [Package, CachePath]),
+ file:write_file(CachePath, Binary),
+ case etag(CachePath) of
+ ETag ->
+ serve_from_cache(TmpDir, CachePath, Package, State);
+ FileETag ->
+ ?DEBUG("Downloaded file ~s ETag ~s doesn't match returned ETag ~s", [CachePath, ETag, FileETag]),
+ {bad_download, CachePath}
+ end.
+
+
+extract(TmpDir, CachePath) ->
+ ec_file:mkdir_p(TmpDir),
+ {ok, Files} = erl_tar:extract(CachePath, [memory]),
+ {"contents.tar.gz", Contents} = lists:keyfind("contents.tar.gz", 1, Files),
+ {"VERSION", Version} = lists:keyfind("VERSION", 1, Files),
+ {"metadata.config", Meta} = lists:keyfind("metadata.config", 1, Files),
+ {Files, Contents, Version, Meta}.
+
+checksums(Pkg, Files, Contents, Version, Meta, State) ->
+ Blob = <<Version/binary, Meta/binary, Contents/binary>>,
+ <<X:256/big-unsigned>> = crypto:hash(sha256, Blob),
+ BinChecksum = list_to_binary(string:to_upper(lists:flatten(io_lib:format("~64.16.0b", [X])))),
+ RegistryChecksum = rebar_packages:registry_checksum(Pkg, State),
+ {"CHECKSUM", TarChecksum} = lists:keyfind("CHECKSUM", 1, Files),
+ {BinChecksum, RegistryChecksum, TarChecksum}.
make_vsn(_) ->
{error, "Replacing version of type pkg not supported."}.
+
+request(Url, ETag) ->
+ case httpc:request(get, {Url, [{"if-none-match", ETag} || ETag =/= false]},
+ [{relaxed, true}],
+ [{body_format, binary}]) of
+ {ok, {{_Version, 200, _Reason}, Headers, Body}} ->
+ ?DEBUG("Successfully downloaded ~s", [Url]),
+ {"etag", ETag1} = lists:keyfind("etag", 1, Headers),
+ {ok, Body, string:strip(ETag1, both, $")};
+ {ok, {{_Version, 304, _Reason}, _Headers, _Body}} ->
+ ?DEBUG("Cached copy of ~s still valid", [Url]),
+ {ok, cached};
+ {ok, {{_Version, Code, _Reason}, _Headers, _Body}} ->
+ ?DEBUG("Request to ~p failed: status code ~p", [Url, Code]),
+ error;
+ {error, Reason} ->
+ ?DEBUG("Request to ~p failed: ~p", [Url, Reason]),
+ error
+ end.
+
+etag(Path) ->
+ case file:read_file(Path) of
+ {ok, Binary} ->
+ <<X:128/big-unsigned-integer>> = crypto:hash(md5, Binary),
+ string:to_lower(lists:flatten(io_lib:format("~32.16.0b", [X])));
+ {error, _} ->
+ false
+ end.