You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by ga...@apache.org on 2017/05/08 21:44:03 UTC

[05/24] jclouds git commit: JCLOUDS-1005: Backblaze B2 object operations

JCLOUDS-1005: Backblaze B2 object operations


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/bd6a495a
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/bd6a495a
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/bd6a495a

Branch: refs/heads/master
Commit: bd6a495a04959e3dca157c1cb1c87943f86021bc
Parents: 4393c47
Author: Andrew Gaul <ga...@apache.org>
Authored: Fri May 20 16:24:19 2016 -0700
Committer: Andrew Gaul <ga...@apache.org>
Committed: Thu Jun 9 16:15:11 2016 -0700

----------------------------------------------------------------------
 .../b2/src/main/java/org/jclouds/b2/B2Api.java  |   4 +
 .../jclouds/b2/binders/UploadFileBinder.java    |  51 ++
 .../main/java/org/jclouds/b2/domain/Action.java |  33 ++
 .../java/org/jclouds/b2/domain/B2Object.java    |  55 ++
 .../org/jclouds/b2/domain/B2ObjectList.java     |  52 ++
 .../jclouds/b2/domain/DeleteFileResponse.java   |  32 ++
 .../org/jclouds/b2/domain/HideFileResponse.java |  37 ++
 .../jclouds/b2/domain/UploadFileResponse.java   |  41 ++
 .../jclouds/b2/domain/UploadUrlResponse.java    |  35 ++
 .../java/org/jclouds/b2/features/ObjectApi.java | 150 +++++
 .../filters/RequestAuthorizationDownload.java   |  59 ++
 .../b2/functions/ParseB2ObjectFromResponse.java |  58 ++
 .../handlers/ParseB2ErrorFromJsonContent.java   |   8 +
 .../org/jclouds/b2/reference/B2Headers.java     |  36 ++
 .../jclouds/b2/features/ObjectApiLiveTest.java  | 284 ++++++++++
 .../jclouds/b2/features/ObjectApiMockTest.java  | 545 +++++++++++++++++++
 ...e_file_version_already_deleted_response.json |   5 +
 .../test/resources/delete_object_request.json   |   4 +
 .../test/resources/delete_object_response.json  |   4 +
 .../get_file_info_deleted_file_response.json    |   5 +
 .../test/resources/get_file_info_request.json   |   3 +
 .../test/resources/get_file_info_response.json  |  12 +
 .../get_upload_url_deleted_bucket_response.json |   5 +
 .../test/resources/get_upload_url_request.json  |   3 +
 .../test/resources/get_upload_url_response.json |   5 +
 .../src/test/resources/hide_file_request.json   |   4 +
 .../src/test/resources/hide_file_response.json  |   6 +
 .../test/resources/list_file_names_request.json |   3 +
 .../resources/list_file_names_response.json     |  19 +
 .../resources/list_file_versions_request.json   |   3 +
 .../resources/list_file_versions_response.json  |  27 +
 .../test/resources/upload_file_response.json    |  13 +
 32 files changed, 1601 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/B2Api.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/B2Api.java b/providers/b2/src/main/java/org/jclouds/b2/B2Api.java
index 19419b5..bbb6404 100644
--- a/providers/b2/src/main/java/org/jclouds/b2/B2Api.java
+++ b/providers/b2/src/main/java/org/jclouds/b2/B2Api.java
@@ -20,6 +20,7 @@ import java.io.Closeable;
 
 import org.jclouds.b2.features.AuthorizationApi;
 import org.jclouds.b2.features.BucketApi;
+import org.jclouds.b2.features.ObjectApi;
 import org.jclouds.rest.annotations.Delegate;
 
 /** Provides access to Backblaze B2 resources via their REST API. */
@@ -29,4 +30,7 @@ public interface B2Api extends Closeable {
 
    @Delegate
    BucketApi getBucketApi();
+
+   @Delegate
+   ObjectApi getObjectApi();
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/binders/UploadFileBinder.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/binders/UploadFileBinder.java b/providers/b2/src/main/java/org/jclouds/b2/binders/UploadFileBinder.java
new file mode 100644
index 0000000..9384033
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/binders/UploadFileBinder.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.binders;
+
+import java.util.Map;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.b2.domain.UploadUrlResponse;
+import org.jclouds.b2.reference.B2Headers;
+import org.jclouds.rest.MapBinder;
+
+import com.google.common.net.HttpHeaders;
+import com.google.common.net.PercentEscaper;
+
+public final class UploadFileBinder implements MapBinder {
+   private static final PercentEscaper escaper = new PercentEscaper("._-/~!$'()*;=:@", false);
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Map<String, Object> postParams) {
+      UploadUrlResponse uploadUrl = (UploadUrlResponse) postParams.get("uploadUrl");
+      String fileName = (String) postParams.get("fileName");
+      Map<String, String> fileInfo = (Map<String, String>) postParams.get("fileInfo");
+      HttpRequest.Builder builder = request.toBuilder()
+            .endpoint(uploadUrl.uploadUrl())
+            .replaceHeader(HttpHeaders.AUTHORIZATION, uploadUrl.authorizationToken())
+            .replaceHeader(B2Headers.FILE_NAME, escaper.escape(fileName));
+      for (Map.Entry<String, String> entry : fileInfo.entrySet()) {
+         builder.replaceHeader(B2Headers.FILE_INFO_PREFIX + entry.getKey(), entry.getValue());
+      }
+      return (R) builder.build();
+   }
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Object input) {
+      throw new UnsupportedOperationException();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/Action.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/Action.java b/providers/b2/src/main/java/org/jclouds/b2/domain/Action.java
new file mode 100644
index 0000000..bd6c852
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/Action.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import com.google.common.base.CaseFormat;
+
+public enum Action {
+   UPLOAD,
+   HIDE;
+
+   public static Action fromValue(String symbol) {
+      return Action.valueOf(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, symbol));
+   }
+
+   @Override
+   public String toString() {
+      return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name());
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/B2Object.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/B2Object.java b/providers/b2/src/main/java/org/jclouds/b2/domain/B2Object.java
new file mode 100644
index 0000000..8b99a8e
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/B2Object.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.jclouds.io.Payload;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+
+@AutoValue
+public abstract class B2Object {
+   public abstract String fileId();
+   public abstract String fileName();
+   @Nullable public abstract String contentSha1();
+   @Nullable public abstract Map<String, String> fileInfo();
+   @Nullable public abstract Payload payload();
+   @Nullable public abstract Date uploadTimestamp();
+   @Nullable public abstract Action action();
+   @Nullable public abstract String accountId();
+   @Nullable public abstract String bucketId();
+   @Nullable public abstract Long contentLength();
+   @Nullable public abstract String contentType();
+
+   @SerializedNames({"fileId", "fileName", "accountId", "bucketId", "contentLength", "contentSha1", "contentType", "fileInfo", "action", "uploadTimestamp", "payload"})
+   public static B2Object create(String fileId, String fileName, @Nullable String accountId, @Nullable String bucketId, @Nullable Long contentLength, @Nullable String contentSha1, @Nullable String contentType, @Nullable Map<String, String> fileInfo, @Nullable Action action, @Nullable Long uploadTimestamp, @Nullable Payload payload) {
+      if ("none".equals(contentSha1)) {
+         // large files may have "none" sha1
+         contentSha1 = null;
+      }
+      if (fileInfo != null) {
+         fileInfo = ImmutableMap.copyOf(fileInfo);
+      }
+      Date date = uploadTimestamp == null ? null : new Date(uploadTimestamp);
+      return new AutoValue_B2Object(fileId, fileName, contentSha1, fileInfo, payload, date, action, accountId, bucketId, contentLength, contentType);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/B2ObjectList.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/B2ObjectList.java b/providers/b2/src/main/java/org/jclouds/b2/domain/B2ObjectList.java
new file mode 100644
index 0000000..780ab75
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/B2ObjectList.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import java.util.Date;
+import java.util.List;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+
+@AutoValue
+public abstract class B2ObjectList {
+   public abstract List<Entry> files();
+   @Nullable public abstract String nextFileId();
+   @Nullable public abstract String nextFileName();
+
+   @SerializedNames({"files", "nextFileId", "nextFileName"})
+   public static B2ObjectList create(List<Entry> files, @Nullable String nextFileId, @Nullable String nextFileName) {
+      return new AutoValue_B2ObjectList(ImmutableList.copyOf(files), nextFileId, nextFileName);
+   }
+
+   @AutoValue
+   public abstract static class Entry {
+      public abstract Action action();
+      public abstract String fileId();
+      public abstract String fileName();
+      public abstract long size();
+      public abstract Date uploadTimestamp();
+
+      @SerializedNames({"action", "fileId", "fileName", "size", "uploadTimestamp"})
+      public static Entry create(Action action, String fileId, String fileName, long size, long uploadTimestamp) {
+         return new AutoValue_B2ObjectList_Entry(action, fileId, fileName, size, new Date(uploadTimestamp));
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/DeleteFileResponse.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/DeleteFileResponse.java b/providers/b2/src/main/java/org/jclouds/b2/domain/DeleteFileResponse.java
new file mode 100644
index 0000000..21e5470
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/DeleteFileResponse.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class DeleteFileResponse {
+   public abstract String fileName();
+   public abstract String fileId();
+
+   @SerializedNames({"fileName", "fileId"})
+   public static DeleteFileResponse create(String fileName, String fileId) {
+      return new AutoValue_DeleteFileResponse(fileName, fileId);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/HideFileResponse.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/HideFileResponse.java b/providers/b2/src/main/java/org/jclouds/b2/domain/HideFileResponse.java
new file mode 100644
index 0000000..d7e5e11
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/HideFileResponse.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import java.util.Date;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class HideFileResponse {
+   /** Always "hide". */
+   public abstract Action action();
+   public abstract String fileId();
+   public abstract String fileName();
+   public abstract Date uploadTimestamp();
+
+   @SerializedNames({"action", "fileId", "fileName", "uploadTimestamp"})
+   public static HideFileResponse create(Action action, String fileId, String fileName, long uploadTimestamp) {
+      return new AutoValue_HideFileResponse(action, fileId, fileName, new Date(uploadTimestamp));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/UploadFileResponse.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/UploadFileResponse.java b/providers/b2/src/main/java/org/jclouds/b2/domain/UploadFileResponse.java
new file mode 100644
index 0000000..dc235db
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/UploadFileResponse.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import java.util.Map;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+
+@AutoValue
+public abstract class UploadFileResponse {
+   public abstract String fileId();
+   public abstract String fileName();
+   public abstract String accountId();
+   public abstract String bucketId();
+   public abstract long contentLength();
+   public abstract String contentSha1();
+   public abstract String contentType();
+   public abstract Map<String, String> fileInfo();
+
+   @SerializedNames({"fileId", "fileName", "accountId", "bucketId", "contentLength", "contentSha1", "contentType", "fileInfo"})
+   public static UploadFileResponse create(String fileId, String fileName, String accountId, String bucketId, long contentLength, String contentSha1, String contentType, Map<String, String> fileInfo) {
+      return new AutoValue_UploadFileResponse(fileId, fileName, accountId, bucketId, contentLength, contentSha1, contentType, ImmutableMap.copyOf(fileInfo));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/domain/UploadUrlResponse.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/domain/UploadUrlResponse.java b/providers/b2/src/main/java/org/jclouds/b2/domain/UploadUrlResponse.java
new file mode 100644
index 0000000..36712ca
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/domain/UploadUrlResponse.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.domain;
+
+import java.net.URI;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class UploadUrlResponse {
+   public abstract String bucketId();
+   public abstract URI uploadUrl();
+   public abstract String authorizationToken();
+
+   @SerializedNames({"bucketId", "uploadUrl", "authorizationToken"})
+   public static UploadUrlResponse create(String bucketId, URI uploadUrl, String authorizationToken) {
+      return new AutoValue_UploadUrlResponse(bucketId, uploadUrl, authorizationToken);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/features/ObjectApi.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/features/ObjectApi.java b/providers/b2/src/main/java/org/jclouds/b2/features/ObjectApi.java
new file mode 100644
index 0000000..4864864
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/features/ObjectApi.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.features;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import static org.jclouds.blobstore.attr.BlobScopes.CONTAINER;
+
+import java.util.Map;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.blobstore.attr.BlobScope;
+import org.jclouds.http.options.GetOptions;
+import org.jclouds.io.Payload;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.b2.binders.UploadFileBinder;
+import org.jclouds.b2.domain.B2Object;
+import org.jclouds.b2.domain.B2ObjectList;
+import org.jclouds.b2.domain.DeleteFileResponse;
+import org.jclouds.b2.domain.HideFileResponse;
+import org.jclouds.b2.domain.UploadFileResponse;
+import org.jclouds.b2.domain.UploadUrlResponse;
+import org.jclouds.b2.filters.RequestAuthorization;
+import org.jclouds.b2.filters.RequestAuthorizationDownload;
+import org.jclouds.b2.functions.ParseB2ObjectFromResponse;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.MapBinder;
+import org.jclouds.rest.annotations.PayloadParam;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+@BlobScope(CONTAINER)
+public interface ObjectApi {
+   @Named("b2_get_upload_url")
+   @POST
+   @Path("/b2api/v1/b2_get_upload_url")
+   @RequestFilters(RequestAuthorization.class)
+   @MapBinder(BindToJsonPayload.class)
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   UploadUrlResponse getUploadUrl(@PayloadParam("bucketId") String bucketId);
+
+   @Named("b2_upload_file")
+   @POST
+   @MapBinder(UploadFileBinder.class)
+   @Consumes(APPLICATION_JSON)
+   UploadFileResponse uploadFile(@PayloadParam("uploadUrl") UploadUrlResponse uploadUrl, @PayloadParam("fileName") String fileName, @HeaderParam("X-Bz-Content-Sha1") String contentSha1, @PayloadParam("fileInfo") Map<String, String> fileInfo, Payload payload);
+
+   @Named("b2_delete_file_version")
+   @POST
+   @Path("/b2api/v1/b2_delete_file_version")
+   @MapBinder(BindToJsonPayload.class)
+   @RequestFilters(RequestAuthorization.class)
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   DeleteFileResponse deleteFileVersion(@PayloadParam("fileName") String fileName, @PayloadParam("fileId") String fileId);
+
+   @Named("b2_get_file_info")
+   @POST
+   @Path("/b2api/v1/b2_get_file_info")
+   @MapBinder(BindToJsonPayload.class)
+   @RequestFilters(RequestAuthorization.class)
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   @Fallback(NullOnNotFoundOr404.class)
+   B2Object getFileInfo(@PayloadParam("fileId") String fileId);
+
+   @Named("b2_download_file_by_id")
+   @GET
+   @Path("/b2api/v1/b2_download_file_by_id")
+   @RequestFilters(RequestAuthorizationDownload.class)
+   @ResponseParser(ParseB2ObjectFromResponse.class)
+   @Fallback(NullOnNotFoundOr404.class)
+   B2Object downloadFileById(@QueryParam("fileId") String fileId);
+
+   @Named("b2_download_file_by_id")
+   @GET
+   @Path("/b2api/v1/b2_download_file_by_id")
+   @RequestFilters(RequestAuthorizationDownload.class)
+   @ResponseParser(ParseB2ObjectFromResponse.class)
+   @Fallback(NullOnNotFoundOr404.class)
+   B2Object downloadFileById(@QueryParam("fileId") String fileId, GetOptions options);
+
+   @Named("b2_download_file_by_name")
+   @GET
+   @Path("/file/{bucketName}/{fileName}")
+   @RequestFilters(RequestAuthorizationDownload.class)
+   @ResponseParser(ParseB2ObjectFromResponse.class)
+   @Fallback(NullOnNotFoundOr404.class)
+   B2Object downloadFileByName(@PathParam("bucketName") String bucketName, @PathParam("fileName") String fileName);
+
+   @Named("b2_download_file_by_name")
+   @GET
+   @Path("/file/{bucketName}/{fileName}")
+   @RequestFilters(RequestAuthorizationDownload.class)
+   @ResponseParser(ParseB2ObjectFromResponse.class)
+   @Fallback(NullOnNotFoundOr404.class)
+   B2Object downloadFileByName(@PathParam("bucketName") String bucketName, @PathParam("fileName") String fileName, GetOptions options);
+
+   @Named("b2_list_file_names")
+   @GET
+   @Path("/b2api/v1/b2_list_file_names")
+   @MapBinder(BindToJsonPayload.class)
+   @RequestFilters(RequestAuthorization.class)
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   B2ObjectList listFileNames(@PayloadParam("bucketId") String bucketId, @PayloadParam("startFileName") @Nullable String startFileName, @PayloadParam("maxFileCount") @Nullable Integer maxFileCount);
+
+   @Named("b2_list_file_versions")
+   @GET
+   @Path("/b2api/v1/b2_list_file_versions")
+   @MapBinder(BindToJsonPayload.class)
+   @RequestFilters(RequestAuthorization.class)
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   B2ObjectList listFileVersions(@PayloadParam("bucketId") String bucketId, @PayloadParam("startFileId") @Nullable String startFileId, @PayloadParam("startFileName") @Nullable String startFileName, @PayloadParam("maxFileCount") @Nullable Integer maxFileCount);
+
+   @Named("b2_hide_file")
+   @POST
+   @Path("/b2api/v1/b2_hide_file")
+   @MapBinder(BindToJsonPayload.class)
+   @RequestFilters(RequestAuthorization.class)
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   HideFileResponse hideFile(@PayloadParam("bucketId") String bucketId, @PayloadParam("fileName") String fileName);
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/filters/RequestAuthorizationDownload.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/filters/RequestAuthorizationDownload.java b/providers/b2/src/main/java/org/jclouds/b2/filters/RequestAuthorizationDownload.java
new file mode 100644
index 0000000..e5f01ed
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/filters/RequestAuthorizationDownload.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.filters;
+
+import java.net.URI;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.collect.Memoized;
+import org.jclouds.http.HttpException;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpRequestFilter;
+import org.jclouds.b2.domain.Authorization;
+
+import com.google.common.base.Supplier;
+import com.google.common.net.HttpHeaders;
+
+@Singleton
+public final class RequestAuthorizationDownload implements HttpRequestFilter {
+   private final Supplier<Authorization> auth;
+
+   @Inject
+   RequestAuthorizationDownload(@Memoized Supplier<Authorization> auth) {
+      this.auth = auth;
+   }
+
+   @Override
+   public HttpRequest filter(HttpRequest request) throws HttpException {
+      Authorization auth = this.auth.get();
+
+      // Replace with download URL
+      URI endpoint = request.getEndpoint();
+      endpoint = URI.create(auth.downloadUrl() +
+            (endpoint.getPort() == -1 ? "" : ":" + endpoint.getPort()) +
+            endpoint.getRawPath() +
+            (endpoint.getQuery() == null ? "" : "?" + endpoint.getQuery()));
+
+      request = request.toBuilder()
+            .endpoint(endpoint)
+            .replaceHeader(HttpHeaders.AUTHORIZATION, auth.authorizationToken())
+            .build();
+      return request;
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/functions/ParseB2ObjectFromResponse.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/functions/ParseB2ObjectFromResponse.java b/providers/b2/src/main/java/org/jclouds/b2/functions/ParseB2ObjectFromResponse.java
new file mode 100644
index 0000000..35fcab9
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/functions/ParseB2ObjectFromResponse.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.functions;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Date;
+import java.util.Map;
+
+import org.jclouds.http.HttpResponse;
+import org.jclouds.io.MutableContentMetadata;
+import org.jclouds.io.Payload;
+import org.jclouds.b2.domain.B2Object;
+import org.jclouds.b2.reference.B2Headers;
+
+import com.google.common.base.Function;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+
+public final class ParseB2ObjectFromResponse implements Function<HttpResponse, B2Object> {
+   @Override
+   public B2Object apply(HttpResponse from) {
+      Payload payload = from.getPayload();
+      MutableContentMetadata contentMeta = payload.getContentMetadata();
+
+      String fileId = from.getFirstHeaderOrNull(B2Headers.FILE_ID);
+      String fileName;
+      try {
+         fileName = URLDecoder.decode(from.getFirstHeaderOrNull(B2Headers.FILE_NAME), "UTF-8");
+      } catch (UnsupportedEncodingException uee) {
+         throw Throwables.propagate(uee);
+      }
+      String contentSha1 = from.getFirstHeaderOrNull(B2Headers.CONTENT_SHA1);
+      ImmutableMap.Builder<String, String> fileInfo = ImmutableMap.builder();
+      for (Map.Entry<String, String> entry : from.getHeaders().entries()) {
+         if (entry.getKey().regionMatches(true, 0, B2Headers.FILE_INFO_PREFIX, 0, B2Headers.FILE_INFO_PREFIX.length())) {
+            fileInfo.put(entry.getKey().substring(B2Headers.FILE_INFO_PREFIX.length()), entry.getValue());
+         }
+      }
+      Date uploadTimestamp = new Date(Long.parseLong(from.getFirstHeaderOrNull(B2Headers.UPLOAD_TIMESTAMP)));
+
+      return B2Object.create(fileId, fileName, null, null, contentMeta.getContentLength(), contentSha1, contentMeta.getContentType(), fileInfo.build(), null, uploadTimestamp.getTime(), payload);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/handlers/ParseB2ErrorFromJsonContent.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/handlers/ParseB2ErrorFromJsonContent.java b/providers/b2/src/main/java/org/jclouds/b2/handlers/ParseB2ErrorFromJsonContent.java
index b48760c..6442e28 100644
--- a/providers/b2/src/main/java/org/jclouds/b2/handlers/ParseB2ErrorFromJsonContent.java
+++ b/providers/b2/src/main/java/org/jclouds/b2/handlers/ParseB2ErrorFromJsonContent.java
@@ -17,6 +17,7 @@
 package org.jclouds.b2.handlers;
 
 import org.jclouds.blobstore.ContainerNotFoundException;
+import org.jclouds.blobstore.KeyNotFoundException;
 import org.jclouds.http.HttpCommand;
 import org.jclouds.http.HttpErrorHandler;
 import org.jclouds.http.HttpResponse;
@@ -24,6 +25,7 @@ import org.jclouds.http.functions.ParseJson;
 import org.jclouds.json.Json;
 import org.jclouds.b2.B2ResponseException;
 import org.jclouds.b2.domain.B2Error;
+import org.jclouds.rest.ResourceNotFoundException;
 
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
@@ -39,6 +41,12 @@ public final class ParseB2ErrorFromJsonContent extends ParseJson<B2Error> implem
          return new ContainerNotFoundException(exception);
       } else if ("bad_json".equals(error.code())) {
          return new IllegalArgumentException(error.message(), exception);
+      } else if ("bad_request".equals(error.code())) {
+         return new IllegalArgumentException(error.message(), exception);
+      } else if ("file_not_present".equals(error.code())) {
+         return new KeyNotFoundException(exception);
+      } else if ("not_found".equals(error.code())) {
+         return new ResourceNotFoundException(error.message(), exception);
       } else {
          return exception;
       }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/main/java/org/jclouds/b2/reference/B2Headers.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/main/java/org/jclouds/b2/reference/B2Headers.java b/providers/b2/src/main/java/org/jclouds/b2/reference/B2Headers.java
new file mode 100644
index 0000000..4937c1b
--- /dev/null
+++ b/providers/b2/src/main/java/org/jclouds/b2/reference/B2Headers.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.reference;
+
+public final class B2Headers {
+   public static final String CONTENT_SHA1 = "X-Bz-Content-Sha1";
+   public static final String FILE_ID = "X-Bz-File-Id";
+   public static final String FILE_NAME = "X-Bz-File-Name";
+   public static final String UPLOAD_TIMESTAMP = "X-Bz-Upload-Timestamp";
+   /**
+    * Recommended user metadata for last-modified.  The value should be a base 10 number which represents a UTC time
+    * when the original source file was last modified. It is a base 10 number of milliseconds since midnight, January
+    * 1, 1970 UTC.
+    */
+   public static final String LAST_MODIFIED = "X-Bz-Info-src_last_modified_millis";
+
+   public static final String FILE_INFO_PREFIX = "X-Bz-Info-";
+
+   private B2Headers() {
+      throw new AssertionError("intentionally unimplemented");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiLiveTest.java b/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiLiveTest.java
new file mode 100644
index 0000000..3de3cdb
--- /dev/null
+++ b/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiLiveTest.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.features;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Random;
+
+import org.jclouds.io.Payload;
+import org.jclouds.io.Payloads;
+import org.jclouds.b2.domain.Action;
+import org.jclouds.b2.domain.B2Object;
+import org.jclouds.b2.domain.B2ObjectList;
+import org.jclouds.b2.domain.Bucket;
+import org.jclouds.b2.domain.BucketType;
+import org.jclouds.b2.domain.HideFileResponse;
+import org.jclouds.b2.domain.UploadFileResponse;
+import org.jclouds.b2.domain.UploadUrlResponse;
+import org.jclouds.b2.internal.BaseB2ApiLiveTest;
+import org.jclouds.util.Closeables2;
+import org.jclouds.utils.TestUtils;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteSource;
+
+public final class ObjectApiLiveTest extends BaseB2ApiLiveTest {
+   private static final Random random = new Random();
+
+   @Test(groups = "live")
+   public void testGetFileInfo() throws Exception {
+      BucketApi bucketApi = api.getBucketApi();
+      ObjectApi objectApi = api.getObjectApi();
+
+      ByteSource byteSource = TestUtils.randomByteSource().slice(0, 1024);
+      Payload payload = Payloads.newByteSourcePayload(byteSource);
+      payload.getContentMetadata().setContentLength(byteSource.size());
+      String fileName = "file-name";
+      String contentSha1 = byteSource.hash(Hashing.sha1()).toString();
+      String contentType = "text/plain";
+      payload.getContentMetadata().setContentType(contentType);
+      Map<String, String> fileInfo = ImmutableMap.of("author", "unknown");
+
+      Bucket response = bucketApi.createBucket(getBucketName(), BucketType.ALL_PRIVATE);
+      UploadFileResponse uploadFile = null;
+      try {
+         UploadUrlResponse uploadUrl = objectApi.getUploadUrl(response.bucketId());
+
+         uploadFile = objectApi.uploadFile(uploadUrl, fileName, contentSha1, fileInfo, payload);
+
+         B2Object b2Object = objectApi.getFileInfo(uploadFile.fileId());
+         assertThat(b2Object.fileId()).isEqualTo(uploadFile.fileId());
+         assertThat(b2Object.fileName()).isEqualTo(fileName);
+         assertThat(b2Object.accountId()).isEqualTo(response.accountId());
+         assertThat(b2Object.bucketId()).isEqualTo(response.bucketId());
+         assertThat(b2Object.contentLength()).isEqualTo(byteSource.size());
+         assertThat(b2Object.contentSha1()).isEqualTo(contentSha1);
+         assertThat(b2Object.contentType()).isEqualTo(contentType);
+         assertThat(b2Object.fileInfo()).isEqualTo(fileInfo);
+         assertThat(b2Object.action()).isEqualTo(Action.UPLOAD);
+         assertThat(b2Object.uploadTimestamp()).isAfterYear(2015);
+         assertThat(b2Object.payload()).isNull();
+      } finally {
+         if (uploadFile != null) {
+            objectApi.deleteFileVersion(uploadFile.fileName(), uploadFile.fileId());
+         }
+         bucketApi.deleteBucket(response.bucketId());
+      }
+   }
+
+   @Test(groups = "live")
+   public void testDownloadFileById() throws Exception {
+      BucketApi bucketApi = api.getBucketApi();
+      ObjectApi objectApi = api.getObjectApi();
+
+      ByteSource byteSource = TestUtils.randomByteSource().slice(0, 1024);
+      Payload payload = Payloads.newByteSourcePayload(byteSource);
+      payload.getContentMetadata().setContentLength(byteSource.size());
+      String fileName = "file-name";
+      String contentSha1 = byteSource.hash(Hashing.sha1()).toString();
+      String contentType = "text/plain";
+      payload.getContentMetadata().setContentType(contentType);
+      Map<String, String> fileInfo = ImmutableMap.of("author", "unknown");
+
+      Bucket response = bucketApi.createBucket(getBucketName(), BucketType.ALL_PRIVATE);
+      UploadFileResponse uploadFile = null;
+      try {
+         UploadUrlResponse uploadUrl = objectApi.getUploadUrl(response.bucketId());
+
+         uploadFile = objectApi.uploadFile(uploadUrl, fileName, contentSha1, fileInfo, payload);
+
+         B2Object b2Object = objectApi.downloadFileById(uploadFile.fileId());
+         payload = b2Object.payload();
+         assertThat(b2Object.fileName()).isEqualTo(fileName);
+         assertThat(b2Object.contentSha1()).isEqualTo(contentSha1);
+         assertThat(b2Object.fileInfo()).isEqualTo(fileInfo);
+         assertThat(b2Object.uploadTimestamp()).isAfterYear(2015);
+         assertThat(payload.getContentMetadata().getContentType()).isEqualTo(contentType);
+
+         InputStream actual = null;
+         InputStream expected = null;
+         try {
+            actual = payload.openStream();
+            expected = byteSource.openStream();
+            assertThat(actual).hasContentEqualTo(expected);
+         } finally {
+            Closeables2.closeQuietly(expected);
+            Closeables2.closeQuietly(actual);
+         }
+      } finally {
+         if (uploadFile != null) {
+            objectApi.deleteFileVersion(uploadFile.fileName(), uploadFile.fileId());
+         }
+         bucketApi.deleteBucket(response.bucketId());
+      }
+   }
+
+   @Test(groups = "live")
+   public void testDownloadFileByName() throws Exception {
+      BucketApi bucketApi = api.getBucketApi();
+      ObjectApi objectApi = api.getObjectApi();
+
+      String bucketName = getBucketName();
+      ByteSource byteSource = TestUtils.randomByteSource().slice(0, 1024);
+      Payload payload = Payloads.newByteSourcePayload(byteSource);
+      payload.getContentMetadata().setContentLength(byteSource.size());
+      String fileName = "file name";  // intentionally using spaces in file name
+      String contentSha1 = byteSource.hash(Hashing.sha1()).toString();
+      String contentType = "text/plain";
+      payload.getContentMetadata().setContentType(contentType);
+      Map<String, String> fileInfo = ImmutableMap.of("author", "unknown");
+
+      Bucket response = bucketApi.createBucket(bucketName, BucketType.ALL_PRIVATE);
+      UploadFileResponse uploadFile = null;
+      try {
+         UploadUrlResponse uploadUrl = objectApi.getUploadUrl(response.bucketId());
+
+         uploadFile = objectApi.uploadFile(uploadUrl, fileName, contentSha1, fileInfo, payload);
+
+         B2Object b2Object = objectApi.downloadFileByName(bucketName, fileName);
+         payload = b2Object.payload();
+         assertThat(b2Object.fileName()).isEqualTo(fileName);
+         assertThat(b2Object.contentSha1()).isEqualTo(contentSha1);
+         assertThat(b2Object.fileInfo()).isEqualTo(fileInfo);
+         assertThat(b2Object.uploadTimestamp()).isAfterYear(2015);
+         assertThat(payload.getContentMetadata().getContentType()).isEqualTo(contentType);
+
+         InputStream actual = null;
+         InputStream expected = null;
+         try {
+            actual = payload.openStream();
+            expected = byteSource.openStream();
+            assertThat(actual).hasContentEqualTo(expected);
+         } finally {
+            Closeables2.closeQuietly(expected);
+            Closeables2.closeQuietly(actual);
+         }
+      } finally {
+         if (uploadFile != null) {
+            objectApi.deleteFileVersion(uploadFile.fileName(), uploadFile.fileId());
+         }
+         bucketApi.deleteBucket(response.bucketId());
+      }
+   }
+
+   @Test(groups = "live")
+   public void testListFileNames() throws Exception {
+      BucketApi bucketApi = api.getBucketApi();
+      ObjectApi objectApi = api.getObjectApi();
+
+      Bucket response = bucketApi.createBucket(getBucketName(), BucketType.ALL_PRIVATE);
+      int numFiles = 3;
+      ImmutableList.Builder<UploadFileResponse> uploadFiles = ImmutableList.builder();
+      try {
+         for (int i = 0; i < numFiles; ++i) {
+            uploadFiles.add(createFile(objectApi, response.bucketId(), "file" + i));
+         }
+
+         B2ObjectList list = objectApi.listFileNames(response.bucketId(), null, null);
+         assertThat(list.files()).hasSize(numFiles);
+      } finally {
+         for (UploadFileResponse uploadFile : uploadFiles.build()) {
+            objectApi.deleteFileVersion(uploadFile.fileName(), uploadFile.fileId());
+         }
+         bucketApi.deleteBucket(response.bucketId());
+      }
+   }
+
+   @Test(groups = "live")
+   public void testListFileVersions() throws Exception {
+      BucketApi bucketApi = api.getBucketApi();
+      ObjectApi objectApi = api.getObjectApi();
+
+      Bucket response = bucketApi.createBucket(getBucketName(), BucketType.ALL_PRIVATE);
+      int numFiles = 3;
+      ImmutableList.Builder<UploadFileResponse> uploadFiles = ImmutableList.builder();
+      try {
+         for (int i = 0; i < numFiles; ++i) {
+            uploadFiles.add(createFile(objectApi, response.bucketId(), "file"));
+         }
+
+         B2ObjectList list = objectApi.listFileNames(response.bucketId(), null, null);
+         assertThat(list.files()).hasSize(1);
+
+         list = objectApi.listFileVersions(response.bucketId(), null, null, null);
+         assertThat(list.files()).hasSize(numFiles);
+      } finally {
+         for (UploadFileResponse uploadFile : uploadFiles.build()) {
+            objectApi.deleteFileVersion(uploadFile.fileName(), uploadFile.fileId());
+         }
+         bucketApi.deleteBucket(response.bucketId());
+      }
+   }
+
+   @Test(groups = "live")
+   public void testHideFile() throws Exception {
+      BucketApi bucketApi = api.getBucketApi();
+      ObjectApi objectApi = api.getObjectApi();
+      String fileName = "file-name";
+
+      Bucket response = bucketApi.createBucket(getBucketName(), BucketType.ALL_PRIVATE);
+      UploadFileResponse uploadFile = null;
+      HideFileResponse hideFile = null;
+      try {
+         uploadFile = createFile(objectApi, response.bucketId(), fileName);
+
+         B2ObjectList list = objectApi.listFileNames(response.bucketId(), null, null);
+         assertThat(list.files()).hasSize(1);
+
+         hideFile = objectApi.hideFile(response.bucketId(), fileName);
+
+         list = objectApi.listFileNames(response.bucketId(), null, null);
+         assertThat(list.files()).isEmpty();
+
+         list = objectApi.listFileVersions(response.bucketId(), null, null, null);
+         assertThat(list.files()).hasSize(2);
+      } finally {
+         if (hideFile != null) {
+            objectApi.deleteFileVersion(hideFile.fileName(), hideFile.fileId());
+         }
+         if (uploadFile != null) {
+            objectApi.deleteFileVersion(uploadFile.fileName(), uploadFile.fileId());
+         }
+         bucketApi.deleteBucket(response.bucketId());
+      }
+   }
+
+   private static String getBucketName() {
+      return "jcloudstestbucket-" + random.nextInt(Integer.MAX_VALUE);
+   }
+
+   private static UploadFileResponse createFile(ObjectApi objectApi, String bucketId, String fileName) throws IOException {
+      ByteSource byteSource = TestUtils.randomByteSource().slice(0, 1024);
+      Payload payload = Payloads.newByteSourcePayload(byteSource);
+      payload.getContentMetadata().setContentLength(byteSource.size());
+      String contentSha1 = byteSource.hash(Hashing.sha1()).toString();
+      String contentType = "text/plain";
+      payload.getContentMetadata().setContentType(contentType);
+      Map<String, String> fileInfo = ImmutableMap.of("author", "unknown");
+
+      UploadUrlResponse uploadUrl = objectApi.getUploadUrl(bucketId);
+
+      return objectApi.uploadFile(uploadUrl, fileName, contentSha1, fileInfo, payload);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiMockTest.java b/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiMockTest.java
new file mode 100644
index 0000000..100f909
--- /dev/null
+++ b/providers/b2/src/test/java/org/jclouds/b2/features/ObjectApiMockTest.java
@@ -0,0 +1,545 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file 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.
+ */
+package org.jclouds.b2.features;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import java.util.Properties;
+
+import org.jclouds.ContextBuilder;
+import org.jclouds.blobstore.ContainerNotFoundException;
+import org.jclouds.blobstore.KeyNotFoundException;
+import org.jclouds.concurrent.config.ExecutorServiceModule;
+import org.jclouds.http.options.GetOptions;
+import org.jclouds.io.Payload;
+import org.jclouds.io.Payloads;
+import org.jclouds.b2.B2Api;
+import org.jclouds.b2.domain.Action;
+import org.jclouds.b2.domain.B2Object;
+import org.jclouds.b2.domain.B2ObjectList;
+import org.jclouds.b2.domain.DeleteFileResponse;
+import org.jclouds.b2.domain.HideFileResponse;
+import org.jclouds.b2.domain.UploadFileResponse;
+import org.jclouds.b2.domain.UploadUrlResponse;
+import org.jclouds.b2.reference.B2Headers;
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.net.HttpHeaders;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.google.inject.Module;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+@Test(groups = "unit", testName = "ObjectApiMockTest")
+public final class ObjectApiMockTest {
+   private final Set<Module> modules = ImmutableSet.<Module> of(
+         new ExecutorServiceModule(MoreExecutors.sameThreadExecutor()));
+
+   private static final String BUCKET_NAME = "BUCKET_NAME";
+   private static final String BUCKET_ID = "4a48fe8875c6214145260818";
+   private static final String FILE_ID = "4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104";
+   private static final String FILE_NAME = "typing_test.txt";
+   private static final String CONTENT_TYPE = "text/plain";
+   private static final String SHA1 = "bae5ed658ab3546aee12f23f36392f35dba1ebdd";
+   private static final String PAYLOAD = "The quick brown fox jumped over the lazy dog.\n";
+   private static final Map<String, String> FILE_INFO = ImmutableMap.of("author", "unknown");
+
+   public void testGetUploadUrl() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setBody(stringFromResource("/get_upload_url_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         UploadUrlResponse response = api.getUploadUrl(BUCKET_ID);
+         assertThat(response.bucketId()).isEqualTo(BUCKET_ID);
+         assertThat(response.uploadUrl()).isEqualTo(URI.create("https://pod-000-1005-03.backblaze.com/b2api/v1/b2_upload_file?cvt=c001_v0001005_t0027&bucket=4a48fe8875c6214145260818"));
+         assertThat(response.authorizationToken()).isEqualTo("2_20151009170037_f504a0f39a0f4e657337e624_9754dde94359bd7b8f1445c8f4cc1a231a33f714_upld");
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_get_upload_url", "/get_upload_url_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testGetUploadUrlDeletedBucket() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setResponseCode(400).setBody(stringFromResource("/get_upload_url_deleted_bucket_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         try {
+            api.getUploadUrl(BUCKET_ID);
+            failBecauseExceptionWasNotThrown(ContainerNotFoundException.class);
+         } catch (ContainerNotFoundException cnfe) {
+            // expected
+         }
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_get_upload_url", "/get_upload_url_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testUploadFile() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/upload_file_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         String accountId = "d522aa47a10f";
+
+         UploadUrlResponse uploadUrl = UploadUrlResponse.create(BUCKET_ID, server.getUrl("/b2api/v1/b2_upload_file/4a48fe8875c6214145260818/c001_v0001007_t0042").toURI(), "FAKE-AUTHORIZATION-TOKEN");
+         Payload payload = Payloads.newStringPayload(PAYLOAD);
+         payload.getContentMetadata().setContentType(CONTENT_TYPE);
+         UploadFileResponse response = api.uploadFile(uploadUrl, FILE_NAME, SHA1, FILE_INFO, payload);
+
+         assertThat(response.fileId()).isEqualTo(FILE_ID);
+         assertThat(response.fileName()).isEqualTo(FILE_NAME);
+         assertThat(response.accountId()).isEqualTo(accountId);
+         assertThat(response.bucketId()).isEqualTo(BUCKET_ID);
+         assertThat(response.contentLength()).isEqualTo(PAYLOAD.length());
+         assertThat(response.contentSha1()).isEqualTo(SHA1);
+         assertThat(response.contentType()).isEqualTo(CONTENT_TYPE);
+         assertThat(response.fileInfo()).isEqualTo(FILE_INFO);
+
+         assertThat(server.getRequestCount()).isEqualTo(1);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_upload_file/4a48fe8875c6214145260818/c001_v0001007_t0042");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testDeleteFileVersion() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setBody(stringFromResource("/delete_object_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         DeleteFileResponse response = api.deleteFileVersion(FILE_NAME, FILE_ID);
+         assertThat(response.fileName()).isEqualTo(FILE_NAME);
+         assertThat(response.fileId()).isEqualTo(FILE_ID);
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_delete_file_version", "/delete_object_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testDeleteAlreadyDeletedFileVersion() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setResponseCode(400).setBody(stringFromResource("/delete_file_version_already_deleted_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         try {
+            api.deleteFileVersion(FILE_NAME, FILE_ID);
+            failBecauseExceptionWasNotThrown(KeyNotFoundException.class);
+         } catch (KeyNotFoundException knfe) {
+            // expected
+         }
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_delete_file_version", "/delete_object_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testGetFileInfo() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setBody(stringFromResource("/get_file_info_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         B2Object b2Object = api.getFileInfo("4_ze73ede9c9c8412db49f60715_f100b4e93fbae6252_d20150824_m224353_c900_v8881000_t0001");
+         assertThat(b2Object.fileId()).isEqualTo("4_ze73ede9c9c8412db49f60715_f100b4e93fbae6252_d20150824_m224353_c900_v8881000_t0001");
+         assertThat(b2Object.fileName()).isEqualTo("akitty.jpg");
+         assertThat(b2Object.accountId()).isEqualTo("7eecc42b9675");
+         assertThat(b2Object.bucketId()).isEqualTo("e73ede9c9c8412db49f60715");
+         assertThat(b2Object.contentLength()).isEqualTo(122573);
+         assertThat(b2Object.contentSha1()).isEqualTo("a01a21253a07fb08a354acd30f3a6f32abb76821");
+         assertThat(b2Object.contentType()).isEqualTo("image/jpeg");
+         assertThat(b2Object.fileInfo()).isEqualTo(ImmutableMap.<String, String>of());
+         assertThat(b2Object.action()).isEqualTo(Action.UPLOAD);
+         assertThat(b2Object.uploadTimestamp()).isAfterYear(2014);
+         assertThat(b2Object.payload()).isNull();
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_get_file_info", "/get_file_info_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testGetFileInfoDeletedFileVersion() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setResponseCode(404).setBody(stringFromResource("/get_file_info_deleted_file_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         B2Object b2Object = api.getFileInfo("4_ze73ede9c9c8412db49f60715_f100b4e93fbae6252_d20150824_m224353_c900_v8881000_t0001");
+         assertThat(b2Object).isNull();
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_get_file_info", "/get_file_info_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testDownloadFileById() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+
+      server.enqueue(new MockResponse()
+            .addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE)
+            .addHeader(B2Headers.FILE_ID, FILE_ID)
+            .addHeader(B2Headers.FILE_NAME, FILE_NAME)
+            .addHeader(B2Headers.CONTENT_SHA1, SHA1)
+            .addHeader(B2Headers.UPLOAD_TIMESTAMP, String.valueOf(1500000000000L))
+            .addHeader(B2Headers.FILE_INFO_PREFIX + FILE_INFO.entrySet().iterator().next().getKey(), FILE_INFO.entrySet().iterator().next().getValue())
+            .setBody(PAYLOAD));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+
+         B2Object b2Object = api.downloadFileById(FILE_ID);
+
+         assertThat(b2Object.fileId()).isEqualTo(FILE_ID);
+         assertThat(b2Object.fileName()).isEqualTo(FILE_NAME);
+         assertThat(b2Object.contentSha1()).isEqualTo(SHA1);
+         assertThat(b2Object.fileInfo()).isEqualTo(FILE_INFO);
+         assertThat(b2Object.uploadTimestamp()).isAfterYear(2015);
+         assertThat(b2Object.payload().getContentMetadata().getContentLength()).isEqualTo(PAYLOAD.length());
+         assertThat(b2Object.payload().getContentMetadata().getContentType()).isEqualTo(CONTENT_TYPE);
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+
+         RecordedRequest request = server.takeRequest();
+         assertThat(request.getMethod()).isEqualTo("GET");
+         assertThat(request.getPath()).isEqualTo("/b2api/v1/b2_authorize_account");
+
+         request = server.takeRequest();
+         assertThat(request.getMethod()).isEqualTo("GET");
+         assertThat(request.getPath()).isEqualTo("/b2api/v1/b2_download_file_by_id?fileId=4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testDownloadFileByIdOptions() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+
+      server.enqueue(new MockResponse()
+            .addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE)
+            .addHeader(B2Headers.FILE_ID, FILE_ID)
+            .addHeader(B2Headers.FILE_NAME, FILE_NAME)
+            .addHeader(B2Headers.CONTENT_SHA1, SHA1)
+            .addHeader(B2Headers.UPLOAD_TIMESTAMP, String.valueOf(1500000000000L))
+            .addHeader(B2Headers.FILE_INFO_PREFIX + FILE_INFO.entrySet().iterator().next().getKey(), FILE_INFO.entrySet().iterator().next().getValue())
+            .setBody(PAYLOAD));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+
+         B2Object b2Object = api.downloadFileById(FILE_ID, new GetOptions().range(42, 69));
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+
+         RecordedRequest request = server.takeRequest();
+         assertThat(request.getMethod()).isEqualTo("GET");
+         assertThat(request.getPath()).isEqualTo("/b2api/v1/b2_authorize_account");
+
+         request = server.takeRequest();
+         assertThat(request.getMethod()).isEqualTo("GET");
+         assertThat(request.getPath()).isEqualTo("/b2api/v1/b2_download_file_by_id?fileId=4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104");
+         assertThat(request.getHeaders()).contains("Range: bytes=42-69");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testDownloadFileByName() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+
+      server.enqueue(new MockResponse()
+            .addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE)
+            .addHeader(B2Headers.FILE_ID, FILE_ID)
+            .addHeader(B2Headers.FILE_NAME, FILE_NAME)
+            .addHeader(B2Headers.CONTENT_SHA1, SHA1)
+            .addHeader(B2Headers.UPLOAD_TIMESTAMP, String.valueOf(1500000000000L))
+            .addHeader(B2Headers.FILE_INFO_PREFIX + FILE_INFO.entrySet().iterator().next().getKey(), FILE_INFO.entrySet().iterator().next().getValue())
+            .setBody(PAYLOAD));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+
+         B2Object b2Object = api.downloadFileByName(BUCKET_NAME, FILE_NAME);
+
+         assertThat(b2Object.fileId()).isEqualTo(FILE_ID);
+         assertThat(b2Object.fileName()).isEqualTo(FILE_NAME);
+         assertThat(b2Object.contentSha1()).isEqualTo(SHA1);
+         assertThat(b2Object.fileInfo()).isEqualTo(FILE_INFO);
+         assertThat(b2Object.uploadTimestamp()).isAfterYear(2015);
+         assertThat(b2Object.payload().getContentMetadata().getContentLength()).isEqualTo(PAYLOAD.length());
+         assertThat(b2Object.payload().getContentMetadata().getContentType()).isEqualTo(CONTENT_TYPE);
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+
+         RecordedRequest request = server.takeRequest();
+         assertThat(request.getMethod()).isEqualTo("GET");
+         assertThat(request.getPath()).isEqualTo("/b2api/v1/b2_authorize_account");
+
+         request = server.takeRequest();
+         assertThat(request.getMethod()).isEqualTo("GET");
+         assertThat(request.getPath()).isEqualTo("/file/BUCKET_NAME/typing_test.txt");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testListFileNames() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setBody(stringFromResource("/list_file_names_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         String accountId = "d522aa47a10f";
+
+         B2ObjectList list = api.listFileNames(BUCKET_ID, null, null);
+
+         assertThat(list.nextFileName()).isNull();
+         assertThat(list.files()).hasSize(2);
+
+         B2ObjectList.Entry object = list.files().get(0);
+         assertThat(object.action()).isEqualTo(Action.UPLOAD);
+         assertThat(object.fileId()).isEqualTo("4_z27c88f1d182b150646ff0b16_f1004ba650fe24e6b_d20150809_m012853_c100_v0009990_t0000");
+         assertThat(object.fileName()).isEqualTo("files/hello.txt");
+         assertThat(object.size()).isEqualTo(6);
+         assertThat(object.uploadTimestamp()).isEqualTo(new Date(1439083733000L));
+
+         object = list.files().get(1);
+         assertThat(object.action()).isEqualTo(Action.UPLOAD);
+         assertThat(object.fileId()).isEqualTo("4_z27c88f1d182b150646ff0b16_f1004ba650fe24e6c_d20150809_m012854_c100_v0009990_t0000");
+         assertThat(object.fileName()).isEqualTo("files/world.txt");
+         assertThat(object.size()).isEqualTo(6);
+         assertThat(object.uploadTimestamp()).isEqualTo(new Date(1439083734000L));
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_list_file_names", "/list_file_names_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testListFileVersions() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setBody(stringFromResource("/list_file_versions_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         String accountId = "d522aa47a10f";
+
+         B2ObjectList list = api.listFileVersions(BUCKET_ID, null, null, null);
+
+         assertThat(list.nextFileId()).isEqualTo("4_z27c88f1d182b150646ff0b16_f100920ddab886247_d20150809_m232316_c100_v0009990_t0003");
+         assertThat(list.nextFileName()).isEqualTo("files/world.txt");
+         assertThat(list.files()).hasSize(3);
+
+         B2ObjectList.Entry object = list.files().get(0);
+         assertThat(object.action()).isEqualTo(Action.UPLOAD);
+         assertThat(object.fileId()).isEqualTo("4_z27c88f1d182b150646ff0b16_f100920ddab886245_d20150809_m232316_c100_v0009990_t0003");
+         assertThat(object.fileName()).isEqualTo("files/hello.txt");
+         assertThat(object.size()).isEqualTo(6);
+         assertThat(object.uploadTimestamp()).isEqualTo(new Date(1439162596000L));
+
+         object = list.files().get(1);
+         assertThat(object.action()).isEqualTo(Action.HIDE);
+         assertThat(object.fileId()).isEqualTo("4_z27c88f1d182b150646ff0b16_f100920ddab886247_d20150809_m232323_c100_v0009990_t0005");
+         assertThat(object.fileName()).isEqualTo("files/world.txt");
+         assertThat(object.size()).isEqualTo(0);
+         assertThat(object.uploadTimestamp()).isEqualTo(new Date(1439162603000L));
+
+         object = list.files().get(2);
+         assertThat(object.action()).isEqualTo(Action.UPLOAD);
+         assertThat(object.fileId()).isEqualTo("4_z27c88f1d182b150646ff0b16_f100920ddab886246_d20150809_m232316_c100_v0009990_t0003");
+         assertThat(object.fileName()).isEqualTo("files/world.txt");
+         assertThat(object.size()).isEqualTo(6);
+         assertThat(object.uploadTimestamp()).isEqualTo(new Date(1439162596000L));
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_list_file_versions", "/list_file_versions_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testHideFile() throws Exception {
+      MockWebServer server = createMockWebServer();
+      server.enqueue(new MockResponse().setBody(stringFromResource("/authorize_account_response.json")));
+      server.enqueue(new MockResponse().setBody(stringFromResource("/hide_file_response.json")));
+
+      try {
+         ObjectApi api = api(server.getUrl("/").toString(), "b2").getObjectApi();
+         String accountId = "d522aa47a10f";
+
+         HideFileResponse response = api.hideFile(BUCKET_ID, FILE_NAME);
+         assertThat(response.action()).isEqualTo(Action.HIDE);
+         assertThat(response.fileId()).isEqualTo("4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104");
+         assertThat(response.fileName()).isEqualTo(FILE_NAME);
+         assertThat(response.uploadTimestamp()).isEqualTo(new Date(1437815673000L));
+
+         assertThat(server.getRequestCount()).isEqualTo(2);
+         assertAuthentication(server);
+         assertRequest(server.takeRequest(), "POST", "/b2api/v1/b2_hide_file", "/hide_file_request.json");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public B2Api api(String uri, String provider, Properties overrides) {
+      return ContextBuilder.newBuilder(provider)
+            .credentials("ACCOUNT_ID", "APPLICATION_KEY")
+            .endpoint(uri)
+            .overrides(overrides)
+            .modules(modules)
+            .buildApi(new TypeToken<B2Api>(getClass()) {});
+   }
+
+   public B2Api api(String uri, String provider) {
+      return api(uri, provider, new Properties());
+   }
+
+   public static MockWebServer createMockWebServer() throws IOException {
+      MockWebServer server = new MockWebServer();
+      server.play();
+      URL url = server.getUrl("");
+      return server;
+   }
+
+   public void assertAuthentication(MockWebServer server) {
+      assertThat(server.getRequestCount()).isGreaterThanOrEqualTo(1);
+      try {
+         assertThat(server.takeRequest().getRequestLine()).isEqualTo("GET /b2api/v1/b2_authorize_account HTTP/1.1");
+      } catch (InterruptedException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+
+   /**
+    * Ensures the request has a json header for the proper REST methods.
+    *
+    * @param request
+    * @param method
+    *           The request method (such as GET).
+    * @param path
+    *           The path requested for this REST call.
+    * @see RecordedRequest
+    */
+   public void assertRequest(RecordedRequest request, String method, String path) {
+      assertThat(request.getMethod()).isEqualTo(method);
+      assertThat(request.getPath()).isEqualTo(path);
+   }
+
+   /**
+    * Ensures the request is json and has the same contents as the resource
+    * file provided.
+    *
+    * @param request
+    * @param method
+    *           The request method (such as GET).
+    * @param resourceLocation
+    *           The location of the resource file. Contents will be compared to
+    *           the request body as JSON.
+    * @see RecordedRequest
+    */
+   public void assertRequest(RecordedRequest request, String method, String path, String resourceLocation) {
+      assertRequest(request, method, path);
+      assertContentTypeIsJson(request);
+      JsonParser parser = new JsonParser();
+      JsonElement requestJson;
+      try {
+         requestJson = parser.parse(new String(request.getBody(), Charsets.UTF_8));
+      } catch (Exception e) {
+         throw Throwables.propagate(e);
+      }
+      JsonElement resourceJson = parser.parse(stringFromResource(resourceLocation));
+      assertThat(requestJson).isEqualTo(resourceJson);
+   }
+
+   /**
+    * Ensures the request has a json header.
+    *
+    * @param request
+    * @see RecordedRequest
+    */
+   private void assertContentTypeIsJson(RecordedRequest request) {
+      assertThat(request.getHeaders()).contains("Content-Type: application/json");
+   }
+
+   /**
+    * Get a string from a resource
+    *
+    * @param resourceName
+    *           The name of the resource.
+    * @return The content of the resource
+    */
+   public String stringFromResource(String resourceName) {
+      try {
+         return Strings2.toStringAndClose(getClass().getResourceAsStream(resourceName));
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/delete_file_version_already_deleted_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/delete_file_version_already_deleted_response.json b/providers/b2/src/test/resources/delete_file_version_already_deleted_response.json
new file mode 100644
index 0000000..43dd03d
--- /dev/null
+++ b/providers/b2/src/test/resources/delete_file_version_already_deleted_response.json
@@ -0,0 +1,5 @@
+{
+   "status" : 400,
+   "code" : "file_not_present",
+   "message" : "File not present: file-name 4_za7acecf18b053f3258580715_f1036e7f957cafbe1_d20160609_m045216_c001_v0001011_t0035"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/delete_object_request.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/delete_object_request.json b/providers/b2/src/test/resources/delete_object_request.json
new file mode 100644
index 0000000..faccfa8
--- /dev/null
+++ b/providers/b2/src/test/resources/delete_object_request.json
@@ -0,0 +1,4 @@
+{
+    "fileName": "typing_test.txt",
+    "fileId": "4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/delete_object_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/delete_object_response.json b/providers/b2/src/test/resources/delete_object_response.json
new file mode 100644
index 0000000..768ce19
--- /dev/null
+++ b/providers/b2/src/test/resources/delete_object_response.json
@@ -0,0 +1,4 @@
+{
+    "fileId" : "4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104",
+    "fileName" : "typing_test.txt"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/get_file_info_deleted_file_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/get_file_info_deleted_file_response.json b/providers/b2/src/test/resources/get_file_info_deleted_file_response.json
new file mode 100644
index 0000000..5cf4eab
--- /dev/null
+++ b/providers/b2/src/test/resources/get_file_info_deleted_file_response.json
@@ -0,0 +1,5 @@
+{
+   "status" : 404,
+   "code" : "not_found",
+   "message" : "file_state_deleted"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/get_file_info_request.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/get_file_info_request.json b/providers/b2/src/test/resources/get_file_info_request.json
new file mode 100644
index 0000000..5ab3a63
--- /dev/null
+++ b/providers/b2/src/test/resources/get_file_info_request.json
@@ -0,0 +1,3 @@
+{
+    "fileId": "4_ze73ede9c9c8412db49f60715_f100b4e93fbae6252_d20150824_m224353_c900_v8881000_t0001"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/get_file_info_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/get_file_info_response.json b/providers/b2/src/test/resources/get_file_info_response.json
new file mode 100644
index 0000000..b322d86
--- /dev/null
+++ b/providers/b2/src/test/resources/get_file_info_response.json
@@ -0,0 +1,12 @@
+{
+    "accountId": "7eecc42b9675",
+    "bucketId": "e73ede9c9c8412db49f60715",
+    "contentLength": 122573,
+    "contentSha1": "a01a21253a07fb08a354acd30f3a6f32abb76821",
+    "contentType": "image/jpeg",
+    "fileId": "4_ze73ede9c9c8412db49f60715_f100b4e93fbae6252_d20150824_m224353_c900_v8881000_t0001",
+    "fileInfo": {},
+    "fileName": "akitty.jpg",
+    "action": "upload",
+    "uploadTimestamp": 1439083733000
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/get_upload_url_deleted_bucket_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/get_upload_url_deleted_bucket_response.json b/providers/b2/src/test/resources/get_upload_url_deleted_bucket_response.json
new file mode 100644
index 0000000..007a1eb
--- /dev/null
+++ b/providers/b2/src/test/resources/get_upload_url_deleted_bucket_response.json
@@ -0,0 +1,5 @@
+{
+   "status" : 400,
+   "code" : "bad_bucket_id",
+   "message" : "Bucket b7ecac119bd53f3258580715 does not exist"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/get_upload_url_request.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/get_upload_url_request.json b/providers/b2/src/test/resources/get_upload_url_request.json
new file mode 100644
index 0000000..80cb5ba
--- /dev/null
+++ b/providers/b2/src/test/resources/get_upload_url_request.json
@@ -0,0 +1,3 @@
+{
+    "bucketId" : "4a48fe8875c6214145260818"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/get_upload_url_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/get_upload_url_response.json b/providers/b2/src/test/resources/get_upload_url_response.json
new file mode 100644
index 0000000..0be7f61
--- /dev/null
+++ b/providers/b2/src/test/resources/get_upload_url_response.json
@@ -0,0 +1,5 @@
+{
+    "bucketId" : "4a48fe8875c6214145260818",
+    "uploadUrl" : "https://pod-000-1005-03.backblaze.com/b2api/v1/b2_upload_file?cvt=c001_v0001005_t0027&bucket=4a48fe8875c6214145260818",
+    "authorizationToken" : "2_20151009170037_f504a0f39a0f4e657337e624_9754dde94359bd7b8f1445c8f4cc1a231a33f714_upld"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/hide_file_request.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/hide_file_request.json b/providers/b2/src/test/resources/hide_file_request.json
new file mode 100644
index 0000000..10e05e0
--- /dev/null
+++ b/providers/b2/src/test/resources/hide_file_request.json
@@ -0,0 +1,4 @@
+{
+    "bucketId": "4a48fe8875c6214145260818",
+    "fileName": "typing_test.txt"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/hide_file_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/hide_file_response.json b/providers/b2/src/test/resources/hide_file_response.json
new file mode 100644
index 0000000..85c6853
--- /dev/null
+++ b/providers/b2/src/test/resources/hide_file_response.json
@@ -0,0 +1,6 @@
+{
+    "action" : "hide",
+    "fileId" : "4_h4a48fe8875c6214145260818_f000000000000472a_d20140104_m032022_c001_v0000123_t0104",
+    "fileName" : "typing_test.txt",
+    "uploadTimestamp" : 1437815673000
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/list_file_names_request.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/list_file_names_request.json b/providers/b2/src/test/resources/list_file_names_request.json
new file mode 100644
index 0000000..32b805c
--- /dev/null
+++ b/providers/b2/src/test/resources/list_file_names_request.json
@@ -0,0 +1,3 @@
+{
+    "bucketId": "4a48fe8875c6214145260818"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/list_file_names_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/list_file_names_response.json b/providers/b2/src/test/resources/list_file_names_response.json
new file mode 100644
index 0000000..51d95ba
--- /dev/null
+++ b/providers/b2/src/test/resources/list_file_names_response.json
@@ -0,0 +1,19 @@
+{
+    "files": [
+        {
+            "action": "upload",
+            "fileId": "4_z27c88f1d182b150646ff0b16_f1004ba650fe24e6b_d20150809_m012853_c100_v0009990_t0000",
+            "fileName": "files/hello.txt",
+            "size": 6,
+            "uploadTimestamp": 1439083733000
+        },
+        {
+            "action": "upload",
+            "fileId": "4_z27c88f1d182b150646ff0b16_f1004ba650fe24e6c_d20150809_m012854_c100_v0009990_t0000",
+            "fileName": "files/world.txt",
+            "size": 6,
+            "uploadTimestamp": 1439083734000
+        }
+    ],
+    "nextFileName": null
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/list_file_versions_request.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/list_file_versions_request.json b/providers/b2/src/test/resources/list_file_versions_request.json
new file mode 100644
index 0000000..b083b91
--- /dev/null
+++ b/providers/b2/src/test/resources/list_file_versions_request.json
@@ -0,0 +1,3 @@
+{
+   "bucketId": "4a48fe8875c6214145260818"
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/bd6a495a/providers/b2/src/test/resources/list_file_versions_response.json
----------------------------------------------------------------------
diff --git a/providers/b2/src/test/resources/list_file_versions_response.json b/providers/b2/src/test/resources/list_file_versions_response.json
new file mode 100644
index 0000000..e7aaf48
--- /dev/null
+++ b/providers/b2/src/test/resources/list_file_versions_response.json
@@ -0,0 +1,27 @@
+{
+    "files": [
+        {
+            "action": "upload",
+            "fileId": "4_z27c88f1d182b150646ff0b16_f100920ddab886245_d20150809_m232316_c100_v0009990_t0003",
+            "fileName": "files/hello.txt",
+            "size": 6,
+            "uploadTimestamp": 1439162596000
+        },
+        {
+            "action": "hide",
+            "fileId": "4_z27c88f1d182b150646ff0b16_f100920ddab886247_d20150809_m232323_c100_v0009990_t0005",
+            "fileName": "files/world.txt",
+            "size": 0,
+            "uploadTimestamp": 1439162603000
+        },
+        {
+            "action": "upload",
+            "fileId": "4_z27c88f1d182b150646ff0b16_f100920ddab886246_d20150809_m232316_c100_v0009990_t0003",
+            "fileName": "files/world.txt",
+            "size": 6,
+            "uploadTimestamp": 1439162596000
+        }
+    ],
+    "nextFileId": "4_z27c88f1d182b150646ff0b16_f100920ddab886247_d20150809_m232316_c100_v0009990_t0003",
+    "nextFileName": "files/world.txt"
+}