summaryrefslogtreecommitdiff
path: root/src/rebar_pkg_resource.erl
blob: f6bb29b11bc3841cd9db3a8145773b998e36a7b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ts=4 sw=4 et
-module(rebar_pkg_resource).

-behaviour(rebar_resource).

-export([lock/2
        ,download/3
        ,needs_update/2
        ,make_vsn/1]).

-include("rebar.hrl").

-define(DEFAULT_CDN, "https://s3.amazonaws.com/s3.hex.pm/tarballs").

lock(_AppDir, Source) ->
    Source.

needs_update(Dir, {pkg, _Name, Vsn}) ->
    [AppInfo] = rebar_app_discover:find_apps([Dir], all),
    case rebar_app_info:original_vsn(AppInfo) =:= ec_cnv:to_list(Vsn) of
        true ->
            false;
        false ->
            true
    end.

download(TmpDir, Pkg={pkg, Name, Vsn}, State) ->
    CDN = rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN),
    PackageDir = hex_package_dir(CDN, 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} ->
            {failed_extract, CachePath};
        {Chk, _Reg, Chk} ->
            {bad_registry_checksum, CachePath};
        {_Bin, _Reg, _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("Download ETag ~s doesn't match returned ETag ~s", [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_sum(Pkg, State),
    {"CHECKSUM", TarChecksum} = lists:keyfind("CHECKSUM", 1, Files),
    {BinChecksum, RegistryChecksum, TarChecksum}.

make_vsn(_) ->
    {error, "Replacing version of type pkg not supported."}.

%% Use the shared hex package directory unless a non-default package repo is used
hex_package_dir(?DEFAULT_CDN, _) ->
    filename:join([rebar_dir:home_dir(), ".hex", "packages"]);
hex_package_dir(CDN, State) ->
    CacheDir = rebar_dir:global_cache_dir(State),
    {ok, {_, _, Host, _, _, _}} = http_uri:parse(CDN),
    CDNPath = filename:join(lists:reverse(string:tokens(Host, "."))),
    PackageDir = filename:join([CacheDir, "hex", CDNPath, "packages"]),
    ok = filelib:ensure_dir(filename:join(PackageDir, "placeholder")),
    PackageDir.

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.