summaryrefslogtreecommitdiff
path: root/src/index.erl
blob: a6849d65a2818871d92bec743e97974967efff2d (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
132
133
134
135
136
137
138
139
140
141
%%% 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, add_nosync/3, addlast_nosync/2, indexsize/1, sync/1]).

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

-spec add(string(), integer() | last, binary()) -> ok.
add(Basepath, Index, Entry) ->
    add(Basepath, Index, Entry, sync).

-spec add_nosync(string(), integer() | last, binary()) -> ok.
add_nosync(Basepath, Index, Entry) ->
    add(Basepath, Index, Entry, nosync).

add(Basepath, Index, Entry, Syncflag) 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),
            case Syncflag of
                sync ->
                    sync(Basepath);
                nosync ->
                    ok
            end;
        {error, Error} ->
            util:exit_with_error(Error, writefile,
                                 "Error opening file for writing")
    end.


-spec sync(string()) -> ok.
sync(Basepath) ->
    util:fsync([Basepath, filename:dirname(Basepath)]).

-spec addlast_nosync(string(), integer()) -> ok.
addlast_nosync(Basepath, Entry) ->
    add_nosync(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),
            file:close(File),
            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 ->
                    file:close(File),
                    noentry
            end;
        {error, Error} ->
            util:exit_with_error(Error, readfile,
                                 "Error opening file for reading")
    end.