Skip to content

Commit

Permalink
feat(Storage): Refactor list objects API to include path (#3580)
Browse files Browse the repository at this point in the history
* feat(Storage): Refactor list objects API to include `path`

* working on review comments
  • Loading branch information
harsh62 authored Mar 21, 2024
1 parent 597473a commit 8364412
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,22 @@ public struct StorageListRequest: AmplifyOperationRequest {
/// - Tag: StorageListRequest
public let options: Options

/// The unique path for the object in storage
///
/// - Tag: StorageListRequest.path
public let path: (any StoragePath)?

/// - Tag: StorageListRequest.init
@available(*, deprecated, message: "Use init(path:options)")
public init(options: Options) {
self.options = options
self.path = nil
}

/// - Tag: StorageListRequest.init
public init(path: any StoragePath, options: Options) {
self.options = options
self.path = path
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,12 @@ extension AWSS3StoragePlugin {
options: StorageListRequest.Options? = nil
) async throws -> StorageListResult {
let options = options ?? StorageListRequest.Options()
let prefix = "" //TODO: resolve path
let result = try await storageService.list(prefix: prefix, options: options)

let channel = HubChannel(from: categoryType)
let payload = HubPayload(eventName: HubPayload.EventName.Storage.list, context: options, data: result)
Amplify.Hub.dispatch(to: channel, payload: payload)

return result
let request = StorageListRequest(path: path, options: options)
let task = AWSS3StorageListObjectsTask(
request,
storageConfiguration: storageConfiguration,
storageBehaviour: storageService)
return try await task.value
}

public func handleBackgroundEvents(identifier: String) async -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ protocol AWSS3StorageServiceBehavior {
accelerate: Bool?,
onEvent: @escaping StorageServiceMultiPartUploadEventHandler)

@available(*, deprecated, message: "Use `AWSS3StorageListObjectsTask` instead")
func list(prefix: String,
options: StorageListRequest.Options) async throws -> StorageListResult

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation
import AWSS3
import AWSPluginsCore

protocol StorageListObjectsTask: AmplifyTaskExecution where Request == StorageListRequest, Success == StorageListResult, Failure == StorageError {}

class AWSS3StorageListObjectsTask: StorageListObjectsTask, DefaultLogger {

let request: StorageListRequest
let storageConfiguration: AWSS3StoragePluginConfiguration
let storageBehaviour: AWSS3StorageServiceBehavior

init(_ request: StorageListRequest,
storageConfiguration: AWSS3StoragePluginConfiguration,
storageBehaviour: AWSS3StorageServiceBehavior) {
self.request = request
self.storageConfiguration = storageConfiguration
self.storageBehaviour = storageBehaviour
}

var eventName: HubPayloadEventName {
HubPayload.EventName.Storage.list
}

var eventNameCategoryType: CategoryType {
.storage
}

func execute() async throws -> StorageListResult {
guard let path = try await request.path?.resolvePath() else {
throw StorageError.validation(
"path",
"`path` is required for removing an object",
"Make sure that a valid `path` is passed for removing an object")
}
let input = ListObjectsV2Input(bucket: storageBehaviour.bucket,
continuationToken: request.options.nextToken,
delimiter: nil,
maxKeys: Int(request.options.pageSize),
prefix: path,
startAfter: nil)
do {
let response = try await storageBehaviour.client.listObjectsV2(input: input)
let contents: S3BucketContents = response.contents ?? []
let items = try contents.map { s3Object in
guard let key = s3Object.key else {
throw StorageError.unknown("Missing key in response")
}
return StorageListResult.Item(
path: path,
key: key,
eTag: s3Object.eTag,
lastModified: s3Object.lastModified)
}
return StorageListResult(items: items, nextToken: response.nextContinuationToken)
} catch let error as StorageErrorConvertible {
throw error.storageError
} catch {
throw StorageError.service(
"Service error occurred.",
"Please inspect the underlying error for more details.",
error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase {
/// - Given: A configured Storage GetURL Task with mocked service
/// - When: AWSS3StorageGetURLTask value is invoked
/// - Then: A URL should be returned.
func testRemoveTaskSuccess() async throws {
func testGetURLTaskSuccess() async throws {

let somePath = "/path"
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
Expand All @@ -42,7 +42,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase {
/// - Given: A configured Storage GetURL Task with mocked service, throwing `NotFound` exception
/// - When: AWSS3StorageGetURLTask value is invoked
/// - Then: A storage service error should be returned, with an underlying service error
func testRemoveTaskNoBucket() async throws {
func testGetURLTaskNoBucket() async throws {
let somePath = "/path"

let serviceMock = MockAWSS3StorageService()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSPluginsCore
@testable import AWSS3StoragePlugin
@testable import AWSPluginsTestCommon
import AWSS3

class AWSS3StorageListObjectsTaskTests: XCTestCase {

/// - Given: A configured Storage List Objects Task with mocked service
/// - When: AWSS3StorageListObjectsTask value is invoked
/// - Then: A list of keys should be returned.
func testListObjectsTaskSuccess() async throws {
let serviceMock = MockAWSS3StorageService()
let client = serviceMock.client as! MockS3Client
client.listObjectsV2Handler = { input in
return .init(
contents: [
.init(eTag: "tag", key: "key", lastModified: Date()),
.init(eTag: "tag", key: "key", lastModified: Date())],
nextContinuationToken: "continuationToken"
)
}

let request = StorageListRequest(
path: StringStoragePath.fromString("/path"), options: .init())
let task = AWSS3StorageListObjectsTask(
request,
storageConfiguration: AWSS3StoragePluginConfiguration(),
storageBehaviour: serviceMock)
let value = try await task.value
XCTAssertEqual(value.items.count, 2)
XCTAssertEqual(value.nextToken, "continuationToken")
XCTAssertEqual(value.items[0].eTag, "tag")
XCTAssertEqual(value.items[0].key, "key")
XCTAssertNotNil(value.items[0].lastModified)

}

/// - Given: A configured ListObjects Remove Task with mocked service, throwing `NoSuchKey` exception
/// - When: AWSS3StorageListObjectsTask value is invoked
/// - Then: A storage service error should be returned, with an underlying service error
func testListObjectsTaskNoBucket() async throws {
let serviceMock = MockAWSS3StorageService()
let client = serviceMock.client as! MockS3Client
client.listObjectsV2Handler = { input in
throw AWSS3.NoSuchKey()
}

let request = StorageListRequest(
path: StringStoragePath.fromString("/path"), options: .init())
let task = AWSS3StorageListObjectsTask(
request,
storageConfiguration: AWSS3StoragePluginConfiguration(),
storageBehaviour: serviceMock)
do {
_ = try await task.value
XCTFail("Task should throw an exception")
}
catch {
guard let storageError = error as? StorageError,
case .service(_, _, let underlyingError) = storageError else {
XCTFail("Should throw a Storage service error, instead threw \(error)")
return
}
XCTAssertTrue(underlyingError is AWSS3.NoSuchKey,
"Underlying error should be NoSuchKey, instead got \(String(describing: underlyingError))")
}
}

/// - Given: A configured Storage ListObjects Task with invalid path
/// - When: AWSS3StorageListObjectsTask value is invoked
/// - Then: A storage validation error should be returned
func testListObjectsTaskWithInvalidPath() async throws {
let serviceMock = MockAWSS3StorageService()

let request = StorageListRequest(
path: StringStoragePath.fromString("path"), options: .init())
let task = AWSS3StorageListObjectsTask(
request,
storageConfiguration: AWSS3StoragePluginConfiguration(),
storageBehaviour: serviceMock)
do {
_ = try await task.value
XCTFail("Task should throw an exception")
}
catch {
guard let storageError = error as? StorageError,
case .validation(let field, _, _, _) = storageError else {
XCTFail("Should throw a storage validation error, instead threw \(error)")
return
}

XCTAssertEqual(field, "path", "Field in error should be `path`")
}
}

}

0 comments on commit 8364412

Please sign in to comment.