summaryrefslogtreecommitdiff
path: root/src/index.erl
blob: 96195e3a8d56bc36cacff7a3f69f61331b1701a8 (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
%%% Copyright (c) 2014, NORDUnet A/S.
%%% See LICENSE for licensing information.

%% Implements an interface to a file pair (basename and
%% basename.chksum) that stores an ordered list of fixed-size entries.
%% Entries can be added at the end and are retrieved by index. Entries
%% can also be added at already existing indices, but then the
%% contents must be the same.
%%
%% Writes(add, addlast) need to be serialized.

%% TODO: Checksums

-module(index).
-export([get/2, getrange/3, add/3, addlast/2, indexsize/1]).

-define(ENTRYSIZE, 32).
-define(ENTRYSIZEINFILE, (?ENTRYSIZE*2+1)).

-spec add(string(), integer() | last, binary()) -> ok.
add(Basepath, Index, Entry) when is_binary(Entry), size(Entry) == ?ENTRYSIZE ->
    case file:open(Basepath, [read, write, binary]) of
        {ok, File} ->
            {ok, Position} = file:position(File, eof),
            Mode = case Index of
                       last when Position rem ?ENTRYSIZEINFILE == 0 ->
                           write;
                       Index when is_integer(Index),
                                  Index * ?ENTRYSIZEINFILE == Position ->
                           write;
                       Index when is_integer(Index),
                                  Index * ?ENTRYSIZEINFILE < Position ->
                           read;
                       _ ->
                           util:exit_with_error(invalid, writefile,
                                                "Index not valid")
                   end,
            EntryText = hex:bin_to_hexstr(Entry) ++ "\n",
            case Mode of
                write ->
                    ok = file:write(File, EntryText);
                read ->
                    {ok, _Position} =
                        file:position(File, {bof, Index * ?ENTRYSIZEINFILE}),
                    {ok, OldEntryText} = file:read(File, ?ENTRYSIZEINFILE),
                    %% check that the written content is the same as
                    %% the old content
                    case binary_to_list(OldEntryText) of
                        EntryText ->
                            ok;
                        _ ->
                            util:exit_with_error(invalid, writefile,
                                                 "Written content not the" ++
                                                     " same as old content")
                    end
            end,
            ok = file:close(File),
            util:fsync([Basepath, filename:dirname(Basepath)]);
        {error, Error} ->
            util:exit_with_error(Error, writefile,
                                 "Error opening file for writing")
    end.


-spec addlast(string(), integer()) -> ok.
addlast(Basepath, Entry) ->
    add(Basepath, last, Entry).

decodedata(Binary) ->
    lists:reverse(decodedata(Binary, [])).

decodedata(<<>>, Acc) ->
    Acc;
decodedata(<<Entry:?ENTRYSIZE/binary-unit:16, "\n", Rest/binary>>, Acc) ->
    decodedata(Rest, [mochihex:to_bin(binary_to_list(Entry)) | Acc]);
decodedata(<<_:?ENTRYSIZE/binary-unit:16, _>>, _Acc) ->
    util:exit_with_error(badformat, readindex,
                         "Index line not ending with linefeed").

-spec indexsize(string()) -> integer().
indexsize(Basepath) ->
    case file:open(Basepath, [read, binary]) of
        {ok, File} ->
            {ok, Filesize} = file:position(File, eof),
            lager:debug("file ~p size ~p", [Basepath, Filesize]),
            Filesize div ?ENTRYSIZEINFILE;
        {error, Error} ->
            util:exit_with_error(Error, readfile,
                                 "Error opening file for reading")
    end.

-spec get(string(), integer()) -> binary() | noentry.
get(Basepath, Index) ->
    case getrange(Basepath, Index, Index) of
        noentry ->
            noentry;
        [Entry] ->
            Entry
    end.

-spec getrange(string(), integer(), integer()) -> [binary()].
getrange(Basepath, Start, End) when Start =< End ->
    lager:debug("path ~p start ~p end ~p", [Basepath, Start, End]),
    case file:open(Basepath, [read, binary]) of
        {ok, File} ->
            {ok, Filesize} = file:position(File, eof),
            if
                End * ?ENTRYSIZEINFILE + ?ENTRYSIZEINFILE =< Filesize ->
                    {ok, _Position} = file:position(File,
                                                    Start * ?ENTRYSIZEINFILE),
                    {ok, EntryText} =
                        file:read(File, ?ENTRYSIZEINFILE * (End - Start + 1)),
                    Entry = decodedata(EntryText),
                    lager:debug("entries ~p", [length(Entry)]),
                    file:close(File),
                    Entry;
                true ->
                    noentry
            end;
        {error, Error} ->
            util:exit_with_error(Error, readfile,
                                 "Error opening file for reading")
    end.