Skip to content

Commit

Permalink
Merge branch 'release/1.3' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredmorrow committed Apr 4, 2013
2 parents 19670b2 + 9e8aaa8 commit 4452a6e
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 127 deletions.
12 changes: 12 additions & 0 deletions RELEASE-NOTES.org
@@ -1,3 +1,15 @@
* Riak CS 1.3.1 Release Notes
** Bugs Fixed
- Fix bug in handling of active object manifests in the case of
overwrite or delete that could lead to old object versions being
resurrected.
- Fix improper capitalization of user metadata header names.
- Fix issue where the S3 rewrite module omits any query parameters
that are not S3 subresources. Also correct handling of query
parameters so that parameter values are not URL decoded twice. This
primarily affects pre-signed URLs because the access key and request
signature are included as query parameters.
- Fix for issue with init script stop.
* Riak CS 1.3.0 Release Notes * Riak CS 1.3.0 Release Notes
** Bugs Fixed ** Bugs Fixed
- Fix handling of cases where buckets have siblings. Previously this - Fix handling of cases where buckets have siblings. Previously this
Expand Down
83 changes: 81 additions & 2 deletions client_tests/python/boto_test.py 100644 → 100755
Expand Up @@ -49,8 +49,8 @@ def md5_from_key(boto_key):
return m.hexdigest() return m.hexdigest()


# `parts_list` should be a list of file-like objects # `parts_list` should be a list of file-like objects
def upload_multipart(bucket, key_name, parts_list): def upload_multipart(bucket, key_name, parts_list, metadata={}):
upload = bucket.initiate_multipart_upload(key_name) upload = bucket.initiate_multipart_upload(key_name, metadata=metadata)
for index, val in enumerate(parts_list): for index, val in enumerate(parts_list):
upload.upload_part_from_file(val, index + 1) upload.upload_part_from_file(val, index + 1)
upload.complete_upload() upload.complete_upload()
Expand Down Expand Up @@ -480,6 +480,85 @@ def test_small_strings_upload_1(self):
self.assertEqual(e.status, 403) self.assertEqual(e.status, 403)
self.assertEqual(e.reason, 'Forbidden') self.assertEqual(e.reason, 'Forbidden')


class ObjectMetadataTest(S3ApiVerificationTestBase):
"Test object metadata, e.g. Content-Encoding, x-amz-meta-*, for PUT/GET"

metadata = {
"Content-Disposition": 'attachment; filename="metaname.txt"',
"Content-Encoding": 'identity',
"Expires": "Tue, 19 Jan 2038 03:14:07 GMT",
"mtime": "1364742057",
"UID": "0",
"with-hypen": "1"}

updated_metadata = {
"Content-Disposition": 'attachment; filename="newname.txt"',
"Expires": "Tue, 19 Jan 2038 03:14:07 GMT",
"mtime": "2222222222",
"uid": "0",
"new-entry": "NEW"}

def test_normal_object_metadata(self):
key_name = str(uuid.uuid4())
bucket = self.conn.create_bucket(self.bucket_name)
key = Key(bucket, key_name)
for k,v in self.metadata.items():
key.set_metadata(k, v)
key.set_contents_from_string("test_normal_object_metadata")
self.assert_metadata(bucket, key_name)
self.change_metadata(bucket, key_name)
self.assert_updated_metadata(bucket, key_name)

def test_mp_object_metadata(self):
key_name = str(uuid.uuid4())
bucket = self.conn.create_bucket(self.bucket_name)
upload = upload_multipart(bucket, key_name, [StringIO("part1")],
metadata=self.metadata)
self.assert_metadata(bucket, key_name)
self.change_metadata(bucket, key_name)
self.assert_updated_metadata(bucket, key_name)

def assert_metadata(self, bucket, key_name):
key = Key(bucket, key_name)
key.get_contents_as_string()

self.assertEqual(key.content_disposition,
'attachment; filename="metaname.txt"')
self.assertEqual(key.content_encoding, "identity")
# TODO: Expires header can be accessed by boto?
# self.assertEqual(key.expires, "Tue, 19 Jan 2038 03:14:07 GMT")
self.assertEqual(key.get_metadata("mtime"), "1364742057")
self.assertEqual(key.get_metadata("uid"), "0")
self.assertEqual(key.get_metadata("with-hypen"), "1")
# x-amz-meta-* headers should be normalized to lowercase
self.assertEqual(key.get_metadata("Mtime"), None)
self.assertEqual(key.get_metadata("MTIME"), None)
self.assertEqual(key.get_metadata("Uid"), None)
self.assertEqual(key.get_metadata("UID"), None)
self.assertEqual(key.get_metadata("With-Hypen"), None)

def change_metadata(self, bucket, key_name):
key = Key(bucket, key_name)
key.copy(bucket.name, key_name, self.updated_metadata)

def assert_updated_metadata(self, bucket, key_name):
key = Key(bucket, key_name)
key.get_contents_as_string()

# unchanged
self.assertEqual(key.get_metadata("uid"), "0")
# updated
self.assertEqual(key.content_disposition,
'attachment; filename="newname.txt"')
self.assertEqual(key.get_metadata("mtime"), "2222222222")
# removed
self.assertEqual(key.content_encoding, None)
self.assertEqual(key.get_metadata("with-hypen"), None)
# inserted
self.assertEqual(key.get_metadata("new-entry"), "NEW")
# TODO: Expires header can be accessed by boto?
# self.assertEqual(key.expires, "Tue, 19 Jan 2038 03:14:07 GMT")



if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
2 changes: 1 addition & 1 deletion rebar.config
Expand Up @@ -26,7 +26,7 @@
]}. ]}.


{deps, [ {deps, [
{node_package, "1.2.1", {git, "git://github.com/basho/node_package", {tag, "1.2.1"}}}, {node_package, "1.2.2", {git, "git://github.com/basho/node_package", {tag, "1.2.2"}}},
{webmachine, ".*", {git, "git://github.com/basho/webmachine", {tag, "1.10.0"}}}, {webmachine, ".*", {git, "git://github.com/basho/webmachine", {tag, "1.10.0"}}},
{riakc, ".*", {git, "git://github.com/basho/riak-erlang-client", {tag, "1.3.1.1"}}}, {riakc, ".*", {git, "git://github.com/basho/riak-erlang-client", {tag, "1.3.1.1"}}},
{lager, ".*", {git, "git://github.com/basho/lager", {tag, "1.2.2"}}}, {lager, ".*", {git, "git://github.com/basho/lager", {tag, "1.2.2"}}},
Expand Down
2 changes: 1 addition & 1 deletion rel/reltool.config
Expand Up @@ -2,7 +2,7 @@
%% ex: ts=4 sw=4 et %% ex: ts=4 sw=4 et
{sys, [ {sys, [
{lib_dirs, ["../deps", "../apps"]}, {lib_dirs, ["../deps", "../apps"]},
{rel, "riak-cs", "1.3.0", {rel, "riak-cs", "1.3.1",
[ [
kernel, kernel,
stdlib, stdlib,
Expand Down
56 changes: 56 additions & 0 deletions riak_test/tests/cs512_regression_test.erl
@@ -0,0 +1,56 @@
%% ---------------------------------------------------------------------
%%
%% Copyright (c) 2007-2013 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.
%%
%% ---------------------------------------------------------------------

-module(cs512_regression_test).

-export([confirm/0]).
-include_lib("eunit/include/eunit.hrl").

-define(BUCKET, "riak-test-bucket").
-define(KEY, "test-key").

confirm() ->
{ok, UserConfig} = setup(),
put_and_get(UserConfig, <<"OLD">>),
put_and_get(UserConfig, <<"NEW">>),
delete(UserConfig),
assert_notfound(UserConfig),
pass.

put_and_get(UserConfig, Data) ->
erlcloud_s3:put_object(?BUCKET, ?KEY, Data, UserConfig),
Props = erlcloud_s3:get_object(?BUCKET, ?KEY, UserConfig),
?assertEqual(proplists:get_value(content, Props), Data).

delete(UserConfig) ->
erlcloud_s3:delete_object(?BUCKET, ?KEY, UserConfig).

assert_notfound(UserConfig) ->
?assertException(_,
{aws_error,{http_error,404,"Object Not Found",<<>>}},
erlcloud_s3:get_object(?BUCKET, ?KEY, UserConfig)).

setup() ->
{UserConfig, _} = rtcs:setup(4),
?assertEqual([{buckets, []}], erlcloud_s3:list_buckets(UserConfig)),
?assertEqual(ok, erlcloud_s3:create_bucket(?BUCKET, UserConfig)),
?assertMatch([{buckets, [[{name, ?BUCKET}, _]]}],
erlcloud_s3:list_buckets(UserConfig)),
{ok, UserConfig}.
2 changes: 1 addition & 1 deletion src/riak_cs.app.src
Expand Up @@ -2,7 +2,7 @@
{application, riak_cs, {application, riak_cs,
[ [
{description, "riak_cs"}, {description, "riak_cs"},
{vsn, "1.3.0"}, {vsn, "1.3.1"},
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{applications, [ {applications, [
Expand Down
102 changes: 80 additions & 22 deletions src/riak_cs_gc.erl
Expand Up @@ -32,7 +32,7 @@
-export([decode_and_merge_siblings/2, -export([decode_and_merge_siblings/2,
gc_interval/0, gc_interval/0,
gc_retry_interval/0, gc_retry_interval/0,
gc_active_manifests/5, gc_active_manifests/3,
gc_specific_manifests/5, gc_specific_manifests/5,
epoch_start/0, epoch_start/0,
leeway_seconds/0, leeway_seconds/0,
Expand All @@ -43,30 +43,88 @@
%%% Public API %%% Public API
%%%=================================================================== %%%===================================================================


gc_active_manifests(Manifests, RiakObject, Bucket, Key, RiakcPid) -> %% @doc Keep requesting manifests until there are no more active manifests or
case riak_cs_manifest_utils:active_manifest(Manifests) of %% there is an error. This requires the following to be occur:
{ok, M} -> %% 1) All previously active multipart manifests have had their unused parts cleaned
case riak_cs_mp_utils:clean_multipart_unused_parts(M, RiakcPid) of %% and become active+multipart_clean
same -> %% 2) All active manifests and active+multipart_clean manifests for multipart are GC'd
ActiveUUIDs = [M?MANIFEST.uuid], %%
GCManiResponse = gc_specific_manifests(ActiveUUIDs, %% Note that any error is irrespective of the current position of the GC states.
RiakObject, %% Some manifests may have been GC'd and then an error occurs. In this case the
Bucket, Key, %% client will only get the error response.
RiakcPid), -spec gc_active_manifests(binary(), binary(), pid()) ->
return_active_uuids_from_gc_response(GCManiResponse, {ok, [binary()]} | {error, term()}.
ActiveUUIDs); gc_active_manifests(Bucket, Key, RiakcPid) ->
updated -> gc_active_manifests(Bucket, Key, RiakcPid, []).
updated
%% @private
-spec gc_active_manifests(binary(), binary(), pid(), [binary]) ->
{ok, [binary()]} | {error, term()}.
gc_active_manifests(Bucket, Key, RiakcPid, UUIDs) ->
case get_active_manifests(Bucket, Key, RiakcPid) of
{ok, _RiakObject, []} -> {ok, UUIDs};
{ok, RiakObject, Manifests} ->
UnchangedManifests = clean_manifests(Manifests, RiakcPid),
case gc_manifests(UnchangedManifests, RiakObject, Bucket, Key, RiakcPid) of
{error, _}=Error -> Error;
NewUUIDs -> gc_active_manifests(Bucket, Key, RiakcPid, UUIDs ++ NewUUIDs)
end; end;
_ -> {error, notfound} ->{ok, UUIDs};
{ok, []} {error, _}=Error -> Error
end. end.


%% @private -spec get_active_manifests(binary(), binary(), pid()) ->
return_active_uuids_from_gc_response({ok, _RiakObject}, ActiveUUIDs) -> {ok, riakc_obj:riakc_obj(), [lfs_manifest()]} | {error, term()}.
{ok, ActiveUUIDs}; get_active_manifests(Bucket, Key, RiakcPid) ->
return_active_uuids_from_gc_response({error, _Error}=Error, _ActiveUUIDs) -> active_manifests(riak_cs_utils:get_manifests(RiakcPid, Bucket, Key)).
Error.
-spec active_manifests({ok, riakc_obj:riakc_obj(), [lfs_manifest()]}) ->
{ok, riakc_obj:riakc_obj(), [lfs_manifest()]};
({error, term()}) ->
{error, term()}.
active_manifests({ok, RiakObject, Manifests}) ->
{ok, RiakObject, riak_cs_manifest_utils:active_manifests(Manifests)};
active_manifests({error, _}=Error) -> Error.

-spec clean_manifests([lfs_manifest()], pid()) -> [lfs_manifest()].
clean_manifests(ActiveManifests, RiakcPid) ->
[M || M <- ActiveManifests, clean_multipart_manifest(M, RiakcPid)].

-spec clean_multipart_manifest(lfs_manifest(), pid()) -> true | false.
clean_multipart_manifest(M, RiakcPid) ->
is_multipart_clean(riak_cs_mp_utils:clean_multipart_unused_parts(M, RiakcPid)).

is_multipart_clean(same) ->
true;
is_multipart_clean(updated) ->
false.

-spec gc_manifests(Manifests :: [lfs_manifest()],
RiakObject :: riakc_obj:riakc_obj(),
Bucket :: binary(),
Key :: binary(),
RiakcPid :: pid()) ->
[binary()] | {error, term()}.
gc_manifests(Manifests, RiakObject, Bucket, Key, RiakcPid) ->
catch lists:foldl(fun(M, UUIDs) ->
gc_manifest(M, RiakObject, Bucket, Key, RiakcPid, UUIDs)
end, [], Manifests).

-spec gc_manifest(M :: lfs_manifest(),
RiakObject :: riakc_obj:riakc_obj(),
Bucket :: binary(),
Key :: binary(),
RiakcPid :: pid(),
UUIDs :: [binary()]) ->
[binary()] | no_return().
gc_manifest(M, RiakObject, Bucket, Key, RiakcPid, UUIDs) ->
UUID = M?MANIFEST.uuid,
check(gc_specific_manifests([UUID], RiakObject, Bucket, Key, RiakcPid), [UUID | UUIDs]).

check({ok, _}, Val) ->
Val;
check({error, _}=Error, _Val) ->
throw(Error).


%% @private %% @private
-spec gc_specific_manifests(UUIDsToMark :: [binary()], -spec gc_specific_manifests(UUIDsToMark :: [binary()],
Expand Down
9 changes: 9 additions & 0 deletions src/riak_cs_manifest_utils.erl
Expand Up @@ -31,6 +31,7 @@
%% export Public API %% export Public API
-export([new_dict/2, -export([new_dict/2,
active_manifest/1, active_manifest/1,
active_manifests/1,
active_and_writing_manifests/1, active_and_writing_manifests/1,
overwritten_UUIDs/1, overwritten_UUIDs/1,
mark_pending_delete/2, mark_pending_delete/2,
Expand Down Expand Up @@ -64,6 +65,11 @@ active_manifest(Dict) ->
{ok, Manifest} {ok, Manifest}
end. end.


%% @doc Return all active manifests
-spec active_manifests(orddict:orddict()) -> [lfs_manifest()] | [].
active_manifests(Dict) ->
lists:filter(fun manifest_is_active/1, orddict_values(Dict)).

%% @doc Return a list of all manifests in the %% @doc Return a list of all manifests in the
%% `active' or `writing' state %% `active' or `writing' state
-spec active_and_writing_manifests(orddict:orddict()) -> [lfs_manifest()]. -spec active_and_writing_manifests(orddict:orddict()) -> [lfs_manifest()].
Expand Down Expand Up @@ -298,6 +304,9 @@ leeway_elapsed(Timestamp) ->
orddict_values(OrdDict) -> orddict_values(OrdDict) ->
[V || {_K, V} <- orddict:to_list(OrdDict)]. [V || {_K, V} <- orddict:to_list(OrdDict)].


manifest_is_active(?MANIFEST{state=active}) -> true;
manifest_is_active(_Manifest) -> false.

%% NOTE: This is a foldl function, initial acc = no_active_manifest %% NOTE: This is a foldl function, initial acc = no_active_manifest
most_recent_active_manifest(Manifest=?MANIFEST{state=active}, no_active_manifest) -> most_recent_active_manifest(Manifest=?MANIFEST{state=active}, no_active_manifest) ->
Manifest; Manifest;
Expand Down
2 changes: 1 addition & 1 deletion src/riak_cs_mp_utils.erl
Expand Up @@ -232,7 +232,7 @@ new_manifest(Bucket, Key, ContentType, {_, _, _} = Owner, Opts) ->
UUID = druuid:v4(), UUID = druuid:v4(),
%% TODO: add object metadata here, e.g. content-disposition et al. %% TODO: add object metadata here, e.g. content-disposition et al.
%% TODO: add cluster_id ... which means calling new_manifest/11 not /9. %% TODO: add cluster_id ... which means calling new_manifest/11 not /9.
MetaData = case proplists:get_value(as_is_headers, Opts) of MetaData = case proplists:get_value(meta_data, Opts) of
undefined -> []; undefined -> [];
AsIsHdrs -> AsIsHdrs AsIsHdrs -> AsIsHdrs
end, end,
Expand Down
9 changes: 9 additions & 0 deletions src/riak_cs_s3_response.erl
Expand Up @@ -27,6 +27,7 @@
error_response/5, error_response/5,
list_bucket_response/5, list_bucket_response/5,
list_all_my_buckets_response/3, list_all_my_buckets_response/3,
copy_object_response/3,
no_such_upload_response/3, no_such_upload_response/3,
error_code_to_atom/1]). error_code_to_atom/1]).


Expand Down Expand Up @@ -236,6 +237,14 @@ user_to_xml_owner(?RCS_USER{canonical_id=CanonicalId, display_name=Name}) ->
{'Owner', [{'ID', [CanonicalId]}, {'Owner', [{'ID', [CanonicalId]},
{'DisplayName', [Name]}]}. {'DisplayName', [Name]}]}.


copy_object_response(Manifest, RD, Ctx) ->
LastModified = riak_cs_wm_utils:to_iso_8601(Manifest?MANIFEST.created),
ETag = riak_cs_utils:etag_from_binary(Manifest?MANIFEST.content_md5),
XmlDoc = [{'CopyObjectResponse',
[{'LastModified', [LastModified]},
{'ETag', [ETag]}]}],
respond(200, export_xml(XmlDoc), RD, Ctx).

export_xml(XmlDoc) -> export_xml(XmlDoc) ->
unicode:characters_to_binary( unicode:characters_to_binary(
xmerl:export_simple(XmlDoc, xmerl_xml, [{prolog, ?XML_PROLOG}]), unicode, unicode). xmerl:export_simple(XmlDoc, xmerl_xml, [{prolog, ?XML_PROLOG}]), unicode, unicode).
Expand Down

0 comments on commit 4452a6e

Please sign in to comment.