Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

719 lines (673 sloc) 27.833 kb
%% -------------------------------------------------------------------
%%
%% Copyright (c) 2007-2010 Basho Technologies, Inc. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%% @doc Resource for serving large Riak objects over HTTP.
%%
%% Available operations:
%%
%% GET /Prefix/
%% Get information about the luwak interface, in JSON form:
%% {"props":{Prop1:Val1,Prop2:Val2,...},
%% "keys":[Key1,Key2,...]}.
%% Each property will be included in the "props" object.
%% Including the query param "props=false" will cause the "props"
%% field to be omitted from the response.
%% Including the query param "keys=false" will cause the "keys"
%% field to be omitted from the response.
%%
%% POST /Prefix/
%% Equivalent to "PUT /Prefix/Key" where Key is chosen
%% by the server.
%%
%% GET /Prefix/Key
%% Get the data stored under the named Key.
%% Content-type of the response will be whatever incoming
%% Content-type was used in the request that stored the data.
%% Additional headers will include:
%% Etag: The Riak "vtag" metadata of the object
%% Last-Modified: The last-modified time of the object
%% Encoding: The value of the incoming Encoding header from
%% the request that stored the data.
%% X-Riak-Meta-: Any headers prefixed by X-Riak-Meta- supplied
%% on PUT are returned verbatim
%%
%% PUT /Prefix/Key
%% Store new data under the named Key.
%% A Content-type header *must* be included in the request. The
%% value of this header will be used in the response to subsequent
%% GET requests.
%% The body of the request will be stored literally as the value
%% of the riak_object, and will be served literally as the body of
%% the response to subsequent GET requests.
%% Include an Encoding header if you would like an Encoding header
%% to be included in the response to subsequent GET requests.
%% Include custom metadata using headers prefixed with X-Riak-Meta-.
%% They will be returned verbatim on subsequent GET requests.
%%
%% POST /Prefix/Key
%% Equivalent to "PUT /Prefix/Key" (useful for clients that
%% do not support the PUT method).
%%
%% DELETE /Prefix/Key
%% Delete the data stored under the named Key.
%%
%% Webmachine dispatch lines for this resource should look like:
%%
%% {["luwak"],
%% luwak_wm_file,
%% [{prefix, "luwak"}
%% ]}.
%% {["luwak", key],
%% luwak_wm_file,
%% [{prefix, "luwak"}
%% ]}.
%%
%% These example dispatch lines will expose this resource at
%% /luwak/ and /luwak/Key. The resource will attempt to
%% connect to Riak on the same Erlang node one which the resource
%% is executing.
-module(luwak_wm_file).
-author('Bryan Fink <bryan@basho.com>').
%% webmachine resource exports
-export([
init/1,
service_available/2,
allowed_methods/2,
allow_missing_post/2,
malformed_request/2,
resource_exists/2,
last_modified/2,
generate_etag/2,
content_types_provided/2,
charsets_provided/2,
encodings_provided/2,
content_types_accepted/2,
produce_toplevel_body/2,
post_is_create/2,
create_path/2,
process_post/2,
produce_doc_body/2,
accept_doc_body/2,
delete_resource/2
]).
%% @type context() = term()
-record(ctx, {key, %% binary() - Key (from uri)
client, %% riak_client() - the store client
prefix, %% string() - prefix for resource uris
handle, %% {ok, riak_object()}|{error, term()}
%% - the object found
method, %% atom() - HTTP method for the request
file_props %% list() - file properties to pass to create
}).
-include_lib("webmachine/include/webmachine.hrl").
-include_lib("riak_kv/src/riak_kv_wm_raw.hrl").
-include("luwak.hrl").
-define(HEAD_RANGE, "Range").
-define(HEAD_CRANGE, "Content-Range").
-define(HEAD_BLOCK_SZ, "X-Luwak-Block-Size").
%% @spec init(proplist()) -> {ok, context()}
%% @doc Initialize this resource. This function extracts the
%% 'prefix' property from the dispatch args.
init(Props) ->
{ok, #ctx{prefix=proplists:get_value(prefix, Props),
file_props=[]}}.
%% @spec service_available(reqdata(), context()) ->
%% {boolean(), reqdata(), context()}
%% @doc Determine whether or not a connection to Riak
%% can be established. This function also takes this
%% opportunity to extract the 'bucket' and 'key' path
%% bindings from the dispatch, as well as any vtag
%% query parameter.
service_available(RD, Ctx) ->
case riak:local_client(get_client_id(RD)) of
{ok, C} ->
{true,
RD,
Ctx#ctx{
method=wrq:method(RD),
client=C,
key=case wrq:path_info(key, RD) of
undefined -> undefined;
K -> list_to_binary(K)
end
}};
Error ->
{false,
wrq:set_resp_body(
io_lib:format("Unable to connect to Riak: ~p~n", [Error]),
wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)),
Ctx}
end.
%% @spec get_client_id(reqdata()) -> term()
%% @doc Extract the request's preferred client id from the
%% X-Riak-ClientId header. Return value will be:
%% 'undefined' if no header was found
%% 32-bit binary() if the header could be base64-decoded
%% into a 32-bit binary
%% string() if the header could not be base64-decoded
%% into a 32-bit binary
get_client_id(RD) ->
case wrq:get_req_header(?HEAD_CLIENT, RD) of
undefined -> undefined;
RawId ->
case catch base64:decode(RawId) of
ClientId= <<_:32>> -> ClientId;
_ -> RawId
end
end.
%% @spec allowed_methods(reqdata(), context()) ->
%% {[method()], reqdata(), context()}
%% @doc Get the list of methods this resource supports.
%% HEAD, GET, POST, and PUT are supported at both
%% the bucket and key levels. DELETE is supported
%% at the key level only.
allowed_methods(RD, Ctx=#ctx{key=undefined}) ->
%% bucket-level: no delete
{['HEAD', 'GET', 'POST'], RD, Ctx};
allowed_methods(RD, Ctx) ->
%% key-level: just about anything
{['HEAD', 'GET', 'POST', 'PUT', 'DELETE'], RD, Ctx}.
%% @spec allow_missing_post(reqdata(), context()) ->
%% {true, reqdata(), context()}
%% @doc Makes POST and PUT equivalent for creating new
%% bucket entries.
allow_missing_post(RD, Ctx) ->
{true, RD, Ctx}.
%% @spec malformed_request(reqdata(), context()) ->
%% {boolean(), reqdata(), context()}
%% @doc Determine whether query parameters, request headers,
%% and request body are badly-formed.
%% Body format
%% is not tested for a key-level request (since the
%% body may be any content the client desires).
malformed_request(RD, Ctx) when Ctx#ctx.method =:= 'POST'
orelse Ctx#ctx.method =:= 'PUT' ->
case wrq:get_req_header("Content-Type", RD) of
undefined ->
{true, missing_content_type(RD), Ctx};
_ ->
malformed_block_size(RD, Ctx)
end;
malformed_request(RD, Ctx) when Ctx#ctx.key =:= undefined ->
{false, RD, Ctx};
malformed_request(RD, Ctx) ->
HCtx = ensure_handle(Ctx),
case HCtx#ctx.handle of
{error, notfound} ->
{{halt, 404},
wrq:set_resp_header("Content-Type", "text/plain",
wrq:append_to_response_body(
io_lib:format("not found~n",[]),
RD)),
HCtx};
{error, timeout} ->
{{halt, 500},
wrq:set_resp_header("Content-Type", "text/plain",
wrq:append_to_response_body(
io_lib:format("request timed out~n",[]),
RD)),
HCtx};
{error, Err} ->
{{halt, 500},
wrq:set_resp_header("Content-Type", "text/plain",
wrq:append_to_response_body(
io_lib:format("Error:~n~p~n",[Err]),
RD)),
HCtx};
_ ->
{false, RD, HCtx}
end.
malformed_block_size(RD, Ctx=#ctx{file_props=FP}) ->
HCtx = ensure_handle(Ctx),
case HCtx#ctx.handle of
{ok, _H} -> {false, RD, HCtx};
_ ->
case check_header(RD, ?HEAD_BLOCK_SZ,
fun list_to_integer/1, fun is_pos/1) of
undefined -> {false, RD, HCtx};
{error, Name} ->
{true,
error_resp(RD, "invalid value for ~s~n", [Name]),
HCtx};
{ok, Val} ->
{false, RD, HCtx#ctx{file_props=[{block_size,Val}|FP]}}
end
end.
%% @spec content_types_provided(reqdata(), context()) ->
%% {[{ContentType::string(), Producer::atom()}], reqdata(), context()}
%% @doc List the content types available for representing this resource.
%% "application/json" is the content-type for bucket-level GET requests
%% The content-type for a key-level request is the content-type that
%% was used in the PUT request that stored the document in Riak.
content_types_provided(RD, Ctx=#ctx{key=undefined}) ->
%% top level: JSON description only
{[{"application/json", produce_toplevel_body}], RD, Ctx};
content_types_provided(RD, Ctx=#ctx{method=Method}=Ctx) when Method =:= 'PUT';
Method =:= 'POST' ->
{ContentType, _} = extract_content_type(RD),
{[{ContentType, produce_doc_body}], RD, Ctx};
content_types_provided(RD, Ctx0) ->
case defined_attribute(Ctx0, ?MD_CTYPE) of
{undefined, Ctx} ->
{[{"application/octet-stream", produce_doc_body}], RD, Ctx};
{Ctype, Ctx} ->
{[{Ctype, produce_doc_body}], RD, Ctx}
end.
%% @spec charsets_provided(reqdata(), context()) ->
%% {no_charset|[{Charset::string(), Producer::function()}],
%% reqdata(), context()}
%% @doc List the charsets available for representing this resource.
%% No charset will be specified for a bucket-level request.
%% The charset for a key-level request is the charset that was used
%% in the PUT request that stored the document in Riak (none if
%% no charset was specified at PUT-time).
charsets_provided(RD, Ctx=#ctx{key=undefined}) ->
%% default charset for bucket-level request
{no_charset, RD, Ctx};
charsets_provided(RD, #ctx{method=Method}=Ctx) when Method =:= 'PUT';
Method =:= 'POST' ->
case extract_content_type(RD) of
{_, undefined} ->
{no_charset, RD, Ctx};
{_, Charset} ->
{[{Charset, fun(X) -> X end}], RD, Ctx}
end;
charsets_provided(RD, Ctx0) ->
case defined_attribute(Ctx0, ?MD_CHARSET) of
{undefined, Ctx} ->
{no_charset, RD, Ctx};
{Cset, Ctx} ->
{[{Cset, fun(X) -> X end}], RD, Ctx}
end.
%% @spec encodings_provided(reqdata(), context()) ->
%% {[{Encoding::string(), Producer::function()}], reqdata(), context()}
%% @doc List the encodings available for representing this resource.
%% "identity" and "gzip" are available for bucket-level requests.
%% The encoding for a key-level request is the encoding that was
%% used in the PUT request that stored the document in Riak, or
%% "identity" and "gzip" if no encoding was specified at PUT-time.
encodings_provided(RD, Ctx=#ctx{key=undefined}) ->
%% identity and gzip for bucket-level request
{default_encodings(), RD, Ctx};
encodings_provided(RD, Ctx0) ->
case defined_attribute(Ctx0, ?MD_ENCODING) of
{undefined, Ctx} ->
{default_encodings(), RD, Ctx};
{Enc, Ctx} ->
{[{Enc, fun(X) -> X end}], RD, Ctx}
end.
defined_attribute(Ctx0, Attr) ->
Ctx = ensure_handle(Ctx0),
case Ctx#ctx.handle of
{ok, H} ->
As = luwak_file:get_attributes(H),
case dict:find(Attr, As) of
{ok, A} ->
{A, Ctx};
error ->
{undefined, Ctx}
end;
{error, _} ->
{undefined, Ctx}
end.
file_property(RD, Ctx0, Prop, Default) ->
Ctx = ensure_handle(Ctx0),
case Ctx#ctx.handle of
{ok, H} ->
case luwak_file:get_property(H, Prop) of
undefined ->
{Default, RD, Ctx};
P ->
{P, RD, Ctx}
end;
{error, _} ->
{Default, RD, Ctx}
end.
%% @spec default_encodings() -> [{Encoding::string(), Producer::function()}]
%% @doc The default encodings available: identity and gzip.
default_encodings() ->
[{"identity", fun(X) -> X end},
{"gzip", fun(X) -> zlib:gzip(X) end}].
%% @spec content_types_accepted(reqdata(), context()) ->
%% {[{ContentType::string(), Acceptor::atom()}],
%% reqdata(), context()}
%% @doc Get the list of content types this resource will accept.
%% "application/json" is the only type accepted for bucket-PUT.
%% Whatever content type is specified by the Content-Type header
%% of a key-level PUT request will be accepted by this resource.
%% (A key-level put *must* include a Content-Type header.)
content_types_accepted(RD, Ctx) ->
case wrq:get_req_header(?HEAD_CTYPE, RD) of
undefined ->
%% user must specify content type of the data
{[],
wrq:set_resp_header(
?HEAD_CTYPE,
"text/plain",
wrq:set_resp_body(
["Please include a valid Content-type header.\n"],
RD)),
Ctx};
CType ->
Media = hd(string:tokens(CType, ";")),
case string:tokens(Media, "/") of
["multipart", "byteranges"] ->
{[{Media, accept_byteranges}], RD, Ctx};
[_Type, _Subtype] ->
%% accept whatever the user says
{[{Media, accept_doc_body}], RD, Ctx};
_ ->
{[],
wrq:set_resp_header(
?HEAD_CTYPE,
"text/plain",
wrq:set_resp_body(
["\"", Media, "\""
" is not a valid media type"
" for the Content-type header.\n"],
RD)),
Ctx}
end
end.
%% @spec resource_exists(reqdata(), context()) -> {boolean(), reqdata(), context()}
%% @doc Determine whether or not the requested item exists.
%% Documents exists if a read request to Riak returns {ok, riak_object()}.
resource_exists(RD, Ctx=#ctx{key=undefined}) ->
%% the toplevel always exists
{true, RD, Ctx};
resource_exists(RD, Ctx0) ->
Ctx = ensure_handle(Ctx0),
case Ctx#ctx.handle of
{ok, _} ->
{true, RD, Ctx};
{error, notfound} ->
{false, RD, Ctx};
{error, timeout} ->
{{halt, 503}, RD, Ctx}
end.
%% @spec produce_toplevel_body(reqdata(), context()) -> {binary(), reqdata(), context()}
%% @doc Produce the JSON response to a bucket-level GET.
%% Includes the bucket props unless the "props=false" query param
%% is specified.
%% Includes the keys of the documents in the bucket unless the
%% "keys=false" query param is specified. If "keys=stream" query param
%% is specified, keys will be streamed back to the client in JSON chunks
%% like so: {"keys":[Key1, Key2,...]}.
produce_toplevel_body(RD, Ctx=#ctx{client=C}) ->
SchemaPart =
case wrq:get_qs_value(?Q_PROPS, RD) of
?Q_FALSE -> [];
_ ->
Props = [{<<"o_bucket">>, ?O_BUCKET},
{<<"n_bucket">>, ?N_BUCKET},
{<<"block_default">>,
luwak_file:get_default_block_size()}],
[{?JSON_PROPS, {struct, Props}}]
end,
KeyPart =
case wrq:get_qs_value(?Q_KEYS, RD) of
?Q_STREAM -> stream;
?Q_TRUE -> {ok, KeyList} = C:list_keys(?O_BUCKET),
[{?Q_KEYS, KeyList}];
_ -> []
end,
case KeyPart of
stream -> {{stream, {mochijson2:encode({struct, SchemaPart}),
fun() ->
{ok, ReqId} = C:stream_list_keys(?O_BUCKET),
stream_keys(ReqId)
end}},
RD,
Ctx};
_ ->
{mochijson2:encode({struct, SchemaPart++KeyPart}), RD, Ctx}
end.
stream_keys(ReqId) ->
receive
{ReqId, {keys, Keys}} ->
{mochijson2:encode({struct, [{<<"keys">>, Keys}]}), fun() -> stream_keys(ReqId) end};
{ReqId, done} -> {mochijson2:encode({struct, [{<<"keys">>, []}]}), done}
end.
%% @spec post_is_create(reqdata(), context()) -> {boolean(), reqdata(), context()}
%% @doc POST is considered a document-creation operation for bucket-level
%% requests (this makes webmachine call create_path/2, where the key
%% for the created document will be chosen).
post_is_create(RD, Ctx=#ctx{key=undefined}) ->
%% bucket-POST is create
{true, RD, Ctx};
post_is_create(RD, Ctx) ->
%% key-POST is not create
{false, RD, Ctx}.
%% @spec create_path(reqdata(), context()) -> {string(), reqdata(), context()}
%% @doc Choose the Key for the document created during a bucket-level POST.
%% This function also sets the Location header to generate a
%% 201 Created response.
create_path(RD, Ctx=#ctx{prefix=P}) ->
K = riak_core_util:unique_id_62(),
{K,
wrq:set_resp_header("Location",
lists:append(["/",P,"/",K]),
RD),
Ctx#ctx{key=list_to_binary(K)}}.
%% @spec process_post(reqdata(), context()) -> {true, reqdata(), context()}
%% @doc Pass-through for key-level requests to allow POST to function
%% as PUT for clients that do not support PUT.
process_post(RD, Ctx) -> accept_doc_body(RD, Ctx).
%% @spec accept_doc_body(reqdata(), context()) -> {true, reqdat(), context()}
%% @doc Store the data the client is PUTing in the document.
%% This function translates the headers and body of the HTTP request
%% into their final riak_object() form, and executes the Riak put.
accept_doc_body(RD, Ctx=#ctx{key=K, client=C, file_props=FP}) ->
{CType, Charset} = extract_content_type(RD),
UserMeta = extract_user_meta(RD),
CTypeMD = dict:store(?MD_CTYPE, CType, dict:new()),
CharsetMD = if Charset /= undefined ->
dict:store(?MD_CHARSET, Charset, CTypeMD);
true -> CTypeMD
end,
EncMD = case wrq:get_req_header(?HEAD_ENCODING, RD) of
undefined -> CharsetMD;
E -> dict:store(?MD_ENCODING, E, CharsetMD)
end,
UserMetaMD = dict:store(?MD_USERMETA, UserMeta, EncMD),
H0 = case Ctx#ctx.handle of
{ok, H} -> H;
_ ->
{ok, H} = luwak_file:create(C, K, FP, dict:new()),
H
end,
{ok,H1} = luwak_file:set_attributes(C, H0, UserMetaMD),
HCtx = Ctx#ctx{handle={ok,H1}},
{accept_streambody(RD, HCtx), RD, HCtx}.
accept_streambody(RD, #ctx{handle={ok, H}, client=C, method=Method}) ->
Offset = case Method of
'POST' -> luwak_file:length(C,H);
_ ->0
end,
Stream = luwak_put_stream:start_link(C, H, Offset, 1000),
Size = luwak_file:get_default_block_size(),
{H2, Count} = accept_streambody1(Stream, 0, wrq:stream_req_body(RD, Size)),
H2Len = luwak_file:length(C, H2),
%% truncate will fail if passed a Start >= the length of the file
if Offset+Count < H2Len ->
{ok, _} = luwak_io:truncate(C, H2, Offset+Count),
true;
true -> true
end.
accept_streambody1(Stream, Count0, {Data, Next}) ->
Count = Count0+size(Data),
luwak_put_stream:send(Stream, Data),
if is_function(Next) ->
accept_streambody1(Stream, Count, Next());
Next =:= done ->
luwak_put_stream:close(Stream),
{ok, File} = luwak_put_stream:status(Stream, ?TIMEOUT_DEFAULT),
{File, Count}
end.
%% @spec extract_content_type(reqdata()) ->
%% {ContentType::string(), Charset::string()|undefined}
%% @doc Interpret the Content-Type header in the client's PUT request.
%% This function extracts the content type and charset for use
%% in subsequent GET requests.
extract_content_type(RD) ->
case wrq:get_req_header(?HEAD_CTYPE, RD) of
undefined ->
undefined;
RawCType ->
[CType|RawParams] = string:tokens(RawCType, "; "),
Params = [ list_to_tuple(string:tokens(P, "=")) || P <- RawParams],
{CType, proplists:get_value("charset", Params)}
end.
%% @spec extract_user_meta(reqdata()) -> proplist()
%% @doc Extract headers prefixed by X-Riak-Meta- in the client's PUT request
%% to be returned by subsequent GET requests.
extract_user_meta(RD) ->
lists:filter(fun({K,_V}) ->
lists:prefix(
?HEAD_USERMETA_PREFIX,
string:to_lower(any_to_list(K)))
end,
mochiweb_headers:to_list(wrq:req_headers(RD))).
%% @spec produce_doc_body(reqdata(), context()) -> {binary(), reqdata(), context()}
%% @doc Extract the value of the document, and place it in the response
%% body of the request.
produce_doc_body(RD, Ctx=#ctx{handle={ok, H}, client=C}) ->
{{stream, luwak_file:length(C, H), file_sender(C, H)},
add_user_metadata(RD, H),
Ctx}.
add_user_metadata(RD, Handle) ->
Attr = luwak_file:get_attributes(Handle),
case dict:find(?MD_USERMETA, Attr) of
{ok, UserMeta} ->
lists:foldl(fun({K,V},Acc) ->
wrq:merge_resp_headers([{K,V}],Acc)
end,
RD, UserMeta);
error -> RD
end.
file_sender(C, H) ->
fun(Start, End) ->
%% HTTP specifies the last byte to send,
%% but luwak wants a number of bytes after offset
Stream = luwak_get_stream:start(C, H, Start, 1+End-Start),
(send_file_helper(Stream))()
end.
-define(STREAM_TIMEOUT, 5000).
send_file_helper(Stream) ->
fun() ->
case luwak_get_stream:recv(Stream, ?STREAM_TIMEOUT) of
{Data, _Offset} when is_binary(Data) ->
{Data, send_file_helper(Stream)};
eos ->
{<<>>, done};
closed ->
{<<>>, done};
{error, timeout} ->
{<<>>, done}
end
end.
%% @spec ensure_handle(context()) -> context()
%% @doc Ensure that the 'handle' field of the context() has been filled
%% with the result of a luwak_file:get request. This is a
%% convenience for memoizing the result of a get so it can be
%% used in multiple places in this resource, without having to
%% worry about the order of executing of those places.
ensure_handle(Ctx=#ctx{handle=undefined, key=K, client=C}) ->
Ctx#ctx{handle=luwak_file:get(C, K)};
ensure_handle(Ctx) -> Ctx.
%% @spec delete_resource(reqdata(), context()) -> {true, reqdata(), context()}
%% @doc Delete the document specified.
delete_resource(RD, Ctx=#ctx{key=K, client=C}) ->
case luwak_file:delete(C, K) of
{error, precommit_fail} ->
{{halt, 403}, send_precommit_error(RD, undefined), Ctx};
{error, {precommit_fail, Reason}} ->
{{halt, 403}, send_precommit_error(RD, Reason), Ctx};
ok ->
{true, RD, Ctx}
end.
%% @spec generate_etag(reqdata(), context()) ->
%% {undefined|string(), reqdata(), context()}
%% @doc Get the etag for this resource.
%% Bucket requests will have no etag.
%% Documents will have an etag equal to their vtag. No etag will be
%% given for documents with siblings, if no sibling was chosen with the
%% vtag query param.
generate_etag(RD, Ctx=#ctx{key=undefined}) ->
{undefined, RD, Ctx};
generate_etag(RD, Ctx) ->
file_property(RD, Ctx, checksum, undefined).
%% @spec last_modified(reqdata(), context()) ->
%% {undefined|datetime(), reqdata(), context()}
%% @doc Get the last-modified time for this resource.
%% Bucket requests will have no last-modified time.
%% Documents will have the last-modified time specified by the riak_object.
%% No last-modified time will be given for documents with siblings, if no
%% sibling was chosen with the vtag query param.
last_modified(RD, Ctx=#ctx{key=undefined}) ->
{undefined, RD, Ctx};
last_modified(RD, Ctx) ->
case file_property(RD, Ctx, modified, undefined) of
{Time={_,_,_},NewRD,NewCtx} ->
{calendar:now_to_universal_time(Time), NewRD, NewCtx};
Other ->
Other
end.
any_to_list(V) when is_list(V) ->
V;
any_to_list(V) when is_atom(V) ->
atom_to_list(V);
any_to_list(V) when is_binary(V) ->
binary_to_list(V).
missing_content_type(RD) ->
RD1 = wrq:set_resp_header("Content-Type", "text/plain", RD),
wrq:append_to_response_body(<<"Missing Content-Type request header">>, RD1).
send_precommit_error(RD, Reason) ->
RD1 = wrq:set_resp_header("Content-Type", "text/plain", RD),
Error = if
Reason =:= undefined ->
list_to_binary([atom_to_binary(wrq:method(RD1), utf8),
<<" aborted by pre-commit hook.">>]);
true ->
Reason
end,
wrq:append_to_response_body(Error, RD1).
error_resp(RD, Fmt, Data) ->
RD1 = wrq:set_resp_header("Content-Type", "text/plain", RD),
wrq:append_to_response_body(io_lib:format(Fmt, Data), RD1).
check_header(RD, Name, Cast, Validate) ->
check_header(RD, Name, Cast, Validate, undefined).
check_header(RD, Name, Cast, Validate, Default) ->
case wrq:get_req_header(Name, RD) of
undefined -> Default;
Val0 ->
Val = try Cast(Val0)
catch _:_ -> {error, Name}
end,
case Val of
{error, Name} -> {error, Name};
_ ->
case Validate(Val) of
true -> {ok, Val};
false -> {error, Name}
end
end
end.
is_pos(N) ->
N > 0.
Jump to Line
Something went wrong with that request. Please try again.