| %% @author Bob Ippolito <bob@mochimedia.com> |
| %% @copyright 2010 Mochi Media, Inc. |
| %% |
| %% Permission is hereby granted, free of charge, to any person obtaining a |
| %% copy of this software and associated documentation files (the "Software"), |
| %% to deal in the Software without restriction, including without limitation |
| %% the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| %% and/or sell copies of the Software, and to permit persons to whom the |
| %% Software is furnished to do so, subject to the following conditions: |
| %% |
| %% The above copyright notice and this permission notice shall be included in |
| %% all copies or substantial portions of the Software. |
| %% |
| %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
| %% DEALINGS IN THE SOFTWARE. |
| |
| %% @doc Write newline delimited log files, ensuring that if a truncated |
| %% entry is found on log open then it is fixed before writing. Uses |
| %% delayed writes and raw files for performance. |
| -module(mochilogfile2). |
| -author('bob@mochimedia.com'). |
| |
| -export([open/1, write/2, close/1, name/1]). |
| |
| %% @spec open(Name) -> Handle |
| %% @doc Open the log file Name, creating or appending as necessary. All data |
| %% at the end of the file will be truncated until a newline is found, to |
| %% ensure that all records are complete. |
| open(Name) -> |
| {ok, FD} = file:open(Name, [raw, read, write, delayed_write, binary]), |
| fix_log(FD), |
| {?MODULE, Name, FD}. |
| |
| %% @spec name(Handle) -> string() |
| %% @doc Return the path of the log file. |
| name({?MODULE, Name, _FD}) -> |
| Name. |
| |
| %% @spec write(Handle, IoData) -> ok |
| %% @doc Write IoData to the log file referenced by Handle. |
| write({?MODULE, _Name, FD}, IoData) -> |
| ok = file:write(FD, [IoData, $\n]), |
| ok. |
| |
| %% @spec close(Handle) -> ok |
| %% @doc Close the log file referenced by Handle. |
| close({?MODULE, _Name, FD}) -> |
| ok = file:sync(FD), |
| ok = file:close(FD), |
| ok. |
| |
| fix_log(FD) -> |
| {ok, Location} = file:position(FD, eof), |
| Seek = find_last_newline(FD, Location), |
| {ok, Seek} = file:position(FD, Seek), |
| ok = file:truncate(FD), |
| ok. |
| |
| %% Seek backwards to the last valid log entry |
| find_last_newline(_FD, N) when N =< 1 -> |
| 0; |
| find_last_newline(FD, Location) -> |
| case file:pread(FD, Location - 1, 1) of |
| {ok, <<$\n>>} -> |
| Location; |
| {ok, _} -> |
| find_last_newline(FD, Location - 1) |
| end. |
| |
| %% |
| %% Tests |
| %% |
| -ifdef(TEST). |
| -include_lib("eunit/include/eunit.hrl"). |
| name_test() -> |
| D = mochitemp:mkdtemp(), |
| FileName = filename:join(D, "open_close_test.log"), |
| H = open(FileName), |
| ?assertEqual( |
| FileName, |
| name(H)), |
| close(H), |
| file:delete(FileName), |
| file:del_dir(D), |
| ok. |
| |
| open_close_test() -> |
| D = mochitemp:mkdtemp(), |
| FileName = filename:join(D, "open_close_test.log"), |
| OpenClose = fun () -> |
| H = open(FileName), |
| ?assertEqual( |
| true, |
| filelib:is_file(FileName)), |
| ok = close(H), |
| ?assertEqual( |
| {ok, <<>>}, |
| file:read_file(FileName)), |
| ok |
| end, |
| OpenClose(), |
| OpenClose(), |
| file:delete(FileName), |
| file:del_dir(D), |
| ok. |
| |
| write_test() -> |
| D = mochitemp:mkdtemp(), |
| FileName = filename:join(D, "write_test.log"), |
| F = fun () -> |
| H = open(FileName), |
| write(H, "test line"), |
| close(H), |
| ok |
| end, |
| F(), |
| ?assertEqual( |
| {ok, <<"test line\n">>}, |
| file:read_file(FileName)), |
| F(), |
| ?assertEqual( |
| {ok, <<"test line\ntest line\n">>}, |
| file:read_file(FileName)), |
| file:delete(FileName), |
| file:del_dir(D), |
| ok. |
| |
| fix_log_test() -> |
| D = mochitemp:mkdtemp(), |
| FileName = filename:join(D, "write_test.log"), |
| file:write_file(FileName, <<"first line good\nsecond line bad">>), |
| F = fun () -> |
| H = open(FileName), |
| write(H, "test line"), |
| close(H), |
| ok |
| end, |
| F(), |
| ?assertEqual( |
| {ok, <<"first line good\ntest line\n">>}, |
| file:read_file(FileName)), |
| file:write_file(FileName, <<"first line bad">>), |
| F(), |
| ?assertEqual( |
| {ok, <<"test line\n">>}, |
| file:read_file(FileName)), |
| F(), |
| ?assertEqual( |
| {ok, <<"test line\ntest line\n">>}, |
| file:read_file(FileName)), |
| ok. |
| |
| -endif. |