Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Storage): Refactor list objects API to include path #3580

Merged
merged 2 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`")
}
}

}
Loading