From 83686e3570903acef465a26f59ba8b02f15db2f5 Mon Sep 17 00:00:00 2001 From: Ondrej Rafaj Date: Sat, 12 May 2018 18:05:55 +0100 Subject: [PATCH] listing objects works --- Sources/S3/Extensions/S3+List.swift | 26 ++++++++++ Sources/S3/Models/BucketResults.swift | 68 ++++++++++++++++++++++++++- Sources/S3/Models/Object.swift | 42 +++++++++++++++++ Sources/S3/Models/Owner.swift | 7 +++ Sources/S3/Protocols/S3Client.swift | 43 +++++++++++++++++ Sources/S3DemoApp/S3DemoApp.swift | 17 +++++-- 6 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 Sources/S3/Models/Object.swift diff --git a/Sources/S3/Extensions/S3+List.swift b/Sources/S3/Extensions/S3+List.swift index ee49468..09bda04 100644 --- a/Sources/S3/Extensions/S3+List.swift +++ b/Sources/S3/Extensions/S3+List.swift @@ -11,6 +11,32 @@ import Foundation // Helper S3 extension for getting file indexes extension S3 { + /// Get list of objects + public func list(bucket: String, region: Region? = nil, headers: [String: String], on container: Container) throws -> Future { + let signer = try container.makeS3Signer() + let region = region ?? signer.config.region + guard let baseUrl = URL(string: "https://\(bucket).s3.\(region.rawValue).amazonaws.com/"), let host = baseUrl.host, + var components = URLComponents(string: baseUrl.absoluteString) else { + throw S3.Error.invalidUrl + } + components.queryItems = [ + URLQueryItem(name: "list-type", value: "2") + ] + guard let url = components.url else { + throw S3.Error.invalidUrl + } + var headers = headers + headers["Host"] = host + let awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, headers: headers, payload: .none) + return try make(request: url, method: .GET, headers: awsHeaders, data: "".convertToData(), on: container).map(to: BucketResults.self) { response in + try self.check(response) + return try response.decode(to: BucketResults.self) + } + } + /// Get list of objects + public func list(bucket: String, region: Region? = nil, on container: Container) throws -> Future { + return try list(bucket: bucket, region: region, headers: [:], on: container) + } } diff --git a/Sources/S3/Models/BucketResults.swift b/Sources/S3/Models/BucketResults.swift index da40167..e1ecd72 100644 --- a/Sources/S3/Models/BucketResults.swift +++ b/Sources/S3/Models/BucketResults.swift @@ -9,6 +9,72 @@ import Foundation import Vapor -public class BucketResults { +public struct BucketResults: Content { + + /// Name of the bucket + public let name: String + + /// Keys that begin with the indicated prefix + public let prefix: String? + + /** + All of the keys rolled up into a common prefix count as a single return when calculating the number of returns. See MaxKeys. + + A response can contain CommonPrefixes only if you specify a delimiter. + CommonPrefixes contains all (if there are any) keys between Prefix and the next occurrence of the string specified by a delimiter. + CommonPrefixes lists keys that act like subdirectories in the directory specified by Prefix. + For example, if the prefix is notes/ and the delimiter is a slash (/) as in notes/summer/july, the common prefix is notes/summer/. All of the keys that roll up into a common prefix count as a single return when calculating the number of returns. See MaxKeys + */ + public let commonPrefixes: [CommonPrefix]? + + /// Returns the number of keys included in the response. The value is always less than or equal to the MaxKeys value + public let keyCount: Int + + /// The maximum number of keys returned in the response body + public let maxKeys: Int + + /// Causes keys that contain the same string between the prefix and the first occurrence of the delimiter to be rolled up into a single result element in the CommonPrefixes collection. These rolled-up keys are not returned elsewhere in the response. Each rolled-up result counts as only one return against the MaxKeys value + public let delimiter: String? + + /// Encoding type used by Amazon S3 to encode object key names in the XML response + public let encodingType: String? + + /// Pagination; If StartAfter was sent with the request, it is included in the response + public let startAfter: String? + + /// If the response is truncated, Amazon S3 returns this parameter with a continuation token. You can specify the token as the continuation-token in your next request to retrieve the next set of keys + public let nextContinuationToken: String? + + /// Set to false if all of the results were returned. Set to true if more keys are available to return. If the number of results exceeds that specified by MaxKeys, all of the results might not be returned + public let isTruncated: Bool + + /// Objects + public let objects: [Object] + + enum CodingKeys: String, CodingKey { + case name = "Name" + case prefix = "Prefix" + case commonPrefixes = "CommonPrefixes" + case keyCount = "KeyCount" + case maxKeys = "MaxKeys" + case delimiter = "Delimiter" + case encodingType = "Encoding-Type" + case startAfter = "StartAfter" + case nextContinuationToken = "NextContinuationToken" + case isTruncated = "IsTruncated" + case objects = "Contents" + } + +} + + +public struct CommonPrefix: Codable { + + /// Common prefix name + let path: String + + enum CodingKeys: String, CodingKey { + case path = "Prefix" + } } diff --git a/Sources/S3/Models/Object.swift b/Sources/S3/Models/Object.swift new file mode 100644 index 0000000..ead94d1 --- /dev/null +++ b/Sources/S3/Models/Object.swift @@ -0,0 +1,42 @@ +// +// Object.swift +// S3 +// +// Created by Ondrej Rafaj on 12/05/2018. +// + +import Foundation +import Vapor + + +/// S3 object +public struct Object: Content { + + /// The object's key / file name + let fileName: String + + /// STANDARD | STANDARD_IA | ONEZONE_IA | REDUCED_REDUNDANCY | GLACIER + public let storageClass: String? + + /// The entity tag is an MD5 hash of the object. ETag reflects only changes to the contents of an object, not its metadata + public let etag: String + + /// Owner + public let owner: Owner? + + /// Size in bytes of the object + public let size: Int? + + /// Date and time the object was last modified + public let lastModified: Date + + enum CodingKeys: String, CodingKey { + case fileName = "Key" + case storageClass = "StorageClass" + case etag = "ETag" + case owner = "Owner" + case size = "Size" + case lastModified = "LastModified" + } + +} diff --git a/Sources/S3/Models/Owner.swift b/Sources/S3/Models/Owner.swift index 311bc29..a660316 100644 --- a/Sources/S3/Models/Owner.swift +++ b/Sources/S3/Models/Owner.swift @@ -12,7 +12,14 @@ import Vapor /// Owner object public struct Owner: Content { + /// Owner's ID public let id: String + + /** + Owner's name + - *This value is only included in the response in the US East (N. Virginia), US West (N. California), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), EU (Ireland), and South America (São Paulo) regions.* + - *For a list of all the Amazon S3 supported regions and endpoints, see Regions and Endpoints in the AWS General Reference.* + */ public let name: String? enum CodingKeys: String, CodingKey { diff --git a/Sources/S3/Protocols/S3Client.swift b/Sources/S3/Protocols/S3Client.swift index 02f7e01..d871efa 100644 --- a/Sources/S3/Protocols/S3Client.swift +++ b/Sources/S3/Protocols/S3Client.swift @@ -9,32 +9,75 @@ import Foundation import Vapor +/// S3 client Protocol public protocol S3Client: Service { + + /// Get list of objects func buckets(on: Container) throws -> Future + + /// Create a bucket func create(bucket: String, region: Region?, on container: Container) throws -> Future + + /// Delete a bucket wherever it is +// func delete(bucket: String, on container: Container) throws -> Future + + /// Delete a bucket func delete(bucket: String, region: Region?, on container: Container) throws -> Future + + /// Get bucket location // func location(bucket: String, on container: Container) throws -> Future + /// Get list of objects + func list(bucket: String, region: Region?, on container: Container) throws -> Future + + /// Get list of objects + func list(bucket: String, region: Region?, headers: [String: String], on container: Container) throws -> Future + + /// Upload file to S3 func put(file: File.Upload, headers: [String: String], on: Container) throws -> EventLoopFuture + /// Upload file to S3 func put(file url: URL, destination: String, access: AccessControlList, on: Container) throws -> Future + + /// Upload file to S3 func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + /// Upload file to S3 func put(file path: String, destination: String, access: AccessControlList, on: Container) throws -> Future + + /// Upload file to S3 func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + /// Upload file to S3 func put(string: String, destination: String, on: Container) throws -> Future + + /// Upload file to S3 func put(string: String, destination: String, access: AccessControlList, on: Container) throws -> Future + + /// Upload file to S3 func put(string: String, mime: MediaType, destination: String, on: Container) throws -> Future + + /// Upload file to S3 func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on: Container) throws -> Future + + /// Upload file to S3 func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + /// Retrieve file data from S3 func get(fileInfo file: LocationConvertible, on container: Container) throws -> Future + + /// Retrieve file data from S3 func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future + /// Retrieve file data from S3 func get(file: LocationConvertible, on: Container) throws -> Future + + /// Retrieve file data from S3 func get(file: LocationConvertible, headers: [String: String], on: Container) throws -> Future + /// Delete file from S3 func delete(file: LocationConvertible, on: Container) throws -> Future + + /// Delete file from S3 func delete(file: LocationConvertible, headers: [String: String], on: Container) throws -> Future } diff --git a/Sources/S3DemoApp/S3DemoApp.swift b/Sources/S3DemoApp/S3DemoApp.swift index 429cc2b..5fb2c8c 100644 --- a/Sources/S3DemoApp/S3DemoApp.swift +++ b/Sources/S3DemoApp/S3DemoApp.swift @@ -29,15 +29,26 @@ public func routes(_ router: Router) throws { let s3 = try req.makeS3Client() return try s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) { return ":)" - }.catchMap({ (error) -> (String) in + }.catchMap({ (error) -> (String) in if let error = error.s3ErroMessage() { return error.message } return ":(" - } + } ) } + // Delete bucket + router.get("files") { req -> Future in + let s3 = try req.makeS3Client() + return try s3.list(bucket: "booststore", region: .usEast1, headers: [:], on: req).catchMap({ (error) -> (BucketResults) in + if let error = error.s3ErroMessage() { + print(error.message) + } + throw error + }) + } + // // Bucket location // router.get("bucket/location") { req -> Future in // let s3 = try req.makeS3Client() @@ -58,7 +69,7 @@ public func routes(_ router: Router) throws { // } // Demonstrate work with files - router.get("files") { req -> Future in + router.get("files/test") { req -> Future in let string = "Content of my example file" let fileName = "file-hu.txt"