diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d05f6e..5e45ef0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: MacOS: macos: - xcode: "9.0" + xcode: "10.0.0" steps: - checkout - restore_cache: @@ -11,9 +11,11 @@ jobs: - run: name: Install CMySQL and CTLS command: | + export HOMEBREW_NO_AUTO_UPDATE=1 brew tap vapor/homebrew-tap brew install cmysql brew install ctls + brew install libressl - run: name: Build and Run Tests no_output_timeout: 1800 @@ -25,12 +27,12 @@ jobs: command: | bash <(curl -s https://codecov.io/bash) - save_cache: - key: v1-spm-deps-{{ checksum "Package.swift" }} + key: v2-spm-deps-{{ checksum "Package.swift" }} paths: - .build Linux: docker: - - image: brettrtoomey/vapor-ci:0.0.1 + - image: nodesvapor/vapor-ci:swift-4.2 steps: - checkout - restore_cache: diff --git a/.gitignore b/.gitignore index 60e8a1d..960e827 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +Package.resolved *.xcodeproj Packages/ diff --git a/.swiftlint.yml b/.swiftlint.yml index 1e79593..4cad0e0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,7 +5,7 @@ function_body_length: variable_name: min_length: warning: 2 -line_length: 80 +line_length: 100 disabled_rules: - opening_brace colon: diff --git a/Package.swift b/Package.swift index fd94be0..d256a6a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,27 @@ +// swift-tools-version:4.2 import PackageDescription let package = Package( name: "Storage", + products: [ + .library( + name: "Storage", + targets: ["Storage"] + ) + ], dependencies: [ - .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), - .Package(url: "https://github.com/nodes-vapor/data-uri.git", majorVersion: 1), - .Package(url: "https://github.com/nodes-vapor/aws.git", majorVersion: 1), - .Package(url: "https://github.com/manGoweb/MimeLib.git", majorVersion: 1) + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), + ], + targets: [ + .target( + name: "Storage", + dependencies: [ + "Vapor" + ] + ), + .testTarget( + name: "StorageTests", + dependencies: ["Storage"] + ) ] ) diff --git a/README.md b/README.md index 7ffc67e..84657cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Storage 🗄 -[![Swift Version](https://img.shields.io/badge/Swift-3-brightgreen.svg)](http://swift.org) -[![Vapor Version](https://img.shields.io/badge/Vapor-2-F6CBCA.svg)](http://vapor.codes) +[![Swift Version](https://img.shields.io/badge/Swift-4.2-brightgreen.svg)](http://swift.org) +[![Vapor Version](https://img.shields.io/badge/Vapor-3-30B6FC.svg)](http://vapor.codes) [![Circle CI](https://circleci.com/gh/nodes-vapor/storage/tree/master.svg?style=shield)](https://circleci.com/gh/nodes-vapor/storage) [![codebeat badge](https://codebeat.co/badges/58eeca2c-7b58-4aea-9b09-d80e3b79de19)](https://codebeat.co/projects/github-com-nodes-vapor-storage-master) [![codecov](https://codecov.io/gh/nodes-vapor/storage/branch/master/graph/badge.svg)](https://codecov.io/gh/nodes-vapor/storage) @@ -15,7 +15,7 @@ A package to ease the use of multiple storage and CDN services. * [Upload a file](#upload-a-file-) * [Base 64 and data URI](#base-64-and-data-uri-) * [Download a file](#download-a-file-) -* [Get CDN path](#get-cdn-path) +* [Get CDN path](#get-cdn-path-) * [Delete a file](#delete-a-file-) * [Configuration](#configuration-) * [Network driver](#network-driver-) @@ -31,28 +31,7 @@ Update your `Package.swift` file. ## Getting started 🚀 - -`Storage` offers a [Provider](https://vapor.github.io/documentation/guide/provider.html) and does all configuration through JSON files. - -```swift -import Storage -try drop.addProvider(StorageProvider.self) -``` - -Now, create a JSON file named `Config/storage.json` with the following contents: - -```json -{ - "driver": "s3", - "bucket": "$AWS_S3_BUCKET", - "accessKey": "$AWS_ACCESS_KEY", - "secretKey": "$AWS_SECRET_KEY", - "host": "s3.amazonaws.com", - "cdnUrl": "$CDN_BASE_URL" -} -``` -Learn about [these fields and more](#configuration-). - +Storage makes it easy to start uploading and downloading files. Just register a [network driver](#network-driver) and get going. ## Upload a file 🌐 @@ -63,28 +42,32 @@ Storage.upload( fileName: String?, fileExtension: String?, mime: String?, - folder: String + folder: String, + on container: Container ) throws -> String ``` The aforementioned function will attempt to upload the file using your [selected driver and template](#configuration-) and will return a `String` representing the location of the file. If you want to upload an image named `profile.png` your call site would look like: ```swift -let path = try Storage.upload(bytes: bytes, fileName: "profile.png") -print(path) //prints `/profile.png` +try Storage.upload( + bytes: bytes, + fileName: "profile.png", + on: req +) ``` #### Base64 and data URI 📡 Is your file a base64 or data URI? No problem! ```swift -Storage.upload(base64: "SGVsbG8sIFdvcmxkIQ==", fileName: "base64.txt") -Storage.upload(dataURI: "data:,Hello%2C%20World!", fileName: "data-uri.txt") +Storage.upload(base64: "SGVsbG8sIFdvcmxkIQ==", fileName: "base64.txt", on: req) +Storage.upload(dataURI: "data:,Hello%2C%20World!", fileName: "data-uri.txt", on: req) ``` #### Remote resources Download an asset from a URL and then reupload it to your storage server. ```swift -Storage.upload(url: "http://mysite.com/myimage.png", fileName: "profile.png") +Storage.upload(url: "http://mysite.com/myimage.png", fileName: "profile.png", on: req) ``` @@ -93,7 +76,7 @@ Storage.upload(url: "http://mysite.com/myimage.png", fileName: "profile.png") To download a file that was previously uploaded you simply use the generated path. ```swift //download image as `Foundation.Data` -let data = try Storage.get("/images/profile.png") +let data = try Storage.get("/images/profile.png", on: req) ``` @@ -128,18 +111,18 @@ try Storage.delete("/images/profile.png") #### Network driver 🔨 The network driver is the module responsible for interacting with your 3rd party service. The default, and currently the only, driver is `s3`. -```json -{ - "driver": "s3", - "bucket": "$AWS_S3_BUCKET", - "accessKey": "$AWS_ACCESS_KEY", - "secretKey": "$AWS_SECRET_KEY", - "host": "s3.amazonaws.com", - "cdnUrl": "$CDN_BASE_URL", - "region": "eu-west-1" -} +```swift +import Storage + +let driver = S3Driver( + bucket: "bucket", + accessKey: "access", + secretKey: "secret" +) + +services.register(driver) ``` -The `driver` key is optional and will default to `s3`. `accessKey` and `secretKey` are both required by the S3 driver, while `host`, `bucket` and `region` are all optional. `region` will default to `eu-west-1` and `host` will default to `s3.amazonaws.com` if not provided. The above example uses the environment variables as provided by [Vapor Cloud](https://vapor.cloud/), but you can change this to use hardcoded values although this is not recommended. Another option is to use "fallback values" which can be achieved by using the `:` notion. For example `$AWS_S3_BUCKET:my-bucket` will fallback to `my-bucket` when the `AWS_S3_BUCKET` environment variable is not present. +`bucket`, `accessKey`and `secretKey` are required by the S3 driver, while `template`, `host` and `region` are optional. `region` will default to `eu-west-1` and `host` will default to `s3.amazonaws.com`. #### Upload path 🛣 A times, you may need to upload files to a different scheme than `/file.ext`. You can achieve this by adding the `"template"` field to your `Config/storage.json`. If the field is omitted it will default to `/#file`. diff --git a/Sources/Storage/FileEntity.swift b/Sources/Storage/FileEntity.swift index e182735..bd2e172 100644 --- a/Sources/Storage/FileEntity.swift +++ b/Sources/Storage/FileEntity.swift @@ -1,41 +1,37 @@ import Core -import MimeLib /// Representation of a to-be-uploaded file. public struct FileEntity { - public enum Error : Swift.Error { + public enum Error: Swift.Error { case missingFilename case missingFileExtension case malformedFileName } - - //TODO(Brett): considering changing all `String` fields to `Bytes`. - + /// The raw bytes of the file. - var bytes: Bytes? - - + var bytes: Data? + // The file's name with the extension. var fullFileName: String? { guard let fileName = fileName, let fileExtension = fileExtension else { return nil } - + return [fileName, fileExtension].joined(separator: ".") } - + /// The file's name without the extension. var fileName: String? - + /// The file's extension. var fileExtension: String? - + /// The folder the file was uploaded from. var folder: String? - + /// The type of the file. var mime: String? - + /** FileEntity's default initializer. @@ -47,7 +43,7 @@ public struct FileEntity { - mime: The type of the file. */ public init( - bytes: Bytes? = nil, + bytes: Data? = nil, fileName: String? = nil, fileExtension: String? = nil, folder: String? = nil, @@ -58,7 +54,7 @@ public struct FileEntity { self.fileExtension = fileExtension self.folder = folder self.mime = mime - + sanitize() } } @@ -68,7 +64,7 @@ extension FileEntity { guard fileName != nil else { throw Error.missingFilename } - + guard fileExtension != nil else { throw Error.missingFileExtension } @@ -80,15 +76,15 @@ extension FileEntity { guard let fileName = fullFileName else { throw Error.malformedFileName } - + var path = [ fileName ] - + if let folder = folder { path.insert(folder, at: 0) } - + return path.joined(separator: "/") } } @@ -96,44 +92,43 @@ extension FileEntity { extension FileEntity { mutating func sanitize() { guard let fileName = fileName, fileName.contains(".") else { return } - + let components = fileName.components(separatedBy: ".") - + // don't override if a programmer provided an extension if fileExtension == nil { fileExtension = components.last } - + self.fileName = components.dropLast().joined(separator: ".") } - - + @discardableResult mutating func loadMimeFromFileExtension() -> Bool { guard let fileExtension = fileExtension?.lowercased() else { return false } - + // MimeLib doesn't support `jpg` so do a check here first guard fileExtension != "jpg" else { self.mime = "image/jpeg" return true } - - guard let mime = Mime.get(fileExtension: fileExtension)?.rawValue else { + + guard let mime = getMime(for: fileExtension) else { return false } - + self.mime = mime return true } - + @discardableResult mutating func loadFileExtensionFromMime() -> Bool { guard let mime = mime else { return false } - - guard let fileExtension = Mime.fileExtension(forMime: mime) else { + + guard let fileExtension = getExtension(for: mime) else { return false } - + self.fileExtension = fileExtension return true } diff --git a/Sources/Storage/NetworkDriver.swift b/Sources/Storage/NetworkDriver.swift index 65536e5..59da8d7 100644 --- a/Sources/Storage/NetworkDriver.swift +++ b/Sources/Storage/NetworkDriver.swift @@ -1,69 +1,86 @@ -import S3 import Core +import Vapor import Foundation -protocol NetworkDriver { +public protocol NetworkDriver: Service { var pathBuilder: PathBuilder { get set } - - @discardableResult func upload(entity: inout FileEntity) throws -> String - func get(path: String) throws -> Bytes - func delete(path: String) throws + + @discardableResult + func upload(entity: inout FileEntity, on container: Container) throws -> Future + func get(path: String, on container: Container) throws -> Future<[UInt8]> + func delete(path: String, on container: Container) throws -> Future } -final class S3Driver: NetworkDriver { +public final class S3Driver: NetworkDriver { enum Error: Swift.Error { case nilFileUpload case missingFileExtensionAndType case pathMissingForwardSlash } - - var pathBuilder: PathBuilder - + + public var pathBuilder: PathBuilder var s3: S3 - - init(s3: S3, pathBuilder: PathBuilder) { - self.pathBuilder = pathBuilder - self.s3 = s3 + + public init( + bucket: String, + host: String = "s3.amazonaws.com", + accessKey: String, + secretKey: String, + region: Region = .euWest1, + pathTemplate: String = "" + ) throws { + self.pathBuilder = try ConfigurablePathBuilder(template: pathTemplate) + self.s3 = S3( + host: "\(bucket).\(host)", + accessKey: accessKey, + secretKey: secretKey, + region: region + ) } - + @discardableResult - func upload(entity: inout FileEntity) throws -> String { + public func upload(entity: inout FileEntity, on container: Container) throws -> Future { guard let bytes = entity.bytes else { throw Error.nilFileUpload } - + entity.sanitize() - + if entity.fileExtension == nil { guard entity.loadFileExtensionFromMime() else { throw Error.missingFileExtensionAndType } } - + if entity.mime == nil { guard entity.loadMimeFromFileExtension() else { throw Error.missingFileExtensionAndType } } - + let path = try pathBuilder.build(entity: entity) - + guard path.hasPrefix("/") else { print("The S3 driver requires your path to begin with `/`") print("Please check `template` in `storage.json`.") throw Error.pathMissingForwardSlash } - - try s3.upload(bytes: bytes, path: path, access: .publicRead) - - return path + + return try s3.upload( + bytes: Data(bytes), + path: path, + access: .publicRead, + on: container + ).map { _ in + return path + } } - - func get(path: String) throws -> Bytes { - return try s3.get(path: path) + + public func get(path: String, on container: Container) throws -> Future<[UInt8]> { + return container.future([]) } - - func delete(path: String) throws { - return try s3.delete(file: path) + + public func delete(path: String, on container: Container) throws -> Future { + return container.future() } } diff --git a/Sources/Storage/PathBuilder.swift b/Sources/Storage/PathBuilder.swift index ab642a9..ebc0503 100644 --- a/Sources/Storage/PathBuilder.swift +++ b/Sources/Storage/PathBuilder.swift @@ -1,24 +1,23 @@ -protocol PathBuilder { +public protocol PathBuilder { func build(entity: FileEntity) throws -> String func generateFolder(for mime: String?) -> String? } -extension PathBuilder { - func generateFolder(for mime: String?) -> String? { +public extension PathBuilder { + public func generateFolder(for mime: String?) -> String? { guard let mime = mime else { return nil } - return mime.lowercased().hasPrefix("image") ? "images/original" : "data" } } -struct ConfigurablePathBuilder: PathBuilder { +public struct ConfigurablePathBuilder: PathBuilder { var template: Template - - init(template: String) throws { + + public init(template: String) throws { self.template = try Template.compile(template) } - - func build(entity: FileEntity) throws -> String { + + public func build(entity: FileEntity) throws -> String { return try template.renderPath(for: entity, generateFolder) } } diff --git a/Sources/Storage/Provider.swift b/Sources/Storage/Provider.swift deleted file mode 100644 index 092c104..0000000 --- a/Sources/Storage/Provider.swift +++ /dev/null @@ -1,82 +0,0 @@ -import S3 -import Vapor - -import enum AWSSignatureV4.Region - -///A provider for configuring the `Storage` package. -public final class StorageProvider: Provider { - - public static var repositoryName: String = "Storage" - - public enum Error: Swift.Error { - case missingConfigurationFile - case unsupportedDriver(String) - case missingAccessKey - case missingSecretKey - case missingBucket - case unknownRegion(String) - } - - public init(config: Config) throws { - guard let config = config["storage"] else { - throw Error.missingConfigurationFile - } - - let networkDriver = try buildNetworkDriver(config: config) - Storage.networkDriver = networkDriver - Storage.cdnBaseURL = config["cdnUrl"]?.string - } - - public func boot(_ drop: Droplet) {} - - public func boot(_ config: Config) throws {} - - public func afterInit(_ drop: Droplet) {} - - public func beforeRun(_: Droplet) {} - - private func buildNetworkDriver(config: Config) throws -> NetworkDriver { - let template = config["template"]?.string ?? "/#file" - let networkDriver: NetworkDriver - let driver = config["driver"]?.string ?? "s3" - switch driver { - case "s3": - networkDriver = try buildS3Driver(config: config, template: template) - default: - throw Error.unsupportedDriver(driver) - } - - return networkDriver - } - - private func buildS3Driver(config: Config, template: String) throws -> S3Driver { - guard let accessKey = config["accessKey"]?.string else { - throw Error.missingAccessKey - } - - guard let secretKey = config["secretKey"]?.string else { - throw Error.missingSecretKey - } - - guard let bucket = config["bucket"]?.string else { - throw Error.missingBucket - } - - let host = config["host"]?.string ?? "s3.amazonaws.com" - - let regionString = config["region"]?.string ?? "eu-west-1" - guard let region = Region(rawValue: regionString) else { - throw Error.unknownRegion(regionString) - } - - let s3 = S3( - host: "\(bucket).\(host)", - accessKey: accessKey, - secretKey: secretKey, - region: region - ) - - let pathBuilder = try ConfigurablePathBuilder(template: template) - return S3Driver(s3: s3, pathBuilder: pathBuilder) - } -} diff --git a/Sources/Storage/Storage.swift b/Sources/Storage/Storage.swift index c77f38d..2667a46 100644 --- a/Sources/Storage/Storage.swift +++ b/Sources/Storage/Storage.swift @@ -1,10 +1,7 @@ import Core import HTTP import Vapor -import DataURI -import Transport import Foundation -import FormData public class Storage { public enum Error: Swift.Error { @@ -12,12 +9,11 @@ public class Storage { case cdnBaseURLNotSet case missingFileName } - - static var networkDriver: NetworkDriver? + static var cdnBaseURL: String? - + public static var cdnPathBuilder: ((String, String) -> String)? - + /** Uploads the given `FileEntity`. @@ -27,63 +23,11 @@ public class Storage { - Returns: The path the file was uploaded to. */ @discardableResult - public static func upload(entity: inout FileEntity) throws -> String { - guard let networkDriver = networkDriver else { - throw Error.missingNetworkDriver - } - - return try networkDriver.upload(entity: &entity) - } - - @discardableResult - public static func upload( - formData: Field, - fileName overrideFileName: String? = nil, - fileExtension: String? = nil, - folder: String? = nil - ) throws -> String { - let fileName = formData.filename - let bytes = formData.part.body - - return try upload( - bytes: bytes, - fileName: overrideFileName ?? fileName, - fileExtension: fileExtension, - folder: folder - ) - } - - /** - Downloads the file located at `url` and then uploads it. - - - Parameters: - - url: The location of the file to be downloaded. - - fileName: The name of the file. - - fileExtension: The extension of the file. - - folder: The folder to save the file in. - - - Returns: The path the file was uploaded to. - */ - @discardableResult - public static func upload( - url: String, - fileName: String, - fileExtension: String? = nil, - folder: String? = nil - ) throws -> String { - let response = try EngineClient.factory.get(url) - var entity = FileEntity( - fileName: fileName, - fileExtension: fileExtension, - folder: folder - ) - - entity.bytes = response.body.bytes - entity.mime = response.contentType - - return try upload(entity: &entity) + public static func upload(entity: inout FileEntity, on container: Container) throws -> Future { + let networkDriver = try container.make(NetworkDriver.self) + return try networkDriver.upload(entity: &entity, on: container) } - + /** Uploads bytes to a storage server. @@ -98,12 +42,13 @@ public class Storage { */ @discardableResult public static func upload( - bytes: Bytes, + bytes: Data, fileName: String? = nil, fileExtension: String? = nil, mime: String? = nil, - folder: String? = nil - ) throws -> String { + folder: String? = nil, + on container: Container + ) throws -> Future { var entity = FileEntity( bytes: bytes, fileName: fileName, @@ -111,39 +56,10 @@ public class Storage { folder: folder, mime: mime ) - - return try upload(entity: &entity) - } - - /** - Uploads a base64 encoded URI to a storage server. - - - Parameters: - - base64: The raw, base64 encoded, bytes of the file in `String` representation. - - fileName: The name of the file. - - fileExtension: The extension of the file. - - mime: The mime type of the file. - - folder: The folder to save the file in. - - - Returns: The path the file was uploaded to. - */ - @discardableResult - public static func upload( - base64: String, - fileName: String? = nil, - fileExtension: String? = nil, - mime: String? = nil, - folder: String? = nil - ) throws -> String { - return try upload( - bytes: base64.makeBytes().base64Decoded, - fileName: fileName, - fileExtension: fileExtension, - mime: mime, - folder: folder - ) + + return try upload(entity: &entity, on: container) } - + /** Decodes and uploads a data URI. @@ -160,75 +76,71 @@ public class Storage { dataURI: String, fileName: String? = nil, fileExtension: String? = nil, - folder: String? = nil - ) throws -> String { + folder: String? = nil, + on container: Container + ) throws -> Future { let (bytes, type) = try dataURI.dataURIDecoded() return try upload( bytes: bytes, fileName: fileName, fileExtension: fileExtension, mime: type, - folder: folder + folder: folder, + on: container ) } - + /** Downloads the file at `path`. - Parameters: - path: The path of the file to be downloaded. - - Returns: The downloaded file as `Bytes`/`[UInt8]`. + - Returns: The downloaded file. */ - public static func get(path: String) throws -> Bytes { - guard let networkDriver = networkDriver else { - throw Error.missingNetworkDriver - } - - return try networkDriver.get(path: path) + public static func get(path: String, on container: Container) throws -> Future<[UInt8]> { + let networkDriver = try container.make(NetworkDriver.self) + return try networkDriver.get(path: path, on: container) } - + /// Appends the asset's path with the base CDN URL. public static func getCDNPath(for path: String) throws -> String { guard let cdnBaseURL = cdnBaseURL else { throw Error.cdnBaseURLNotSet } - + if let cdnPathBuilder = cdnPathBuilder { return cdnPathBuilder(cdnBaseURL, path) } - + return cdnBaseURL + path } - + /// Appends the asset's path with the base CDN URL. With support for optional public static func getCDNPath(optional path: String?) throws -> String? { guard let pathUnwrapped = path else { return nil } - + guard let cdnBaseURL = cdnBaseURL else { throw Error.cdnBaseURLNotSet } - + if let cdnPathBuilder = cdnPathBuilder { return cdnPathBuilder(cdnBaseURL, pathUnwrapped) } - + return cdnBaseURL + pathUnwrapped } - + /** Deletes the file at `path`. - Parameters: - path: The path of the file to be deleted. */ - public static func delete(path: String) throws { - guard let networkDriver = networkDriver else { - throw Error.missingNetworkDriver - } - - try networkDriver.delete(path: path) + public static func delete(path: String, on container: Container) throws -> Future { + let networkDriver = try container.make(NetworkDriver.self) + return try networkDriver.delete(path: path, on: container) } } diff --git a/Sources/Storage/Template.swift b/Sources/Storage/Template.swift index f649ced..cb88fe8 100644 --- a/Sources/Storage/Template.swift +++ b/Sources/Storage/Template.swift @@ -2,14 +2,14 @@ import Core import Random import Foundation -extension Byte { +extension UInt8 { /// # - static var octothorp: Byte = 0x23 + static var octothorp: UInt8 = 0x23 } struct Template { let calendar = Calendar(identifier: .gregorian) - + enum Error: Swift.Error { case invalidAlias(String) case failedToExtractDate @@ -20,7 +20,7 @@ struct Template { case mimeNotProvided case mimeFolderNotProvided } - + enum Alias: String { case file = "#file" case fileName = "#fileName" @@ -34,16 +34,16 @@ struct Template { case timestamp = "#timestamp" case uuid = "#uuid" } - + enum PathPart { - case literal(Bytes) + case literal([UInt8]) case alias(Alias) } - - var scanner: Scanner + + var scanner: Scanner var parts: [PathPart] = [] - - init(scanner: Scanner) { + + init(scanner: Scanner) { self.scanner = scanner } } @@ -51,82 +51,82 @@ struct Template { extension Template { static func compile(_ templateString: String) throws -> Template { var template = Template(scanner: Scanner(templateString.bytes)) - + while let part = try template.extractPart() { template.parts.append(part) } - + return template } - + func renderPath( for entity: FileEntity, _ mimeFolderBuilder: (String?) -> String? ) throws -> String { let dateComponents = getDateComponents() - - var pathBytes: [Byte] = [] - + + var pathUInt8s: [UInt8] = [] + for part in parts { switch part { case .literal(let bytes): - pathBytes += bytes + pathUInt8s += bytes case .alias(let alias): switch alias { case .file: guard let fullFileName = entity.fullFileName else { throw Error.malformedFileName } - pathBytes += fullFileName.bytes - + pathUInt8s += fullFileName.bytes + case .fileName: guard let fileName = entity.fileName else { throw Error.fileNameNotProvided } - pathBytes += fileName.bytes - + pathUInt8s += fileName.bytes + case .fileExtension: guard let fileExtension = entity.fileExtension else { throw Error.fileExtensionNotProvided } - pathBytes += fileExtension.bytes - + pathUInt8s += fileExtension.bytes + case .folder: guard let folder = entity.folder else { throw Error.folderNotProvided } - pathBytes += folder.bytes - + pathUInt8s += folder.bytes + case .mime: guard let mime = entity.mime else { throw Error.mimeNotProvided } - pathBytes += mime.bytes - + pathUInt8s += mime.bytes + case .mimeFolder: guard let mimeFolder = mimeFolderBuilder(entity.mime) else { throw Error.mimeFolderNotProvided } - pathBytes += mimeFolder.bytes - + pathUInt8s += mimeFolder.bytes + case .day: guard let day = dateComponents.day else { throw Error.failedToExtractDate } - pathBytes += "\(day)".bytes - + pathUInt8s += "\(day)".bytes + case .month: guard let month = dateComponents.month else { throw Error.failedToExtractDate } - pathBytes += "\(month)".bytes - + pathUInt8s += "\(month)".bytes + case .year: guard let year = dateComponents.year else { throw Error.failedToExtractDate } - pathBytes += "\(year)".bytes - + pathUInt8s += "\(year)".bytes + case .timestamp: guard let hours = dateComponents.hour, @@ -136,24 +136,24 @@ extension Template { throw Error.failedToExtractDate } let time = formatTime(hours: hours, minutes: minutes, seconds: seconds) - pathBytes += time.bytes - + pathUInt8s += time.bytes + case .uuid: - let uuidBytes = UUID().uuidString.bytes - pathBytes += uuidBytes + let uuidUInt8s = UUID().uuidString.bytes + pathUInt8s += uuidUInt8s } } } - - return String(bytes: pathBytes) + + return String(bytes: pathUInt8s, encoding: .utf8) ?? "" } } extension Template { mutating func extractPart() throws -> PathPart? { guard let byte = scanner.peek() else { return nil } - - if byte == Byte.octothorp { + + if byte == UInt8.octothorp { return try extractAlias() } else { return extractLiteral() @@ -184,54 +184,54 @@ extension Template { insert(into: _trie, Alias.uuid) return _trie }() - + mutating func extractAlias() throws -> PathPart { - var partial: [Byte] = [] - + var partial: [UInt8] = [] + var peeked = 0 defer { scanner.pop(peeked) } - + var current = Template.trie - + while let byte = scanner.peek(aheadBy: peeked) { peeked += 1 - + guard let next = current[byte] else { break } - + if let value = next.value { - if let nextByte = scanner.peek(aheadBy: peeked) { - guard next[nextByte] != nil else { + if let nextUInt8 = scanner.peek(aheadBy: peeked) { + guard next[nextUInt8] != nil else { return .alias(value) } - partial += byte + partial.append(byte) current = next continue } - + return .alias(value) } - - partial += byte + + partial.append(byte) current = next } - let invalidAlias = String(bytes: partial) + let invalidAlias = String(bytes: partial, encoding: .utf8) ?? "" throw Error.invalidAlias(invalidAlias) } - + mutating func extractLiteral() -> PathPart { - var partial: [Byte] = [] + var partial: [UInt8] = [] var peeked = 0 defer { scanner.pop(peeked) } - + while let byte = scanner.peek(aheadBy: peeked), - byte != Byte.octothorp + byte != UInt8.octothorp { peeked += 1 - partial += byte + partial.append(byte) } - + return .literal(partial) } } @@ -243,16 +243,16 @@ extension Template { from: Date() ) } - + func padDigitLeft(_ digit: Int) -> String { return digit < 10 ? "0\(digit)" : "\(digit)" } - + func formatTime(hours: Int, minutes: Int, seconds: Int) -> String { let hours = padDigitLeft(hours) let minutes = padDigitLeft(minutes) let seconds = padDigitLeft(seconds) - + return "\(hours):\(minutes):\(seconds)" } } @@ -269,7 +269,7 @@ extension Template.Error: Equatable { (.mimeNotProvided, .mimeNotProvided), (.mimeFolderNotProvided, .mimeFolderNotProvided): return true - + default: return false } @@ -281,10 +281,10 @@ extension Template.PathPart: Equatable { switch (lhs, rhs) { case (.literal(let a), literal(let b)): return a == b - + case (.alias(let a), .alias(let b)): return a == b - + default: return false } diff --git a/Sources/Storage/Utilities/DataURI.swift b/Sources/Storage/Utilities/DataURI.swift new file mode 100644 index 0000000..53d7ecf --- /dev/null +++ b/Sources/Storage/Utilities/DataURI.swift @@ -0,0 +1,176 @@ +import Foundation + +/// A parser for decoding Data URIs. +public struct DataURIParser { + enum Error: Swift.Error { + case invalidScheme + case invalidURI + } + + var scanner: Scanner + + init(scanner: Scanner) { + self.scanner = scanner + } +} + +extension DataURIParser { + /** + Parses a Data URI and returns its type and data. + + - Parameters: + - uri: The URI to be parsed. + + - Returns: (data: [UInt8], type: [UInt8], typeMetadata: [UInt8]?) + */ + public static func parse(uri: String) throws -> (Data, [UInt8], [UInt8]?) { + guard uri.hasPrefix("data:") else { + throw Error.invalidScheme + } + + var scanner: Scanner = Scanner(uri.bytes) + //pop the bytes "data:" + scanner.pop(5) + + var parser = DataURIParser(scanner: scanner) + var (type, typeMetadata) = try parser.extractType() + var data = try parser.extractData() + + //Required by RFC 2397 + if type.isEmpty { + type = "text/plain;charset=US-ASCII".bytes + } + + if let typeMetadata = typeMetadata, typeMetadata == "base64".bytes { + data = Data(base64Encoded: data) ?? Data() + } + + return (data, type, typeMetadata) + } +} + +extension DataURIParser { + mutating func extractType() throws -> ([UInt8], [UInt8]?) { + let type = consume(until: [.comma, .semicolon]) + + guard let byte = scanner.peek() else { + throw Error.invalidURI + } + + var typeMetadata: [UInt8]? + + if byte == .semicolon { + typeMetadata = try extractTypeMetadata() + } + + return (type, typeMetadata) + } + + mutating func extractTypeMetadata() throws -> [UInt8] { + assert(scanner.peek() == .semicolon) + scanner.pop() + + return consume(until: [.comma]) + } + + mutating func extractData() throws -> Data { + assert(scanner.peek() == .comma) + scanner.pop() + return try Data(consumePercentDecoded()) + } +} + +extension DataURIParser { + @discardableResult + mutating func consume() -> [UInt8] { + var bytes: [UInt8] = [] + + while let byte = scanner.peek() { + scanner.pop() + bytes.append(byte) + } + + return bytes + } + + @discardableResult + mutating func consumePercentDecoded() throws -> [UInt8] { + var bytes: [UInt8] = [] + + while var byte = scanner.peek() { + if byte == .percent { + byte = try decodePercentEncoding() + } + + scanner.pop() + bytes.append(byte) + } + + return bytes + } + + @discardableResult + mutating func consume(until terminators: Set) -> [UInt8] { + var bytes: [UInt8] = [] + + while let byte = scanner.peek(), !terminators.contains(byte) { + scanner.pop() + bytes.append(byte) + } + + return bytes + } + + @discardableResult + mutating func consume(while conditional: (UInt8) -> Bool) -> [UInt8] { + var bytes: [UInt8] = [] + + while let byte = scanner.peek(), conditional(byte) { + scanner.pop() + bytes.append(byte) + } + + return bytes + } +} + +extension DataURIParser { + mutating func decodePercentEncoding() throws -> UInt8 { + assert(scanner.peek() == .percent) + + guard + let leftMostDigit = scanner.peek(aheadBy: 1), + let rightMostDigit = scanner.peek(aheadBy: 2) + else { + throw Error.invalidURI + } + + scanner.pop(2) + + return (leftMostDigit.asciiCode * 0x10) + rightMostDigit.asciiCode + } +} + +extension UInt8 { + internal var asciiCode: UInt8 { + if self >= 48 && self <= 57 { + return self - 48 + } else if self >= 65 && self <= 70 { + return self - 55 + } else { + return 0 + } + } +} + +extension String { + /** + Parses a Data URI and returns its data and type. + + - Returns: The type of the file and its data as bytes. + */ + public func dataURIDecoded() throws -> (data: Data, type: String) { + let (data, type, _) = try DataURIParser.parse(uri: self) + return (data, String(bytes: type, encoding: .utf8) ?? "") + } +} diff --git a/Sources/Storage/Utilities/Mime.swift b/Sources/Storage/Utilities/Mime.swift new file mode 100644 index 0000000..4ee8ea4 --- /dev/null +++ b/Sources/Storage/Utilities/Mime.swift @@ -0,0 +1,3084 @@ +/* + Generated using the following link and lots of love + http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types + */ + +func getExtension(for mime: String) -> String? { + switch mime { + case "application/andrew-inset": + return "ez" + case "application/applixware": + return "aw" + case "application/atom+xml": + return "atom" + case "application/atomcat+xml": + return "atomcat" + case "application/atomsvc+xml": + return "atomsvc" + case "application/ccxml+xml": + return "ccxml" + case "application/cdmi-capability": + return "cdmia" + case "application/cdmi-container": + return "cdmic" + case "application/cdmi-domain": + return "cdmid" + case "application/cdmi-object": + return "cdmio" + case "application/cdmi-queue": + return "cdmiq" + case "application/cu-seeme": + return "cu" + case "application/davmount+xml": + return "davmount" + case "application/docbook+xml": + return "dbk" + case "application/dssc+der": + return "dssc" + case "application/dssc+xml": + return "xdssc" + case "application/ecmascript": + return "ecma" + case "application/emma+xml": + return "emma" + case "application/epub+zip": + return "epub" + case "application/exi": + return "exi" + case "application/font-tdpfr": + return "pfr" + case "application/gml+xml": + return "gml" + case "application/gpx+xml": + return "gpx" + case "application/gxf": + return "gxf" + case "application/hyperstudio": + return "stk" + case "application/inkml+xml": + return "ink" + case "application/ipfix": + return "ipfix" + case "application/java-archive": + return "jar" + case "application/java-serialized-object": + return "ser" + case "application/java-vm": + return "class" + case "application/javascript": + return "js" + case "application/json": + return "json" + case "application/jsonml+json": + return "jsonml" + case "application/lost+xml": + return "lostxml" + case "application/mac-binhex40": + return "hqx" + case "application/mac-compactpro": + return "cpt" + case "application/mads+xml": + return "mads" + case "application/marc": + return "mrc" + case "application/marcxml+xml": + return "mrcx" + case "application/mathematica": + return "ma" + case "application/mathml+xml": + return "mathml" + case "application/mbox": + return "mbox" + case "application/mediaservercontrol+xml": + return "mscml" + case "application/metalink+xml": + return "metalink" + case "application/metalink4+xml": + return "meta4" + case "application/mets+xml": + return "mets" + case "application/mods+xml": + return "mods" + case "application/mp21": + return "m21" + case "application/mp4": + return "mp4s" + case "application/msword": + return "doc" + case "application/mxf": + return "mxf" + case "application/octet-stream": + return "bin" + case "application/oda": + return "oda" + case "application/oebps-package+xml": + return "opf" + case "application/ogg": + return "ogx" + case "application/omdoc+xml": + return "omdoc" + case "application/onenote": + return "onetoc" + case "application/oxps": + return "oxps" + case "application/patch-ops-error+xml": + return "xer" + case "application/pdf": + return "pdf" + case "application/pgp-encrypted": + return "pgp" + case "application/pgp-signature": + return "asc" + case "application/pics-rules": + return "prf" + case "application/pkcs10": + return "p10" + case "application/pkcs7-mime": + return "p7m" + case "application/pkcs7-signature": + return "p7s" + case "application/pkcs8": + return "p8" + case "application/pkix-attr-cert": + return "ac" + case "application/pkix-cert": + return "cer" + case "application/pkix-crl": + return "crl" + case "application/pkix-pkipath": + return "pkipath" + case "application/pkixcmp": + return "pki" + case "application/pls+xml": + return "pls" + case "application/postscript": + return "ai" + case "application/prs.cww": + return "cww" + case "application/pskc+xml": + return "pskcxml" + case "application/rdf+xml": + return "rdf" + case "application/reginfo+xml": + return "rif" + case "application/relax-ng-compact-syntax": + return "rnc" + case "application/resource-lists+xml": + return "rl" + case "application/resource-lists-diff+xml": + return "rld" + case "application/rls-services+xml": + return "rs" + case "application/rpki-ghostbusters": + return "gbr" + case "application/rpki-manifest": + return "mft" + case "application/rpki-roa": + return "roa" + case "application/rsd+xml": + return "rsd" + case "application/rss+xml": + return "rss" + case "application/rtf": + return "rtf" + case "application/sbml+xml": + return "sbml" + case "application/scvp-cv-request": + return "scq" + case "application/scvp-cv-response": + return "scs" + case "application/scvp-vp-request": + return "spq" + case "application/scvp-vp-response": + return "spp" + case "application/sdp": + return "sdp" + case "application/set-payment-initiation": + return "setpay" + case "application/set-registration-initiation": + return "setreg" + case "application/shf+xml": + return "shf" + case "application/smil+xml": + return "smi" + case "application/sparql-query": + return "rq" + case "application/sparql-results+xml": + return "srx" + case "application/srgs": + return "gram" + case "application/srgs+xml": + return "grxml" + case "application/sru+xml": + return "sru" + case "application/ssdl+xml": + return "ssdl" + case "application/ssml+xml": + return "ssml" + case "application/tei+xml": + return "tei" + case "application/thraud+xml": + return "tfi" + case "application/timestamped-data": + return "tsd" + case "application/vnd.3gpp.pic-bw-large": + return "plb" + case "application/vnd.3gpp.pic-bw-small": + return "psb" + case "application/vnd.3gpp.pic-bw-var": + return "pvb" + case "application/vnd.3gpp2.tcap": + return "tcap" + case "application/vnd.3m.post-it-notes": + return "pwn" + case "application/vnd.accpac.simply.aso": + return "aso" + case "application/vnd.accpac.simply.imp": + return "imp" + case "application/vnd.acucobol": + return "acu" + case "application/vnd.acucorp": + return "atc" + case "application/vnd.adobe.air-application-installer-package+zip": + return "air" + case "application/vnd.adobe.formscentral.fcdt": + return "fcdt" + case "application/vnd.adobe.fxp": + return "fxp" + case "application/vnd.adobe.xdp+xml": + return "xdp" + case "application/vnd.adobe.xfdf": + return "xfdf" + case "application/vnd.ahead.space": + return "ahead" + case "application/vnd.airzip.filesecure.azf": + return "azf" + case "application/vnd.airzip.filesecure.azs": + return "azs" + case "application/vnd.amazon.ebook": + return "azw" + case "application/vnd.americandynamics.acc": + return "acc" + case "application/vnd.amiga.ami": + return "ami" + case "application/vnd.android.package-archive": + return "apk" + case "application/vnd.anser-web-certificate-issue-initiation": + return "cii" + case "application/vnd.anser-web-funds-transfer-initiation": + return "fti" + case "application/vnd.antix.game-component": + return "atx" + case "application/vnd.apple.installer+xml": + return "mpkg" + case "application/vnd.apple.mpegurl": + return "m3u8" + case "application/vnd.aristanetworks.swi": + return "swi" + case "application/vnd.astraea-software.iota": + return "iota" + case "application/vnd.audiograph": + return "aep" + case "application/vnd.blueice.multipass": + return "mpm" + case "application/vnd.bmi": + return "bmi" + case "application/vnd.businessobjects": + return "rep" + case "application/vnd.chemdraw+xml": + return "cdxml" + case "application/vnd.chipnuts.karaoke-mmd": + return "mmd" + case "application/vnd.cinderella": + return "cdy" + case "application/vnd.claymore": + return "cla" + case "application/vnd.cloanto.rp9": + return "rp9" + case "application/vnd.clonk.c4group": + return "c4g" + case "application/vnd.cluetrust.cartomobile-config": + return "c11amc" + case "application/vnd.cluetrust.cartomobile-config-pkg": + return "c11amz" + case "application/vnd.commonspace": + return "csp" + case "application/vnd.contact.cmsg": + return "cdbcmsg" + case "application/vnd.cosmocaller": + return "cmc" + case "application/vnd.crick.clicker": + return "clkx" + case "application/vnd.crick.clicker.keyboard": + return "clkk" + case "application/vnd.crick.clicker.palette": + return "clkp" + case "application/vnd.crick.clicker.template": + return "clkt" + case "application/vnd.crick.clicker.wordbank": + return "clkw" + case "application/vnd.criticaltools.wbs+xml": + return "wbs" + case "application/vnd.ctc-posml": + return "pml" + case "application/vnd.cups-ppd": + return "ppd" + case "application/vnd.curl.car": + return "car" + case "application/vnd.curl.pcurl": + return "pcurl" + case "application/vnd.dart": + return "dart" + case "application/vnd.data-vision.rdz": + return "rdz" + case "application/vnd.dece.data": + return "uvf" + case "application/vnd.dece.ttml+xml": + return "uvt" + case "application/vnd.dece.unspecified": + return "uvx" + case "application/vnd.dece.zip": + return "uvz" + case "application/vnd.denovo.fcselayout-link": + return "fe_launch" + case "application/vnd.dna": + return "dna" + case "application/vnd.dolby.mlp": + return "mlp" + case "application/vnd.dpgraph": + return "dpg" + case "application/vnd.dreamfactory": + return "dfac" + case "application/vnd.ds-keypoint": + return "kpxx" + case "application/vnd.dvb.ait": + return "ait" + case "application/vnd.dvb.service": + return "svc" + case "application/vnd.dynageo": + return "geo" + case "application/vnd.ecowin.chart": + return "mag" + case "application/vnd.enliven": + return "nml" + case "application/vnd.epson.esf": + return "esf" + case "application/vnd.epson.msf": + return "msf" + case "application/vnd.epson.quickanime": + return "qam" + case "application/vnd.epson.salt": + return "slt" + case "application/vnd.epson.ssf": + return "ssf" + case "application/vnd.eszigno3+xml": + return "es3" + case "application/vnd.ezpix-album": + return "ez2" + case "application/vnd.ezpix-package": + return "ez3" + case "application/vnd.fdf": + return "fdf" + case "application/vnd.fdsn.mseed": + return "mseed" + case "application/vnd.fdsn.seed": + return "seed" + case "application/vnd.flographit": + return "gph" + case "application/vnd.fluxtime.clip": + return "ftc" + case "application/vnd.framemaker": + return "fm" + case "application/vnd.frogans.fnc": + return "fnc" + case "application/vnd.frogans.ltf": + return "ltf" + case "application/vnd.fsc.weblaunch": + return "fsc" + case "application/vnd.fujitsu.oasys": + return "oas" + case "application/vnd.fujitsu.oasys2": + return "oa2" + case "application/vnd.fujitsu.oasys3": + return "oa3" + case "application/vnd.fujitsu.oasysgp": + return "fg5" + case "application/vnd.fujitsu.oasysprs": + return "bh2" + case "application/vnd.fujixerox.ddd": + return "ddd" + case "application/vnd.fujixerox.docuworks": + return "xdw" + case "application/vnd.fujixerox.docuworks.binder": + return "xbd" + case "application/vnd.fuzzysheet": + return "fzs" + case "application/vnd.genomatix.tuxedo": + return "txd" + case "application/vnd.geogebra.file": + return "ggb" + case "application/vnd.geogebra.tool": + return "ggt" + case "application/vnd.geometry-explorer": + return "gex" + case "application/vnd.geonext": + return "gxt" + case "application/vnd.geoplan": + return "g2w" + case "application/vnd.geospace": + return "g3w" + case "application/vnd.gmx": + return "gmx" + case "application/vnd.google-earth.kml+xml": + return "kml" + case "application/vnd.google-earth.kmz": + return "kmz" + case "application/vnd.grafeq": + return "gqf" + case "application/vnd.groove-account": + return "gac" + case "application/vnd.groove-help": + return "ghf" + case "application/vnd.groove-identity-message": + return "gim" + case "application/vnd.groove-injector": + return "grv" + case "application/vnd.groove-tool-message": + return "gtm" + case "application/vnd.groove-tool-template": + return "tpl" + case "application/vnd.groove-vcard": + return "vcg" + case "application/vnd.hal+xml": + return "hal" + case "application/vnd.handheld-entertainment+xml": + return "zmm" + case "application/vnd.hbci": + return "hbci" + case "application/vnd.hhe.lesson-player": + return "les" + case "application/vnd.hp-hpgl": + return "hpgl" + case "application/vnd.hp-hpid": + return "hpid" + case "application/vnd.hp-hps": + return "hps" + case "application/vnd.hp-jlyt": + return "jlt" + case "application/vnd.hp-pcl": + return "pcl" + case "application/vnd.hp-pclxl": + return "pclxl" + case "application/vnd.hydrostatix.sof-data": + return "sfd-hdstx" + case "application/vnd.ibm.minipay": + return "mpy" + case "application/vnd.ibm.modcap": + return "afp" + case "application/vnd.ibm.rights-management": + return "irm" + case "application/vnd.ibm.secure-container": + return "sc" + case "application/vnd.iccprofile": + return "icc" + case "application/vnd.igloader": + return "igl" + case "application/vnd.immervision-ivp": + return "ivp" + case "application/vnd.immervision-ivu": + return "ivu" + case "application/vnd.insors.igm": + return "igm" + case "application/vnd.intercon.formnet": + return "xpw" + case "application/vnd.intergeo": + return "i2g" + case "application/vnd.intu.qbo": + return "qbo" + case "application/vnd.intu.qfx": + return "qfx" + case "application/vnd.ipunplugged.rcprofile": + return "rcprofile" + case "application/vnd.irepository.package+xml": + return "irp" + case "application/vnd.is-xpr": + return "xpr" + case "application/vnd.isac.fcs": + return "fcs" + case "application/vnd.jam": + return "jam" + case "application/vnd.jcp.javame.midlet-rms": + return "rms" + case "application/vnd.jisp": + return "jisp" + case "application/vnd.joost.joda-archive": + return "joda" + case "application/vnd.kahootz": + return "ktz" + case "application/vnd.kde.karbon": + return "karbon" + case "application/vnd.kde.kchart": + return "chrt" + case "application/vnd.kde.kformula": + return "kfo" + case "application/vnd.kde.kivio": + return "flw" + case "application/vnd.kde.kontour": + return "kon" + case "application/vnd.kde.kpresenter": + return "kpr" + case "application/vnd.kde.kspread": + return "ksp" + case "application/vnd.kde.kword": + return "kwd" + case "application/vnd.kenameaapp": + return "htke" + case "application/vnd.kidspiration": + return "kia" + case "application/vnd.kinar": + return "kne" + case "application/vnd.koan": + return "skp" + case "application/vnd.kodak-descriptor": + return "sse" + case "application/vnd.las.las+xml": + return "lasxml" + case "application/vnd.llamagraphics.life-balance.desktop": + return "lbd" + case "application/vnd.llamagraphics.life-balance.exchange+xml": + return "lbe" + case "application/vnd.lotus-1-2-3": + return "123" + case "application/vnd.lotus-approach": + return "apr" + case "application/vnd.lotus-freelance": + return "pre" + case "application/vnd.lotus-notes": + return "nsf" + case "application/vnd.lotus-organizer": + return "org" + case "application/vnd.lotus-screencam": + return "scm" + case "application/vnd.lotus-wordpro": + return "lwp" + case "application/vnd.macports.portpkg": + return "portpkg" + case "application/vnd.mcd": + return "mcd" + case "application/vnd.medcalcdata": + return "mc1" + case "application/vnd.mediastation.cdkey": + return "cdkey" + case "application/vnd.mfer": + return "mwf" + case "application/vnd.mfmp": + return "mfm" + case "application/vnd.micrografx.flo": + return "flo" + case "application/vnd.micrografx.igx": + return "igx" + case "application/vnd.mif": + return "mif" + case "application/vnd.mobius.daf": + return "daf" + case "application/vnd.mobius.dis": + return "dis" + case "application/vnd.mobius.mbk": + return "mbk" + case "application/vnd.mobius.mqy": + return "mqy" + case "application/vnd.mobius.msl": + return "msl" + case "application/vnd.mobius.plc": + return "plc" + case "application/vnd.mobius.txf": + return "txf" + case "application/vnd.mophun.application": + return "mpn" + case "application/vnd.mophun.certificate": + return "mpc" + case "application/vnd.mozilla.xul+xml": + return "xul" + case "application/vnd.ms-artgalry": + return "cil" + case "application/vnd.ms-cab-compressed": + return "cab" + case "application/vnd.ms-excel": + return "xls" + case "application/vnd.ms-excel.addin.macroenabled.12": + return "xlam" + case "application/vnd.ms-excel.sheet.binary.macroenabled.12": + return "xlsb" + case "application/vnd.ms-excel.sheet.macroenabled.12": + return "xlsm" + case "application/vnd.ms-excel.template.macroenabled.12": + return "xltm" + case "application/vnd.ms-fontobject": + return "eot" + case "application/vnd.ms-htmlhelp": + return "chm" + case "application/vnd.ms-ims": + return "ims" + case "application/vnd.ms-lrm": + return "lrm" + case "application/vnd.ms-officetheme": + return "thmx" + case "application/vnd.ms-pki.seccat": + return "cat" + case "application/vnd.ms-pki.stl": + return "stl" + case "application/vnd.ms-powerpoint": + return "ppt" + case "application/vnd.ms-powerpoint.addin.macroenabled.12": + return "ppam" + case "application/vnd.ms-powerpoint.presentation.macroenabled.12": + return "pptm" + case "application/vnd.ms-powerpoint.slide.macroenabled.12": + return "sldm" + case "application/vnd.ms-powerpoint.slideshow.macroenabled.12": + return "ppsm" + case "application/vnd.ms-powerpoint.template.macroenabled.12": + return "potm" + case "application/vnd.ms-project": + return "mpp" + case "application/vnd.ms-word.document.macroenabled.12": + return "docm" + case "application/vnd.ms-word.template.macroenabled.12": + return "dotm" + case "application/vnd.ms-works": + return "wps" + case "application/vnd.ms-wpl": + return "wpl" + case "application/vnd.ms-xpsdocument": + return "xps" + case "application/vnd.mseq": + return "mseq" + case "application/vnd.musician": + return "mus" + case "application/vnd.muvee.style": + return "msty" + case "application/vnd.mynfc": + return "taglet" + case "application/vnd.neurolanguage.nlu": + return "nlu" + case "application/vnd.nitf": + return "ntf" + case "application/vnd.noblenet-directory": + return "nnd" + case "application/vnd.noblenet-sealer": + return "nns" + case "application/vnd.noblenet-web": + return "nnw" + case "application/vnd.nokia.n-gage.data": + return "ngdat" + case "application/vnd.nokia.n-gage.symbian.install": + return "n-gage" + case "application/vnd.nokia.radio-preset": + return "rpst" + case "application/vnd.nokia.radio-presets": + return "rpss" + case "application/vnd.novadigm.edm": + return "edm" + case "application/vnd.novadigm.edx": + return "edx" + case "application/vnd.novadigm.ext": + return "ext" + case "application/vnd.oasis.opendocument.chart": + return "odc" + case "application/vnd.oasis.opendocument.chart-template": + return "otc" + case "application/vnd.oasis.opendocument.database": + return "odb" + case "application/vnd.oasis.opendocument.formula": + return "odf" + case "application/vnd.oasis.opendocument.formula-template": + return "odft" + case "application/vnd.oasis.opendocument.graphics": + return "odg" + case "application/vnd.oasis.opendocument.graphics-template": + return "otg" + case "application/vnd.oasis.opendocument.image": + return "odi" + case "application/vnd.oasis.opendocument.image-template": + return "oti" + case "application/vnd.oasis.opendocument.presentation": + return "odp" + case "application/vnd.oasis.opendocument.presentation-template": + return "otp" + case "application/vnd.oasis.opendocument.spreadsheet": + return "ods" + case "application/vnd.oasis.opendocument.spreadsheet-template": + return "ots" + case "application/vnd.oasis.opendocument.text": + return "odt" + case "application/vnd.oasis.opendocument.text-master": + return "odm" + case "application/vnd.oasis.opendocument.text-template": + return "ott" + case "application/vnd.oasis.opendocument.text-web": + return "oth" + case "application/vnd.olpc-sugar": + return "xo" + case "application/vnd.oma.dd2+xml": + return "dd2" + case "application/vnd.openofficeorg.extension": + return "oxt" + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + return "pptx" + case "application/vnd.openxmlformats-officedocument.presentationml.slide": + return "sldx" + case "application/vnd.openxmlformats-officedocument.presentationml.slideshow": + return "ppsx" + case "application/vnd.openxmlformats-officedocument.presentationml.template": + return "potx" + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + return "xlsx" + case "application/vnd.openxmlformats-officedocument.spreadsheetml.template": + return "xltx" + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + return "docx" + case "application/vnd.openxmlformats-officedocument.wordprocessingml.template": + return "dotx" + case "application/vnd.osgeo.mapguide.package": + return "mgp" + case "application/vnd.osgi.dp": + return "dp" + case "application/vnd.osgi.subsystem": + return "esa" + case "application/vnd.palm": + return "pdb" + case "application/vnd.pawaafile": + return "paw" + case "application/vnd.pg.format": + return "str" + case "application/vnd.pg.osasli": + return "ei6" + case "application/vnd.picsel": + return "efif" + case "application/vnd.pmi.widget": + return "wg" + case "application/vnd.pocketlearn": + return "plf" + case "application/vnd.powerbuilder6": + return "pbd" + case "application/vnd.previewsystems.box": + return "box" + case "application/vnd.proteus.magazine": + return "mgz" + case "application/vnd.publishare-delta-tree": + return "qps" + case "application/vnd.pvi.ptid1": + return "ptid" + case "application/vnd.quark.quarkxpress": + return "qxd" + case "application/vnd.realvnc.bed": + return "bed" + case "application/vnd.recordare.musicxml": + return "mxl" + case "application/vnd.recordare.musicxml+xml": + return "musicxml" + case "application/vnd.rig.cryptonote": + return "cryptonote" + case "application/vnd.rim.cod": + return "cod" + case "application/vnd.rn-realmedia": + return "rm" + case "application/vnd.rn-realmedia-vbr": + return "rmvb" + case "application/vnd.route66.link66+xml": + return "link66" + case "application/vnd.sailingtracker.track": + return "st" + case "application/vnd.seemail": + return "see" + case "application/vnd.sema": + return "sema" + case "application/vnd.semd": + return "semd" + case "application/vnd.semf": + return "semf" + case "application/vnd.shana.informed.formdata": + return "ifm" + case "application/vnd.shana.informed.formtemplate": + return "itp" + case "application/vnd.shana.informed.interchange": + return "iif" + case "application/vnd.shana.informed.package": + return "ipk" + case "application/vnd.simtech-mindmapper": + return "twd" + case "application/vnd.smaf": + return "mmf" + case "application/vnd.smart.teacher": + return "teacher" + case "application/vnd.solent.sdkm+xml": + return "sdkm" + case "application/vnd.spotfire.dxp": + return "dxp" + case "application/vnd.spotfire.sfs": + return "sfs" + case "application/vnd.stardivision.calc": + return "sdc" + case "application/vnd.stardivision.draw": + return "sda" + case "application/vnd.stardivision.impress": + return "sdd" + case "application/vnd.stardivision.math": + return "smf" + case "application/vnd.stardivision.writer": + return "sdw" + case "application/vnd.stardivision.writer-global": + return "sgl" + case "application/vnd.stepmania.package": + return "smzip" + case "application/vnd.stepmania.stepchart": + return "sm" + case "application/vnd.sun.xml.calc": + return "sxc" + case "application/vnd.sun.xml.calc.template": + return "stc" + case "application/vnd.sun.xml.draw": + return "sxd" + case "application/vnd.sun.xml.draw.template": + return "std" + case "application/vnd.sun.xml.impress": + return "sxi" + case "application/vnd.sun.xml.impress.template": + return "sti" + case "application/vnd.sun.xml.math": + return "sxm" + case "application/vnd.sun.xml.writer": + return "sxw" + case "application/vnd.sun.xml.writer.global": + return "sxg" + case "application/vnd.sun.xml.writer.template": + return "stw" + case "application/vnd.sus-calendar": + return "sus" + case "application/vnd.svd": + return "svd" + case "application/vnd.symbian.install": + return "sis" + case "application/vnd.syncml+xml": + return "xsm" + case "application/vnd.syncml.dm+wbxml": + return "bdm" + case "application/vnd.syncml.dm+xml": + return "xdm" + case "application/vnd.tao.intent-module-archive": + return "tao" + case "application/vnd.tcpdump.pcap": + return "pcap" + case "application/vnd.tmobile-livetv": + return "tmo" + case "application/vnd.trid.tpt": + return "tpt" + case "application/vnd.triscape.mxs": + return "mxs" + case "application/vnd.trueapp": + return "tra" + case "application/vnd.ufdl": + return "ufd" + case "application/vnd.uiq.theme": + return "utz" + case "application/vnd.umajin": + return "umj" + case "application/vnd.unity": + return "unityweb" + case "application/vnd.uoml+xml": + return "uoml" + case "application/vnd.vcx": + return "vcx" + case "application/vnd.visio": + return "vsd" + case "application/vnd.visionary": + return "vis" + case "application/vnd.vsf": + return "vsf" + case "application/vnd.wap.wbxml": + return "wbxml" + case "application/vnd.wap.wmlc": + return "wmlc" + case "application/vnd.wap.wmlscriptc": + return "wmlsc" + case "application/vnd.webturbo": + return "wtb" + case "application/vnd.wolfram.player": + return "nbp" + case "application/vnd.wordperfect": + return "wpd" + case "application/vnd.wqd": + return "wqd" + case "application/vnd.wt.stf": + return "stf" + case "application/vnd.xara": + return "xar" + case "application/vnd.xfdl": + return "xfdl" + case "application/vnd.yamaha.hv-dic": + return "hvd" + case "application/vnd.yamaha.hv-script": + return "hvs" + case "application/vnd.yamaha.hv-voice": + return "hvp" + case "application/vnd.yamaha.openscoreformat": + return "osf" + case "application/vnd.yamaha.openscoreformat.osfpvg+xml": + return "osfpvg" + case "application/vnd.yamaha.smaf-audio": + return "saf" + case "application/vnd.yamaha.smaf-phrase": + return "spf" + case "application/vnd.yellowriver-custom-menu": + return "cmp" + case "application/vnd.zul": + return "zir" + case "application/vnd.zzazz.deck+xml": + return "zaz" + case "application/voicexml+xml": + return "vxml" + case "application/widget": + return "wgt" + case "application/winhlp": + return "hlp" + case "application/wsdl+xml": + return "wsdl" + case "application/wspolicy+xml": + return "wspolicy" + case "application/x-7z-compressed": + return "7z" + case "application/x-abiword": + return "abw" + case "application/x-ace-compressed": + return "ace" + case "application/x-apple-diskimage": + return "dmg" + case "application/x-authorware-bin": + return "aab" + case "application/x-authorware-map": + return "aam" + case "application/x-authorware-seg": + return "aas" + case "application/x-bcpio": + return "bcpio" + case "application/x-bittorrent": + return "torrent" + case "application/x-blorb": + return "blb" + case "application/x-bzip": + return "bz" + case "application/x-bzip2": + return "bz2" + case "application/x-cbr": + return "cbr" + case "application/x-cdlink": + return "vcd" + case "application/x-cfs-compressed": + return "cfs" + case "application/x-chat": + return "chat" + case "application/x-chess-pgn": + return "pgn" + case "application/x-conference": + return "nsc" + case "application/x-cpio": + return "cpio" + case "application/x-csh": + return "csh" + case "application/x-debian-package": + return "deb" + case "application/x-dgc-compressed": + return "dgc" + case "application/x-director": + return "dir" + case "application/x-doom": + return "wad" + case "application/x-dtbncx+xml": + return "ncx" + case "application/x-dtbook+xml": + return "dtb" + case "application/x-dtbresource+xml": + return "res" + case "application/x-dvi": + return "dvi" + case "application/x-envoy": + return "evy" + case "application/x-eva": + return "eva" + case "application/x-font-bdf": + return "bdf" + case "application/x-font-ghostscript": + return "gsf" + case "application/x-font-linux-psf": + return "psf" + case "application/x-font-pcf": + return "pcf" + case "application/x-font-snf": + return "snf" + case "application/x-font-type1": + return "pfa" + case "application/x-freearc": + return "arc" + case "application/x-futuresplash": + return "spl" + case "application/x-gca-compressed": + return "gca" + case "application/x-glulx": + return "ulx" + case "application/x-gnumeric": + return "gnumeric" + case "application/x-gramps-xml": + return "gramps" + case "application/x-gtar": + return "gtar" + case "application/x-hdf": + return "hdf" + case "application/x-install-instructions": + return "install" + case "application/x-iso9660-image": + return "iso" + case "application/x-java-jnlp-file": + return "jnlp" + case "application/x-latex": + return "latex" + case "application/x-lzh-compressed": + return "lzh" + case "application/x-mie": + return "mie" + case "application/x-mobipocket-ebook": + return "prc" + case "application/x-ms-application": + return "application" + case "application/x-ms-shortcut": + return "lnk" + case "application/x-ms-wmd": + return "wmd" + case "application/x-ms-wmz": + return "wmz" + case "application/x-ms-xbap": + return "xbap" + case "application/x-msaccess": + return "mdb" + case "application/x-msbinder": + return "obd" + case "application/x-mscardfile": + return "crd" + case "application/x-msclip": + return "clp" + case "application/x-msdownload": + return "exe" + case "application/x-msmediaview": + return "mvb" + case "application/x-msmetafile": + return "wmf" + case "application/x-msmoney": + return "mny" + case "application/x-mspublisher": + return "pub" + case "application/x-msschedule": + return "scd" + case "application/x-msterminal": + return "trm" + case "application/x-mswrite": + return "wri" + case "application/x-netcdf": + return "nc" + case "application/x-nzb": + return "nzb" + case "application/x-pkcs12": + return "p12" + case "application/x-pkcs7-certificates": + return "p7b" + case "application/x-pkcs7-certreqresp": + return "p7r" + case "application/x-rar-compressed": + return "rar" + case "application/x-research-info-systems": + return "ris" + case "application/x-sh": + return "sh" + case "application/x-shar": + return "shar" + case "application/x-shockwave-flash": + return "swf" + case "application/x-silverlight-app": + return "xap" + case "application/x-sql": + return "sql" + case "application/x-stuffit": + return "sit" + case "application/x-stuffitx": + return "sitx" + case "application/x-subrip": + return "srt" + case "application/x-sv4cpio": + return "sv4cpio" + case "application/x-sv4crc": + return "sv4crc" + case "application/x-t3vm-image": + return "t3" + case "application/x-tads": + return "gam" + case "application/x-tar": + return "tar" + case "application/x-tcl": + return "tcl" + case "application/x-tex": + return "tex" + case "application/x-tex-tfm": + return "tfm" + case "application/x-texinfo": + return "texinfo" + case "application/x-tgif": + return "obj" + case "application/x-ustar": + return "ustar" + case "application/x-wais-source": + return "src" + case "application/x-x509-ca-cert": + return "der" + case "application/x-xfig": + return "fig" + case "application/x-xliff+xml": + return "xlf" + case "application/x-xpinstall": + return "xpi" + case "application/x-xz": + return "xz" + case "application/x-zmachine": + return "z1" + case "application/xaml+xml": + return "xaml" + case "application/xcap-diff+xml": + return "xdf" + case "application/xenc+xml": + return "xenc" + case "application/xhtml+xml": + return "xhtml" + case "application/xml": + return "xml" + case "application/xml-dtd": + return "dtd" + case "application/xop+xml": + return "xop" + case "application/xproc+xml": + return "xpl" + case "application/xslt+xml": + return "xslt" + case "application/xspf+xml": + return "xspf" + case "application/xv+xml": + return "mxml" + case "application/yang": + return "yang" + case "application/yin+xml": + return "yin" + case "application/zip": + return "zip" + case "audio/adpcm": + return "adp" + case "audio/basic": + return "au" + case "audio/midi": + return "mid" + case "audio/mp4": + return "m4a" + case "audio/mpeg": + return "mpga" + case "audio/ogg": + return "ogg" + case "audio/s3m": + return "s3m" + case "audio/silk": + return "sil" + case "audio/vnd.dece.audio": + return "uvva" + case "audio/vnd.digital-winds": + return "eol" + case "audio/vnd.dra": + return "dra" + case "audio/vnd.dts": + return "dts" + case "audio/vnd.dts.hd": + return "dtshd" + case "audio/vnd.lucent.voice": + return "lvp" + case "audio/vnd.ms-playready.media.pya": + return "pya" + case "audio/vnd.nuera.ecelp4800": + return "ecelp4800" + case "audio/vnd.nuera.ecelp7470": + return "ecelp7470" + case "audio/vnd.nuera.ecelp9600": + return "ecelp9600" + case "audio/vnd.rip": + return "rip" + case "audio/webm": + return "weba" + case "audio/x-aac": + return "aac" + case "audio/x-aiff": + return "aif" + case "audio/x-caf": + return "caf" + case "audio/x-flac": + return "flac" + case "audio/x-matroska": + return "mka" + case "audio/x-mpegurl": + return "m3u" + case "audio/x-ms-wax": + return "wax" + case "audio/x-ms-wma": + return "wma" + case "audio/x-pn-realaudio": + return "ram" + case "audio/x-pn-realaudio-plugin": + return "rmp" + case "audio/x-wav": + return "wav" + case "audio/xm": + return "xm" + case "chemical/x-cdx": + return "cdx" + case "chemical/x-cif": + return "cif" + case "chemical/x-cmdf": + return "cmdf" + case "chemical/x-cml": + return "cml" + case "chemical/x-csml": + return "csml" + case "chemical/x-xyz": + return "xyz" + case "font/collection": + return "ttc" + case "font/otf": + return "otf" + case "font/ttf": + return "ttf" + case "font/woff": + return "woff" + case "font/woff2": + return "woff2" + case "image/bmp": + return "bmp" + case "image/cgm": + return "cgm" + case "image/g3fax": + return "g3" + case "image/gif": + return "gif" + case "image/ief": + return "ief" + case "image/jpeg": + return "jpeg" + case "image/ktx": + return "ktx" + case "image/png": + return "png" + case "image/prs.btif": + return "btif" + case "image/sgi": + return "sgi" + case "image/svg+xml": + return "svg" + case "image/tiff": + return "tiff" + case "image/vnd.adobe.photoshop": + return "psd" + case "image/vnd.dece.graphic": + return "uvi" + case "image/vnd.djvu": + return "djvu" + case "image/vnd.dvb.subtitle": + return "sub" + case "image/vnd.dwg": + return "dwg" + case "image/vnd.dxf": + return "dxf" + case "image/vnd.fastbidsheet": + return "fbs" + case "image/vnd.fpx": + return "fpx" + case "image/vnd.fst": + return "fst" + case "image/vnd.fujixerox.edmics-mmr": + return "mmr" + case "image/vnd.fujixerox.edmics-rlc": + return "rlc" + case "image/vnd.ms-modi": + return "mdi" + case "image/vnd.ms-photo": + return "wdp" + case "image/vnd.net-fpx": + return "npx" + case "image/vnd.wap.wbmp": + return "wbmp" + case "image/vnd.xiff": + return "xif" + case "image/webp": + return "webp" + case "image/x-3ds": + return "3ds" + case "image/x-cmu-raster": + return "ras" + case "image/x-cmx": + return "cmx" + case "image/x-freehand": + return "fh" + case "image/x-icon": + return "ico" + case "image/x-mrsid-image": + return "sid" + case "image/x-pcx": + return "pcx" + case "image/x-pict": + return "pic" + case "image/x-portable-anymap": + return "pnm" + case "image/x-portable-bitmap": + return "pbm" + case "image/x-portable-graymap": + return "pgm" + case "image/x-portable-pixmap": + return "ppm" + case "image/x-rgb": + return "rgb" + case "image/x-tga": + return "tga" + case "image/x-xbitmap": + return "xbm" + case "image/x-xpixmap": + return "xpm" + case "image/x-xwindowdump": + return "xwd" + case "message/rfc822": + return "eml" + case "model/iges": + return "igs" + case "model/mesh": + return "msh" + case "model/vnd.collada+xml": + return "dae" + case "model/vnd.dwf": + return "dwf" + case "model/vnd.gdl": + return "gdl" + case "model/vnd.gtw": + return "gtw" + case "model/vnd.mts": + return "mts" + case "model/vnd.vtu": + return "vtu" + case "model/vrml": + return "wrl" + case "model/x3d+binary": + return "x3db" + case "model/x3d+vrml": + return "x3dv" + case "model/x3d+xml": + return "x3d" + case "text/cache-manifest": + return "appcache" + case "text/calendar": + return "ics" + case "text/css": + return "css" + case "text/csv": + return "csv" + case "text/html": + return "html" + case "text/n3": + return "n3" + case "text/plain": + return "txt" + case "text/prs.lines.tag": + return "dsc" + case "text/richtext": + return "rtx" + case "text/sgml": + return "sgml" + case "text/tab-separated-values": + return "tsv" + case "text/troff": + return "t" + case "text/turtle": + return "ttl" + case "text/uri-list": + return "uri" + case "text/vcard": + return "vcard" + case "text/vnd.curl": + return "curl" + case "text/vnd.curl.dcurl": + return "dcurl" + case "text/vnd.curl.mcurl": + return "mcurl" + case "text/vnd.curl.scurl": + return "scurl" + case "text/vnd.dvb.subtitle": + return "sub" + case "text/vnd.fly": + return "fly" + case "text/vnd.fmi.flexstor": + return "flx" + case "text/vnd.graphviz": + return "gv" + case "text/vnd.in3d.3dml": + return "3dml" + case "text/vnd.in3d.spot": + return "spot" + case "text/vnd.sun.j2me.app-descriptor": + return "jad" + case "text/vnd.wap.wml": + return "wml" + case "text/vnd.wap.wmlscript": + return "wmls" + case "text/x-asm": + return "s" + case "text/x-c": + return "c" + case "text/x-fortran": + return "f" + case "text/x-java-source": + return "java" + case "text/x-nfo": + return "nfo" + case "text/x-opml": + return "opml" + case "text/x-pascal": + return "p" + case "text/x-setext": + return "etx" + case "text/x-sfv": + return "sfv" + case "text/x-uuencode": + return "uu" + case "text/x-vcalendar": + return "vcs" + case "text/x-vcard": + return "vcf" + case "video/3gpp": + return "3gp" + case "video/3gpp2": + return "3g2" + case "video/h261": + return "h261" + case "video/h263": + return "h263" + case "video/h264": + return "h264" + case "video/jpeg": + return "jpgv" + case "video/jpm": + return "jpm" + case "video/mj2": + return "mj2" + case "video/mp4": + return "mp4" + case "video/mpeg": + return "mpeg" + case "video/ogg": + return "ogv" + case "video/quicktime": + return "mov" + case "video/vnd.dece.hd": + return "uvh" + case "video/vnd.dece.mobile": + return "uvm" + case "video/vnd.dece.pd": + return "uvp" + case "video/vnd.dece.sd": + return "uvs" + case "video/vnd.dece.video": + return "uvv" + case "video/vnd.dvb.file": + return "dvb" + case "video/vnd.fvt": + return "fvt" + case "video/vnd.mpegurl": + return "mxu" + case "video/vnd.ms-playready.media.pyv": + return "pyv" + case "video/vnd.uvvu.mp4": + return "uvu" + case "video/vnd.vivo": + return "viv" + case "video/webm": + return "webm" + case "video/x-f4v": + return "f4v" + case "video/x-fli": + return "fli" + case "video/x-flv": + return "flv" + case "video/x-m4v": + return "m4v" + case "video/x-matroska": + return "mkv" + case "video/x-mng": + return "mng" + case "video/x-ms-asf": + return "asf" + case "video/x-ms-vob": + return "vob" + case "video/x-ms-wm": + return "wm" + case "video/x-ms-wmv": + return "wmv" + case "video/x-ms-wmx": + return "wmx" + case "video/x-ms-wvx": + return "wvx" + case "video/x-msvideo": + return "avi" + case "video/x-sgi-movie": + return "movie" + case "video/x-smv": + return "smv" + case "x-conference/x-cooltalk": + return "ice" + default: + return nil + } +} + +func getMime(for extension: String) -> String? { + switch `extension` { + case "ez": + return "application/andrew-inset" + case "aw": + return "application/applixware" + case "atom": + return "application/atom+xml" + case "atomcat": + return "application/atomcat+xml" + case "atomsvc": + return "application/atomsvc+xml" + case "ccxml": + return "application/ccxml+xml" + case "cdmia": + return "application/cdmi-capability" + case "cdmic": + return "application/cdmi-container" + case "cdmid": + return "application/cdmi-domain" + case "cdmio": + return "application/cdmi-object" + case "cdmiq": + return "application/cdmi-queue" + case "cu": + return "application/cu-seeme" + case "davmount": + return "application/davmount+xml" + case "dbk": + return "application/docbook+xml" + case "dssc": + return "application/dssc+der" + case "xdssc": + return "application/dssc+xml" + case "ecma": + return "application/ecmascript" + case "emma": + return "application/emma+xml" + case "epub": + return "application/epub+zip" + case "exi": + return "application/exi" + case "pfr": + return "application/font-tdpfr" + case "gml": + return "application/gml+xml" + case "gpx": + return "application/gpx+xml" + case "gxf": + return "application/gxf" + case "stk": + return "application/hyperstudio" + case "ink", "inkml": + return "application/inkml+xml" + case "ipfix": + return "application/ipfix" + case "jar": + return "application/java-archive" + case "ser": + return "application/java-serialized-object" + case "class": + return "application/java-vm" + case "js": + return "application/javascript" + case "json": + return "application/json" + case "jsonml": + return "application/jsonml+json" + case "lostxml": + return "application/lost+xml" + case "hqx": + return "application/mac-binhex40" + case "cpt": + return "application/mac-compactpro" + case "mads": + return "application/mads+xml" + case "mrc": + return "application/marc" + case "mrcx": + return "application/marcxml+xml" + case "ma", "nb", "mb": + return "application/mathematica" + case "mathml": + return "application/mathml+xml" + case "mbox": + return "application/mbox" + case "mscml": + return "application/mediaservercontrol+xml" + case "metalink": + return "application/metalink+xml" + case "meta4": + return "application/metalink4+xml" + case "mets": + return "application/mets+xml" + case "mods": + return "application/mods+xml" + case "m21", "mp21": + return "application/mp21" + case "mp4s": + return "application/mp4" + case "doc", "dot": + return "application/msword" + case "mxf": + return "application/mxf" + case "bin", "dms", "lrf", "mar", "so", "dist", "distz", "pkg", "bpk", "dump", "elc", "deploy": + return "application/octet-stream" + case "oda": + return "application/oda" + case "opf": + return "application/oebps-package+xml" + case "ogx": + return "application/ogg" + case "omdoc": + return "application/omdoc+xml" + case "onetoc", "onetoc2", "onetmp", "onepkg": + return "application/onenote" + case "oxps": + return "application/oxps" + case "xer": + return "application/patch-ops-error+xml" + case "pdf": + return "application/pdf" + case "pgp": + return "application/pgp-encrypted" + case "asc", "sig": + return "application/pgp-signature" + case "prf": + return "application/pics-rules" + case "p10": + return "application/pkcs10" + case "p7m", "p7c": + return "application/pkcs7-mime" + case "p7s": + return "application/pkcs7-signature" + case "p8": + return "application/pkcs8" + case "ac": + return "application/pkix-attr-cert" + case "cer": + return "application/pkix-cert" + case "crl": + return "application/pkix-crl" + case "pkipath": + return "application/pkix-pkipath" + case "pki": + return "application/pkixcmp" + case "pls": + return "application/pls+xml" + case "ai", "eps", "ps": + return "application/postscript" + case "cww": + return "application/prs.cww" + case "pskcxml": + return "application/pskc+xml" + case "rdf": + return "application/rdf+xml" + case "rif": + return "application/reginfo+xml" + case "rnc": + return "application/relax-ng-compact-syntax" + case "rl": + return "application/resource-lists+xml" + case "rld": + return "application/resource-lists-diff+xml" + case "rs": + return "application/rls-services+xml" + case "gbr": + return "application/rpki-ghostbusters" + case "mft": + return "application/rpki-manifest" + case "roa": + return "application/rpki-roa" + case "rsd": + return "application/rsd+xml" + case "rss": + return "application/rss+xml" + case "rtf": + return "application/rtf" + case "sbml": + return "application/sbml+xml" + case "scq": + return "application/scvp-cv-request" + case "scs": + return "application/scvp-cv-response" + case "spq": + return "application/scvp-vp-request" + case "spp": + return "application/scvp-vp-response" + case "sdp": + return "application/sdp" + case "setpay": + return "application/set-payment-initiation" + case "setreg": + return "application/set-registration-initiation" + case "shf": + return "application/shf+xml" + case "smi", "smil": + return "application/smil+xml" + case "rq": + return "application/sparql-query" + case "srx": + return "application/sparql-results+xml" + case "gram": + return "application/srgs" + case "grxml": + return "application/srgs+xml" + case "sru": + return "application/sru+xml" + case "ssdl": + return "application/ssdl+xml" + case "ssml": + return "application/ssml+xml" + case "tei", "teicorpus": + return "application/tei+xml" + case "tfi": + return "application/thraud+xml" + case "tsd": + return "application/timestamped-data" + case "plb": + return "application/vnd.3gpp.pic-bw-large" + case "psb": + return "application/vnd.3gpp.pic-bw-small" + case "pvb": + return "application/vnd.3gpp.pic-bw-var" + case "tcap": + return "application/vnd.3gpp2.tcap" + case "pwn": + return "application/vnd.3m.post-it-notes" + case "aso": + return "application/vnd.accpac.simply.aso" + case "imp": + return "application/vnd.accpac.simply.imp" + case "acu": + return "application/vnd.acucobol" + case "atc", "acutc": + return "application/vnd.acucorp" + case "air": + return "application/vnd.adobe.air-application-installer-package+zip" + case "fcdt": + return "application/vnd.adobe.formscentral.fcdt" + case "fxp", "fxpl": + return "application/vnd.adobe.fxp" + case "xdp": + return "application/vnd.adobe.xdp+xml" + case "xfdf": + return "application/vnd.adobe.xfdf" + case "ahead": + return "application/vnd.ahead.space" + case "azf": + return "application/vnd.airzip.filesecure.azf" + case "azs": + return "application/vnd.airzip.filesecure.azs" + case "azw": + return "application/vnd.amazon.ebook" + case "acc": + return "application/vnd.americandynamics.acc" + case "ami": + return "application/vnd.amiga.ami" + case "apk": + return "application/vnd.android.package-archive" + case "cii": + return "application/vnd.anser-web-certificate-issue-initiation" + case "fti": + return "application/vnd.anser-web-funds-transfer-initiation" + case "atx": + return "application/vnd.antix.game-component" + case "mpkg": + return "application/vnd.apple.installer+xml" + case "m3u8": + return "application/vnd.apple.mpegurl" + case "swi": + return "application/vnd.aristanetworks.swi" + case "iota": + return "application/vnd.astraea-software.iota" + case "aep": + return "application/vnd.audiograph" + case "mpm": + return "application/vnd.blueice.multipass" + case "bmi": + return "application/vnd.bmi" + case "rep": + return "application/vnd.businessobjects" + case "cdxml": + return "application/vnd.chemdraw+xml" + case "mmd": + return "application/vnd.chipnuts.karaoke-mmd" + case "cdy": + return "application/vnd.cinderella" + case "cla": + return "application/vnd.claymore" + case "rp9": + return "application/vnd.cloanto.rp9" + case "c4g", "c4d", "c4f", "c4p", "c4u": + return "application/vnd.clonk.c4group" + case "c11amc": + return "application/vnd.cluetrust.cartomobile-config" + case "c11amz": + return "application/vnd.cluetrust.cartomobile-config-pkg" + case "csp": + return "application/vnd.commonspace" + case "cdbcmsg": + return "application/vnd.contact.cmsg" + case "cmc": + return "application/vnd.cosmocaller" + case "clkx": + return "application/vnd.crick.clicker" + case "clkk": + return "application/vnd.crick.clicker.keyboard" + case "clkp": + return "application/vnd.crick.clicker.palette" + case "clkt": + return "application/vnd.crick.clicker.template" + case "clkw": + return "application/vnd.crick.clicker.wordbank" + case "wbs": + return "application/vnd.criticaltools.wbs+xml" + case "pml": + return "application/vnd.ctc-posml" + case "ppd": + return "application/vnd.cups-ppd" + case "car": + return "application/vnd.curl.car" + case "pcurl": + return "application/vnd.curl.pcurl" + case "dart": + return "application/vnd.dart" + case "rdz": + return "application/vnd.data-vision.rdz" + case "uvf", "uvvf", "uvd", "uvvd": + return "application/vnd.dece.data" + case "uvt", "uvvt": + return "application/vnd.dece.ttml+xml" + case "uvx", "uvvx": + return "application/vnd.dece.unspecified" + case "uvz", "uvvz": + return "application/vnd.dece.zip" + case "fe_launch": + return "application/vnd.denovo.fcselayout-link" + case "dna": + return "application/vnd.dna" + case "mlp": + return "application/vnd.dolby.mlp" + case "dpg": + return "application/vnd.dpgraph" + case "dfac": + return "application/vnd.dreamfactory" + case "kpxx": + return "application/vnd.ds-keypoint" + case "ait": + return "application/vnd.dvb.ait" + case "svc": + return "application/vnd.dvb.service" + case "geo": + return "application/vnd.dynageo" + case "mag": + return "application/vnd.ecowin.chart" + case "nml": + return "application/vnd.enliven" + case "esf": + return "application/vnd.epson.esf" + case "msf": + return "application/vnd.epson.msf" + case "qam": + return "application/vnd.epson.quickanime" + case "slt": + return "application/vnd.epson.salt" + case "ssf": + return "application/vnd.epson.ssf" + case "es3", "et3": + return "application/vnd.eszigno3+xml" + case "ez2": + return "application/vnd.ezpix-album" + case "ez3": + return "application/vnd.ezpix-package" + case "fdf": + return "application/vnd.fdf" + case "mseed": + return "application/vnd.fdsn.mseed" + case "seed", "dataless": + return "application/vnd.fdsn.seed" + case "gph": + return "application/vnd.flographit" + case "ftc": + return "application/vnd.fluxtime.clip" + case "fm", "frame", "maker", "book": + return "application/vnd.framemaker" + case "fnc": + return "application/vnd.frogans.fnc" + case "ltf": + return "application/vnd.frogans.ltf" + case "fsc": + return "application/vnd.fsc.weblaunch" + case "oas": + return "application/vnd.fujitsu.oasys" + case "oa2": + return "application/vnd.fujitsu.oasys2" + case "oa3": + return "application/vnd.fujitsu.oasys3" + case "fg5": + return "application/vnd.fujitsu.oasysgp" + case "bh2": + return "application/vnd.fujitsu.oasysprs" + case "ddd": + return "application/vnd.fujixerox.ddd" + case "xdw": + return "application/vnd.fujixerox.docuworks" + case "xbd": + return "application/vnd.fujixerox.docuworks.binder" + case "fzs": + return "application/vnd.fuzzysheet" + case "txd": + return "application/vnd.genomatix.tuxedo" + case "ggb": + return "application/vnd.geogebra.file" + case "ggt": + return "application/vnd.geogebra.tool" + case "gex", "gre": + return "application/vnd.geometry-explorer" + case "gxt": + return "application/vnd.geonext" + case "g2w": + return "application/vnd.geoplan" + case "g3w": + return "application/vnd.geospace" + case "gmx": + return "application/vnd.gmx" + case "kml": + return "application/vnd.google-earth.kml+xml" + case "kmz": + return "application/vnd.google-earth.kmz" + case "gqf", "gqs": + return "application/vnd.grafeq" + case "gac": + return "application/vnd.groove-account" + case "ghf": + return "application/vnd.groove-help" + case "gim": + return "application/vnd.groove-identity-message" + case "grv": + return "application/vnd.groove-injector" + case "gtm": + return "application/vnd.groove-tool-message" + case "tpl": + return "application/vnd.groove-tool-template" + case "vcg": + return "application/vnd.groove-vcard" + case "hal": + return "application/vnd.hal+xml" + case "zmm": + return "application/vnd.handheld-entertainment+xml" + case "hbci": + return "application/vnd.hbci" + case "les": + return "application/vnd.hhe.lesson-player" + case "hpgl": + return "application/vnd.hp-hpgl" + case "hpid": + return "application/vnd.hp-hpid" + case "hps": + return "application/vnd.hp-hps" + case "jlt": + return "application/vnd.hp-jlyt" + case "pcl": + return "application/vnd.hp-pcl" + case "pclxl": + return "application/vnd.hp-pclxl" + case "sfd-hdstx": + return "application/vnd.hydrostatix.sof-data" + case "mpy": + return "application/vnd.ibm.minipay" + case "afp", "listafp", "list3820": + return "application/vnd.ibm.modcap" + case "irm": + return "application/vnd.ibm.rights-management" + case "sc": + return "application/vnd.ibm.secure-container" + case "icc", "icm": + return "application/vnd.iccprofile" + case "igl": + return "application/vnd.igloader" + case "ivp": + return "application/vnd.immervision-ivp" + case "ivu": + return "application/vnd.immervision-ivu" + case "igm": + return "application/vnd.insors.igm" + case "xpw", "xpx": + return "application/vnd.intercon.formnet" + case "i2g": + return "application/vnd.intergeo" + case "qbo": + return "application/vnd.intu.qbo" + case "qfx": + return "application/vnd.intu.qfx" + case "rcprofile": + return "application/vnd.ipunplugged.rcprofile" + case "irp": + return "application/vnd.irepository.package+xml" + case "xpr": + return "application/vnd.is-xpr" + case "fcs": + return "application/vnd.isac.fcs" + case "jam": + return "application/vnd.jam" + case "rms": + return "application/vnd.jcp.javame.midlet-rms" + case "jisp": + return "application/vnd.jisp" + case "joda": + return "application/vnd.joost.joda-archive" + case "ktz", "ktr": + return "application/vnd.kahootz" + case "karbon": + return "application/vnd.kde.karbon" + case "chrt": + return "application/vnd.kde.kchart" + case "kfo": + return "application/vnd.kde.kformula" + case "flw": + return "application/vnd.kde.kivio" + case "kon": + return "application/vnd.kde.kontour" + case "kpr", "kpt": + return "application/vnd.kde.kpresenter" + case "ksp": + return "application/vnd.kde.kspread" + case "kwd", "kwt": + return "application/vnd.kde.kword" + case "htke": + return "application/vnd.kenameaapp" + case "kia": + return "application/vnd.kidspiration" + case "kne", "knp": + return "application/vnd.kinar" + case "skp", "skd", "skt", "skm": + return "application/vnd.koan" + case "sse": + return "application/vnd.kodak-descriptor" + case "lasxml": + return "application/vnd.las.las+xml" + case "lbd": + return "application/vnd.llamagraphics.life-balance.desktop" + case "lbe": + return "application/vnd.llamagraphics.life-balance.exchange+xml" + case "123": + return "application/vnd.lotus-1-2-3" + case "apr": + return "application/vnd.lotus-approach" + case "pre": + return "application/vnd.lotus-freelance" + case "nsf": + return "application/vnd.lotus-notes" + case "org": + return "application/vnd.lotus-organizer" + case "scm": + return "application/vnd.lotus-screencam" + case "lwp": + return "application/vnd.lotus-wordpro" + case "portpkg": + return "application/vnd.macports.portpkg" + case "mcd": + return "application/vnd.mcd" + case "mc1": + return "application/vnd.medcalcdata" + case "cdkey": + return "application/vnd.mediastation.cdkey" + case "mwf": + return "application/vnd.mfer" + case "mfm": + return "application/vnd.mfmp" + case "flo": + return "application/vnd.micrografx.flo" + case "igx": + return "application/vnd.micrografx.igx" + case "mif": + return "application/vnd.mif" + case "daf": + return "application/vnd.mobius.daf" + case "dis": + return "application/vnd.mobius.dis" + case "mbk": + return "application/vnd.mobius.mbk" + case "mqy": + return "application/vnd.mobius.mqy" + case "msl": + return "application/vnd.mobius.msl" + case "plc": + return "application/vnd.mobius.plc" + case "txf": + return "application/vnd.mobius.txf" + case "mpn": + return "application/vnd.mophun.application" + case "mpc": + return "application/vnd.mophun.certificate" + case "xul": + return "application/vnd.mozilla.xul+xml" + case "cil": + return "application/vnd.ms-artgalry" + case "cab": + return "application/vnd.ms-cab-compressed" + case "xls", "xlm", "xla", "xlc", "xlt", "xlw": + return "application/vnd.ms-excel" + case "xlam": + return "application/vnd.ms-excel.addin.macroenabled.12" + case "xlsb": + return "application/vnd.ms-excel.sheet.binary.macroenabled.12" + case "xlsm": + return "application/vnd.ms-excel.sheet.macroenabled.12" + case "xltm": + return "application/vnd.ms-excel.template.macroenabled.12" + case "eot": + return "application/vnd.ms-fontobject" + case "chm": + return "application/vnd.ms-htmlhelp" + case "ims": + return "application/vnd.ms-ims" + case "lrm": + return "application/vnd.ms-lrm" + case "thmx": + return "application/vnd.ms-officetheme" + case "cat": + return "application/vnd.ms-pki.seccat" + case "stl": + return "application/vnd.ms-pki.stl" + case "ppt", "pps", "pot": + return "application/vnd.ms-powerpoint" + case "ppam": + return "application/vnd.ms-powerpoint.addin.macroenabled.12" + case "pptm": + return "application/vnd.ms-powerpoint.presentation.macroenabled.12" + case "sldm": + return "application/vnd.ms-powerpoint.slide.macroenabled.12" + case "ppsm": + return "application/vnd.ms-powerpoint.slideshow.macroenabled.12" + case "potm": + return "application/vnd.ms-powerpoint.template.macroenabled.12" + case "mpp", "mpt": + return "application/vnd.ms-project" + case "docm": + return "application/vnd.ms-word.document.macroenabled.12" + case "dotm": + return "application/vnd.ms-word.template.macroenabled.12" + case "wps", "wks", "wcm", "wdb": + return "application/vnd.ms-works" + case "wpl": + return "application/vnd.ms-wpl" + case "xps": + return "application/vnd.ms-xpsdocument" + case "mseq": + return "application/vnd.mseq" + case "mus": + return "application/vnd.musician" + case "msty": + return "application/vnd.muvee.style" + case "taglet": + return "application/vnd.mynfc" + case "nlu": + return "application/vnd.neurolanguage.nlu" + case "ntf", "nitf": + return "application/vnd.nitf" + case "nnd": + return "application/vnd.noblenet-directory" + case "nns": + return "application/vnd.noblenet-sealer" + case "nnw": + return "application/vnd.noblenet-web" + case "ngdat": + return "application/vnd.nokia.n-gage.data" + case "n-gage": + return "application/vnd.nokia.n-gage.symbian.install" + case "rpst": + return "application/vnd.nokia.radio-preset" + case "rpss": + return "application/vnd.nokia.radio-presets" + case "edm": + return "application/vnd.novadigm.edm" + case "edx": + return "application/vnd.novadigm.edx" + case "ext": + return "application/vnd.novadigm.ext" + case "odc": + return "application/vnd.oasis.opendocument.chart" + case "otc": + return "application/vnd.oasis.opendocument.chart-template" + case "odb": + return "application/vnd.oasis.opendocument.database" + case "odf": + return "application/vnd.oasis.opendocument.formula" + case "odft": + return "application/vnd.oasis.opendocument.formula-template" + case "odg": + return "application/vnd.oasis.opendocument.graphics" + case "otg": + return "application/vnd.oasis.opendocument.graphics-template" + case "odi": + return "application/vnd.oasis.opendocument.image" + case "oti": + return "application/vnd.oasis.opendocument.image-template" + case "odp": + return "application/vnd.oasis.opendocument.presentation" + case "otp": + return "application/vnd.oasis.opendocument.presentation-template" + case "ods": + return "application/vnd.oasis.opendocument.spreadsheet" + case "ots": + return "application/vnd.oasis.opendocument.spreadsheet-template" + case "odt": + return "application/vnd.oasis.opendocument.text" + case "odm": + return "application/vnd.oasis.opendocument.text-master" + case "ott": + return "application/vnd.oasis.opendocument.text-template" + case "oth": + return "application/vnd.oasis.opendocument.text-web" + case "xo": + return "application/vnd.olpc-sugar" + case "dd2": + return "application/vnd.oma.dd2+xml" + case "oxt": + return "application/vnd.openofficeorg.extension" + case "pptx": + return "application/vnd.openxmlformats-officedocument.presentationml.presentation" + case "sldx": + return "application/vnd.openxmlformats-officedocument.presentationml.slide" + case "ppsx": + return "application/vnd.openxmlformats-officedocument.presentationml.slideshow" + case "potx": + return "application/vnd.openxmlformats-officedocument.presentationml.template" + case "xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + case "xltx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.template" + case "docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + case "dotx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.template" + case "mgp": + return "application/vnd.osgeo.mapguide.package" + case "dp": + return "application/vnd.osgi.dp" + case "esa": + return "application/vnd.osgi.subsystem" + case "pdb", "pqa", "oprc": + return "application/vnd.palm" + case "paw": + return "application/vnd.pawaafile" + case "str": + return "application/vnd.pg.format" + case "ei6": + return "application/vnd.pg.osasli" + case "efif": + return "application/vnd.picsel" + case "wg": + return "application/vnd.pmi.widget" + case "plf": + return "application/vnd.pocketlearn" + case "pbd": + return "application/vnd.powerbuilder6" + case "box": + return "application/vnd.previewsystems.box" + case "mgz": + return "application/vnd.proteus.magazine" + case "qps": + return "application/vnd.publishare-delta-tree" + case "ptid": + return "application/vnd.pvi.ptid1" + case "qxd", "qxt", "qwd", "qwt", "qxl", "qxb": + return "application/vnd.quark.quarkxpress" + case "bed": + return "application/vnd.realvnc.bed" + case "mxl": + return "application/vnd.recordare.musicxml" + case "musicxml": + return "application/vnd.recordare.musicxml+xml" + case "cryptonote": + return "application/vnd.rig.cryptonote" + case "cod": + return "application/vnd.rim.cod" + case "rm": + return "application/vnd.rn-realmedia" + case "rmvb": + return "application/vnd.rn-realmedia-vbr" + case "link66": + return "application/vnd.route66.link66+xml" + case "st": + return "application/vnd.sailingtracker.track" + case "see": + return "application/vnd.seemail" + case "sema": + return "application/vnd.sema" + case "semd": + return "application/vnd.semd" + case "semf": + return "application/vnd.semf" + case "ifm": + return "application/vnd.shana.informed.formdata" + case "itp": + return "application/vnd.shana.informed.formtemplate" + case "iif": + return "application/vnd.shana.informed.interchange" + case "ipk": + return "application/vnd.shana.informed.package" + case "twd", "twds": + return "application/vnd.simtech-mindmapper" + case "mmf": + return "application/vnd.smaf" + case "teacher": + return "application/vnd.smart.teacher" + case "sdkm", "sdkd": + return "application/vnd.solent.sdkm+xml" + case "dxp": + return "application/vnd.spotfire.dxp" + case "sfs": + return "application/vnd.spotfire.sfs" + case "sdc": + return "application/vnd.stardivision.calc" + case "sda": + return "application/vnd.stardivision.draw" + case "sdd": + return "application/vnd.stardivision.impress" + case "smf": + return "application/vnd.stardivision.math" + case "sdw", "vor": + return "application/vnd.stardivision.writer" + case "sgl": + return "application/vnd.stardivision.writer-global" + case "smzip": + return "application/vnd.stepmania.package" + case "sm": + return "application/vnd.stepmania.stepchart" + case "sxc": + return "application/vnd.sun.xml.calc" + case "stc": + return "application/vnd.sun.xml.calc.template" + case "sxd": + return "application/vnd.sun.xml.draw" + case "std": + return "application/vnd.sun.xml.draw.template" + case "sxi": + return "application/vnd.sun.xml.impress" + case "sti": + return "application/vnd.sun.xml.impress.template" + case "sxm": + return "application/vnd.sun.xml.math" + case "sxw": + return "application/vnd.sun.xml.writer" + case "sxg": + return "application/vnd.sun.xml.writer.global" + case "stw": + return "application/vnd.sun.xml.writer.template" + case "sus", "susp": + return "application/vnd.sus-calendar" + case "svd": + return "application/vnd.svd" + case "sis", "sisx": + return "application/vnd.symbian.install" + case "xsm": + return "application/vnd.syncml+xml" + case "bdm": + return "application/vnd.syncml.dm+wbxml" + case "xdm": + return "application/vnd.syncml.dm+xml" + case "tao": + return "application/vnd.tao.intent-module-archive" + case "pcap", "cap", "dmp": + return "application/vnd.tcpdump.pcap" + case "tmo": + return "application/vnd.tmobile-livetv" + case "tpt": + return "application/vnd.trid.tpt" + case "mxs": + return "application/vnd.triscape.mxs" + case "tra": + return "application/vnd.trueapp" + case "ufd", "ufdl": + return "application/vnd.ufdl" + case "utz": + return "application/vnd.uiq.theme" + case "umj": + return "application/vnd.umajin" + case "unityweb": + return "application/vnd.unity" + case "uoml": + return "application/vnd.uoml+xml" + case "vcx": + return "application/vnd.vcx" + case "vsd", "vst", "vss", "vsw": + return "application/vnd.visio" + case "vis": + return "application/vnd.visionary" + case "vsf": + return "application/vnd.vsf" + case "wbxml": + return "application/vnd.wap.wbxml" + case "wmlc": + return "application/vnd.wap.wmlc" + case "wmlsc": + return "application/vnd.wap.wmlscriptc" + case "wtb": + return "application/vnd.webturbo" + case "nbp": + return "application/vnd.wolfram.player" + case "wpd": + return "application/vnd.wordperfect" + case "wqd": + return "application/vnd.wqd" + case "stf": + return "application/vnd.wt.stf" + case "xar": + return "application/vnd.xara" + case "xfdl": + return "application/vnd.xfdl" + case "hvd": + return "application/vnd.yamaha.hv-dic" + case "hvs": + return "application/vnd.yamaha.hv-script" + case "hvp": + return "application/vnd.yamaha.hv-voice" + case "osf": + return "application/vnd.yamaha.openscoreformat" + case "osfpvg": + return "application/vnd.yamaha.openscoreformat.osfpvg+xml" + case "saf": + return "application/vnd.yamaha.smaf-audio" + case "spf": + return "application/vnd.yamaha.smaf-phrase" + case "cmp": + return "application/vnd.yellowriver-custom-menu" + case "zir", "zirz": + return "application/vnd.zul" + case "zaz": + return "application/vnd.zzazz.deck+xml" + case "vxml": + return "application/voicexml+xml" + case "wgt": + return "application/widget" + case "hlp": + return "application/winhlp" + case "wsdl": + return "application/wsdl+xml" + case "wspolicy": + return "application/wspolicy+xml" + case "7z": + return "application/x-7z-compressed" + case "abw": + return "application/x-abiword" + case "ace": + return "application/x-ace-compressed" + case "dmg": + return "application/x-apple-diskimage" + case "aab", "x32", "u32", "vox": + return "application/x-authorware-bin" + case "aam": + return "application/x-authorware-map" + case "aas": + return "application/x-authorware-seg" + case "bcpio": + return "application/x-bcpio" + case "torrent": + return "application/x-bittorrent" + case "blb", "blorb": + return "application/x-blorb" + case "bz": + return "application/x-bzip" + case "bz2", "boz": + return "application/x-bzip2" + case "cbr", "cba", "cbt", "cbz", "cb7": + return "application/x-cbr" + case "vcd": + return "application/x-cdlink" + case "cfs": + return "application/x-cfs-compressed" + case "chat": + return "application/x-chat" + case "pgn": + return "application/x-chess-pgn" + case "nsc": + return "application/x-conference" + case "cpio": + return "application/x-cpio" + case "csh": + return "application/x-csh" + case "deb", "udeb": + return "application/x-debian-package" + case "dgc": + return "application/x-dgc-compressed" + case "dir", "dcr", "dxr", "cst", "cct", "cxt", "w3d", "fgd", "swa": + return "application/x-director" + case "wad": + return "application/x-doom" + case "ncx": + return "application/x-dtbncx+xml" + case "dtb": + return "application/x-dtbook+xml" + case "res": + return "application/x-dtbresource+xml" + case "dvi": + return "application/x-dvi" + case "evy": + return "application/x-envoy" + case "eva": + return "application/x-eva" + case "bdf": + return "application/x-font-bdf" + case "gsf": + return "application/x-font-ghostscript" + case "psf": + return "application/x-font-linux-psf" + case "pcf": + return "application/x-font-pcf" + case "snf": + return "application/x-font-snf" + case "pfa", "pfb", "pfm", "afm": + return "application/x-font-type1" + case "arc": + return "application/x-freearc" + case "spl": + return "application/x-futuresplash" + case "gca": + return "application/x-gca-compressed" + case "ulx": + return "application/x-glulx" + case "gnumeric": + return "application/x-gnumeric" + case "gramps": + return "application/x-gramps-xml" + case "gtar": + return "application/x-gtar" + case "hdf": + return "application/x-hdf" + case "install": + return "application/x-install-instructions" + case "iso": + return "application/x-iso9660-image" + case "jnlp": + return "application/x-java-jnlp-file" + case "latex": + return "application/x-latex" + case "lzh", "lha": + return "application/x-lzh-compressed" + case "mie": + return "application/x-mie" + case "prc", "mobi": + return "application/x-mobipocket-ebook" + case "application": + return "application/x-ms-application" + case "lnk": + return "application/x-ms-shortcut" + case "wmd": + return "application/x-ms-wmd" + case "wmz": + return "application/x-ms-wmz" + case "xbap": + return "application/x-ms-xbap" + case "mdb": + return "application/x-msaccess" + case "obd": + return "application/x-msbinder" + case "crd": + return "application/x-mscardfile" + case "clp": + return "application/x-msclip" + case "exe", "dll", "com", "bat", "msi": + return "application/x-msdownload" + case "mvb", "m13", "m14": + return "application/x-msmediaview" + case "wmf", "emf", "emz": + return "application/x-msmetafile" + case "mny": + return "application/x-msmoney" + case "pub": + return "application/x-mspublisher" + case "scd": + return "application/x-msschedule" + case "trm": + return "application/x-msterminal" + case "wri": + return "application/x-mswrite" + case "nc", "cdf": + return "application/x-netcdf" + case "nzb": + return "application/x-nzb" + case "p12", "pfx": + return "application/x-pkcs12" + case "p7b", "spc": + return "application/x-pkcs7-certificates" + case "p7r": + return "application/x-pkcs7-certreqresp" + case "rar": + return "application/x-rar-compressed" + case "ris": + return "application/x-research-info-systems" + case "sh": + return "application/x-sh" + case "shar": + return "application/x-shar" + case "swf": + return "application/x-shockwave-flash" + case "xap": + return "application/x-silverlight-app" + case "sql": + return "application/x-sql" + case "sit": + return "application/x-stuffit" + case "sitx": + return "application/x-stuffitx" + case "srt": + return "application/x-subrip" + case "sv4cpio": + return "application/x-sv4cpio" + case "sv4crc": + return "application/x-sv4crc" + case "t3": + return "application/x-t3vm-image" + case "gam": + return "application/x-tads" + case "tar": + return "application/x-tar" + case "tcl": + return "application/x-tcl" + case "tex": + return "application/x-tex" + case "tfm": + return "application/x-tex-tfm" + case "texinfo", "texi": + return "application/x-texinfo" + case "obj": + return "application/x-tgif" + case "ustar": + return "application/x-ustar" + case "src": + return "application/x-wais-source" + case "der", "crt": + return "application/x-x509-ca-cert" + case "fig": + return "application/x-xfig" + case "xlf": + return "application/x-xliff+xml" + case "xpi": + return "application/x-xpinstall" + case "xz": + return "application/x-xz" + case "z1", "z2", "z3", "z4", "z5", "z6", "z7", "z8": + return "application/x-zmachine" + case "xaml": + return "application/xaml+xml" + case "xdf": + return "application/xcap-diff+xml" + case "xenc": + return "application/xenc+xml" + case "xhtml", "xht": + return "application/xhtml+xml" + case "xml", "xsl": + return "application/xml" + case "dtd": + return "application/xml-dtd" + case "xop": + return "application/xop+xml" + case "xpl": + return "application/xproc+xml" + case "xslt": + return "application/xslt+xml" + case "xspf": + return "application/xspf+xml" + case "mxml", "xhvml", "xvml", "xvm": + return "application/xv+xml" + case "yang": + return "application/yang" + case "yin": + return "application/yin+xml" + case "zip": + return "application/zip" + case "adp": + return "audio/adpcm" + case "au", "snd": + return "audio/basic" + case "mid", "midi", "kar", "rmi": + return "audio/midi" + case "m4a", "mp4a": + return "audio/mp4" + case "mpga", "mp2", "mp2a", "mp3", "m2a", "m3a": + return "audio/mpeg" + case "oga", "ogg", "spx": + return "audio/ogg" + case "s3m": + return "audio/s3m" + case "sil": + return "audio/silk" + case "uva", "uvva": + return "audio/vnd.dece.audio" + case "eol": + return "audio/vnd.digital-winds" + case "dra": + return "audio/vnd.dra" + case "dts": + return "audio/vnd.dts" + case "dtshd": + return "audio/vnd.dts.hd" + case "lvp": + return "audio/vnd.lucent.voice" + case "pya": + return "audio/vnd.ms-playready.media.pya" + case "ecelp4800": + return "audio/vnd.nuera.ecelp4800" + case "ecelp7470": + return "audio/vnd.nuera.ecelp7470" + case "ecelp9600": + return "audio/vnd.nuera.ecelp9600" + case "rip": + return "audio/vnd.rip" + case "weba": + return "audio/webm" + case "aac": + return "audio/x-aac" + case "aif", "aiff", "aifc": + return "audio/x-aiff" + case "caf": + return "audio/x-caf" + case "flac": + return "audio/x-flac" + case "mka": + return "audio/x-matroska" + case "m3u": + return "audio/x-mpegurl" + case "wax": + return "audio/x-ms-wax" + case "wma": + return "audio/x-ms-wma" + case "ram", "ra": + return "audio/x-pn-realaudio" + case "rmp": + return "audio/x-pn-realaudio-plugin" + case "wav": + return "audio/x-wav" + case "xm": + return "audio/xm" + case "cdx": + return "chemical/x-cdx" + case "cif": + return "chemical/x-cif" + case "cmdf": + return "chemical/x-cmdf" + case "cml": + return "chemical/x-cml" + case "csml": + return "chemical/x-csml" + case "xyz": + return "chemical/x-xyz" + case "ttc": + return "font/collection" + case "otf": + return "font/otf" + case "ttf": + return "font/ttf" + case "woff": + return "font/woff" + case "woff2": + return "font/woff2" + case "bmp": + return "image/bmp" + case "cgm": + return "image/cgm" + case "g3": + return "image/g3fax" + case "gif": + return "image/gif" + case "ief": + return "image/ief" + case "jpeg", "jpg", "jpe": + return "image/jpeg" + case "ktx": + return "image/ktx" + case "png": + return "image/png" + case "btif": + return "image/prs.btif" + case "sgi": + return "image/sgi" + case "svg", "svgz": + return "image/svg+xml" + case "tiff", "tif": + return "image/tiff" + case "psd": + return "image/vnd.adobe.photoshop" + case "uvi", "uvvi", "uvg", "uvvg": + return "image/vnd.dece.graphic" + case "djvu", "djv": + return "image/vnd.djvu" + case "sub": + return "image/vnd.dvb.subtitle" + case "dwg": + return "image/vnd.dwg" + case "dxf": + return "image/vnd.dxf" + case "fbs": + return "image/vnd.fastbidsheet" + case "fpx": + return "image/vnd.fpx" + case "fst": + return "image/vnd.fst" + case "mmr": + return "image/vnd.fujixerox.edmics-mmr" + case "rlc": + return "image/vnd.fujixerox.edmics-rlc" + case "mdi": + return "image/vnd.ms-modi" + case "wdp": + return "image/vnd.ms-photo" + case "npx": + return "image/vnd.net-fpx" + case "wbmp": + return "image/vnd.wap.wbmp" + case "xif": + return "image/vnd.xiff" + case "webp": + return "image/webp" + case "3ds": + return "image/x-3ds" + case "ras": + return "image/x-cmu-raster" + case "cmx": + return "image/x-cmx" + case "fh", "fhc", "fh4", "fh5", "fh7": + return "image/x-freehand" + case "ico": + return "image/x-icon" + case "sid": + return "image/x-mrsid-image" + case "pcx": + return "image/x-pcx" + case "pic", "pct": + return "image/x-pict" + case "pnm": + return "image/x-portable-anymap" + case "pbm": + return "image/x-portable-bitmap" + case "pgm": + return "image/x-portable-graymap" + case "ppm": + return "image/x-portable-pixmap" + case "rgb": + return "image/x-rgb" + case "tga": + return "image/x-tga" + case "xbm": + return "image/x-xbitmap" + case "xpm": + return "image/x-xpixmap" + case "xwd": + return "image/x-xwindowdump" + case "eml", "mime": + return "message/rfc822" + case "igs", "iges": + return "model/iges" + case "msh", "mesh", "silo": + return "model/mesh" + case "dae": + return "model/vnd.collada+xml" + case "dwf": + return "model/vnd.dwf" + case "gdl": + return "model/vnd.gdl" + case "gtw": + return "model/vnd.gtw" + case "mts": + return "model/vnd.mts" + case "vtu": + return "model/vnd.vtu" + case "wrl", "vrml": + return "model/vrml" + case "x3db", "x3dbz": + return "model/x3d+binary" + case "x3dv", "x3dvz": + return "model/x3d+vrml" + case "x3d", "x3dz": + return "model/x3d+xml" + case "appcache": + return "text/cache-manifest" + case "ics", "ifb": + return "text/calendar" + case "css": + return "text/css" + case "csv": + return "text/csv" + case "html", "htm": + return "text/html" + case "n3": + return "text/n3" + case "txt", "text", "conf", "def", "list", "log", "in": + return "text/plain" + case "dsc": + return "text/prs.lines.tag" + case "rtx": + return "text/richtext" + case "sgml", "sgm": + return "text/sgml" + case "tsv": + return "text/tab-separated-values" + case "t", "tr", "roff", "man", "me", "ms": + return "text/troff" + case "ttl": + return "text/turtle" + case "uri", "uris", "urls": + return "text/uri-list" + case "vcard": + return "text/vcard" + case "curl": + return "text/vnd.curl" + case "dcurl": + return "text/vnd.curl.dcurl" + case "mcurl": + return "text/vnd.curl.mcurl" + case "scurl": + return "text/vnd.curl.scurl" + case "fly": + return "text/vnd.fly" + case "flx": + return "text/vnd.fmi.flexstor" + case "gv": + return "text/vnd.graphviz" + case "3dml": + return "text/vnd.in3d.3dml" + case "spot": + return "text/vnd.in3d.spot" + case "jad": + return "text/vnd.sun.j2me.app-descriptor" + case "wml": + return "text/vnd.wap.wml" + case "wmls": + return "text/vnd.wap.wmlscript" + case "s", "asm": + return "text/x-asm" + case "c", "cc", "cxx", "cpp", "h", "hh", "dic": + return "text/x-c" + case "f", "for", "f77", "f90": + return "text/x-fortran" + case "java": + return "text/x-java-source" + case "nfo": + return "text/x-nfo" + case "opml": + return "text/x-opml" + case "p", "pas": + return "text/x-pascal" + case "etx": + return "text/x-setext" + case "sfv": + return "text/x-sfv" + case "uu": + return "text/x-uuencode" + case "vcs": + return "text/x-vcalendar" + case "vcf": + return "text/x-vcard" + case "3gp": + return "video/3gpp" + case "3g2": + return "video/3gpp2" + case "h261": + return "video/h261" + case "h263": + return "video/h263" + case "h264": + return "video/h264" + case "jpgv": + return "video/jpeg" + case "jpm", "jpgm": + return "video/jpm" + case "mj2", "mjp2": + return "video/mj2" + case "mp4", "mp4v", "mpg4": + return "video/mp4" + case "mpeg", "mpg", "mpe", "m1v", "m2v": + return "video/mpeg" + case "ogv": + return "video/ogg" + case "qt", "mov": + return "video/quicktime" + case "uvh", "uvvh": + return "video/vnd.dece.hd" + case "uvm", "uvvm": + return "video/vnd.dece.mobile" + case "uvp", "uvvp": + return "video/vnd.dece.pd" + case "uvs", "uvvs": + return "video/vnd.dece.sd" + case "uvv", "uvvv": + return "video/vnd.dece.video" + case "dvb": + return "video/vnd.dvb.file" + case "fvt": + return "video/vnd.fvt" + case "mxu", "m4u": + return "video/vnd.mpegurl" + case "pyv": + return "video/vnd.ms-playready.media.pyv" + case "uvu", "uvvu": + return "video/vnd.uvvu.mp4" + case "viv": + return "video/vnd.vivo" + case "webm": + return "video/webm" + case "f4v": + return "video/x-f4v" + case "fli": + return "video/x-fli" + case "flv": + return "video/x-flv" + case "m4v": + return "video/x-m4v" + case "mkv", "mk3d", "mks": + return "video/x-matroska" + case "mng": + return "video/x-mng" + case "asf", "asx": + return "video/x-ms-asf" + case "vob": + return "video/x-ms-vob" + case "wm": + return "video/x-ms-wm" + case "wmv": + return "video/x-ms-wmv" + case "wmx": + return "video/x-ms-wmx" + case "wvx": + return "video/x-ms-wvx" + case "avi": + return "video/x-msvideo" + case "movie": + return "video/x-sgi-movie" + case "smv": + return "video/x-smv" + case "ice": + return "x-conference/x-cooltalk" + default: + return nil + } +} diff --git a/Sources/Storage/Utilities/S3.swift b/Sources/Storage/Utilities/S3.swift new file mode 100644 index 0000000..d8f09d2 --- /dev/null +++ b/Sources/Storage/Utilities/S3.swift @@ -0,0 +1,363 @@ +import Core +import Vapor +import Crypto +import Foundation + +public enum Region: String { + case usEast1 = "us-east-1" + case usEast2 = "us-east-2" + case usWest1 = "us-west-1" + case usWest2 = "us-west-2" + case euWest1 = "eu-west-1" + case euCentral1 = "eu-central-1" + case apSouth1 = "ap-south-1" + case apSoutheast1 = "ap-southeast-1" + case apSoutheast2 = "ap-southeast-2" + case apNortheast1 = "ap-northeast-1" + case apNortheast2 = "ap-northeast-2" + case saEast1 = "sa-east-1" + + public var host: String { + switch self { + case .usEast1: return "s3.amazonaws.com" + case .usEast2: return "s3.us-east-2.amazonaws.com" + case .usWest1: return "s3-us-west-1.amazonaws.com" + case .usWest2: return "s3-us-west-2.amazonaws.com" + case .euWest1: return "s3-eu-west-1.amazonaws.com" + case .euCentral1: return "s3.eu-central-1.amazonaws.com" + case .apSouth1: return "s3.ap-south-1.amazonaws.com" + case .apSoutheast1: return "s3-ap-southeast-1.amazonaws.com" + case .apSoutheast2: return "s3-ap-southeast-2.amazonaws.com" + case .apNortheast1: return "s3-ap-northeast-1.amazonaws.com" + case .apNortheast2: return "s3.ap-northeast-2.amazonaws.com" + case .saEast1: return "s3-sa-east-1.amazonaws.com" + } + } +} + +public enum Payload { + case bytes(Data) + case unsigned + case none +} + +extension Payload { + func hashed() throws -> String { + switch self { + case .bytes(let bytes): + return try SHA256.hash(bytes).hexEncodedString() + + case .unsigned: + return "UNSIGNED-PAYLOAD" + + case .none: + // SHA256 hash of '' + return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } +} + +extension Payload { + var bytes: Data { + switch self { + case .bytes(let bytes): + return bytes + + default: + return Data() + } + } +} + +extension String { + public static let awsQueryAllowed = CharacterSet( + charactersIn: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~=&" + ) + + public static let awsPathAllowed = CharacterSet( + charactersIn: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~/" + ) +} + +public enum AccessControlList: String { + case privateAccess = "private" + case publicRead = "public-read" + case publicReadWrite = "public-read-write" + case awsExecRead = "aws-exec-read" + case authenticatedRead = "authenticated-read" + case bucketOwnerRead = "bucket-owner-read" + case bucketOwnerFullControl = "bucket-owner-full-control" +} + +public struct AWSSignatureV4 { + public enum Method: String { + case delete = "DELETE" + case get = "GET" + case post = "POST" + case put = "PUT" + } + + let service: String + let host: String + let region: String + let accessKey: String + let secretKey: String + let contentType = "application/x-www-form-urlencoded; charset=utf-8" + + internal var unitTestDate: Date? + + var amzDate: String { + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "YYYYMMdd'T'HHmmss'Z'" + return dateFormatter.string(from: unitTestDate ?? Date()) + } + + public init( + service: String, + host: String, + region: Region, + accessKey: String, + secretKey: String + ) { + self.service = service + self.host = host + self.region = region.rawValue + self.accessKey = accessKey + self.secretKey = secretKey + } + + func getStringToSign( + algorithm: String, + date: String, + scope: String, + canonicalHash: String + ) -> String { + return [ + algorithm, + date, + scope, + canonicalHash + ].joined(separator: "\n") + } + + func getSignature(_ stringToSign: String) throws -> String { + let dateHMAC = try HMAC.SHA256.authenticate(dateStamp(), key: "AWS4\(secretKey)") + let regionHMAC = try HMAC.SHA256.authenticate(region, key: dateHMAC) + let serviceHMAC = try HMAC.SHA256.authenticate(service, key: regionHMAC) + let signingHMAC = try HMAC.SHA256.authenticate("aws4_request", key: serviceHMAC) + + let signature = try HMAC.SHA256.authenticate(stringToSign, key: signingHMAC) + return signature.hexEncodedString() + } + + func getCredentialScope() -> String { + return [ + dateStamp(), + region, + service, + "aws4_request" + ].joined(separator: "/") + } + + func getCanonicalRequest( + payloadHash: String, + method: Method, + path: String, + query: String, + canonicalHeaders: String, + signedHeaders: String + ) throws -> String { + let path = path.addingPercentEncoding(withAllowedCharacters: String.awsPathAllowed) ?? "" + let query = query.addingPercentEncoding(withAllowedCharacters: String.awsQueryAllowed) ?? "" + return [ + method.rawValue, + path, + query, + canonicalHeaders, + "", + signedHeaders, + payloadHash + ].joined(separator: "\n") + } + + func dateStamp() -> String { + let date = unitTestDate ?? Date() + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "YYYYMMdd" + return dateFormatter.string(from: date) + } +} + +extension AWSSignatureV4 { + func generateHeadersToSign( + headers: inout [String: String], + host: String, + hash: String + ) { + headers["Host"] = host + headers["X-Amz-Date"] = amzDate + + if hash != "UNSIGNED-PAYLOAD" { + headers["x-amz-content-sha256"] = hash + } + } + + func alphabetize(_ dict: [String: String]) -> [(key: String, value: String)] { + return dict.sorted(by: { $0.0.lowercased() < $1.0.lowercased() }) + } + + func createCanonicalHeaders(_ headers: [(key: String, value: String)]) -> String { + return headers.map { + "\($0.key.lowercased()):\($0.value)" + }.joined(separator: "\n") + } + + func createAuthorizationHeader( + algorithm: String, + credentialScope: String, + signature: String, + signedHeaders: String + ) -> String { + return "\(algorithm) Credential=\(accessKey)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature)" + } +} + +extension AWSSignatureV4 { + /** + Sign a request to be sent to an AWS API. + - returns: + A dictionary with headers to attach to a request + - parameters: + - payload: A hash of this data will be included in the headers + - method: Type of HTTP request + - path: API call being referenced + - query: Additional querystring in key-value format ("?key=value&key2=value2") + - headers: HTTP headers added to the request + */ + public func sign( + payload: Payload = .none, + method: Method = .get, + path: String, + query: String? = nil, + headers: [String: String] = [:] + ) throws -> [String: String] { + let algorithm = "AWS4-HMAC-SHA256" + let credentialScope = getCredentialScope() + let payloadHash = try payload.hashed() + + var headers = headers + + generateHeadersToSign(headers: &headers, host: host, hash: payloadHash) + + let sortedHeaders = alphabetize(headers) + let signedHeaders = sortedHeaders.map { $0.key.lowercased() }.joined(separator: ";") + let canonicalHeaders = createCanonicalHeaders(sortedHeaders) + + let canonicalRequest = try getCanonicalRequest( + payloadHash: payloadHash, + method: method, + path: path, + query: query ?? "", + canonicalHeaders: canonicalHeaders, + signedHeaders: signedHeaders + ) + + let canonicalHash = try SHA256.hash(canonicalRequest).hexEncodedString() + + let stringToSign = getStringToSign( + algorithm: algorithm, + date: amzDate, + scope: credentialScope, + canonicalHash: canonicalHash + ) + + let signature = try getSignature(stringToSign) + + let authorizationHeader = createAuthorizationHeader( + algorithm: algorithm, + credentialScope: credentialScope, + signature: signature, + signedHeaders: signedHeaders + ) + + var requestHeaders: [String: String] = [ + "X-Amz-Date": amzDate, + "Content-Type": contentType, + "x-amz-content-sha256": payloadHash, + "Authorization": authorizationHeader, + "Host": host + ] + + headers.forEach { key, value in + requestHeaders[key] = value + } + + return requestHeaders + } +} + +public struct S3: Service { + public enum Error: Swift.Error { + case unimplemented + case invalidPath + case invalidResponse(HTTPStatus) + } + + let signer: AWSSignatureV4 + public var host: String + + public init( + host: String, + accessKey: String, + secretKey: String, + region: Region + ) { + self.host = host + signer = AWSSignatureV4( + service: "s3", + host: host, + region: region, + accessKey: accessKey, + secretKey: secretKey + ) + } + + public func upload( + bytes: Data, + path: String, + access: AccessControlList = .publicRead, + on container: Container + ) throws -> Future { + guard let url = URL(string: generateURL(for: path)) else { + throw Error.invalidPath + } + + let signedHeaders = try signer.sign( + payload: .bytes(bytes), + method: .put, + path: path, + headers: ["x-amz-acl": access.rawValue] + ) + + var headers: HTTPHeaders = [:] + signedHeaders.forEach { + headers.add(name: $0.key, value: $0.value) + } + + let client = try container.client() + let req = Request(using: container) + req.http.method = .PUT + req.http.headers = headers + req.http.body = HTTPBody(data: bytes) + req.http.url = url + return client.send(req) + } +} + +extension S3 { + func generateURL(for path: String) -> String { + return "https://\(host)\(path)" + } +} diff --git a/Sources/Storage/Utilities/Scanner.swift b/Sources/Storage/Utilities/Scanner.swift index 4092b73..a79dc35 100644 --- a/Sources/Storage/Utilities/Scanner.swift +++ b/Sources/Storage/Utilities/Scanner.swift @@ -9,7 +9,7 @@ extension Scanner { init(_ data: [Element]) { self.elementsCopy = data self.elements = elementsCopy.withUnsafeBufferPointer { $0 } - + self.pointer = elements.baseAddress! } } @@ -19,7 +19,7 @@ extension Scanner { guard pointer.advanced(by: n) < elements.endAddress else { return nil } return pointer.advanced(by: n).pointee } - + /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have @discardableResult mutating func pop() -> Element { @@ -27,7 +27,7 @@ extension Scanner { defer { pointer = pointer.advanced(by: 1) } return pointer.pointee } - + /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have @discardableResult mutating func attemptPop() throws -> Element { @@ -35,7 +35,7 @@ extension Scanner { defer { pointer = pointer.advanced(by: 1) } return pointer.pointee } - + mutating func pop(_ n: Int) { for _ in 0.. { var key: UInt8 var value: ValueType? - + var children: [Trie] = [] - + var isLeaf: Bool { return children.count == 0 } - + convenience init() { self.init(key: 0x00) } - + init(key: UInt8, value: ValueType? = nil) { self.key = key self.value = value @@ -27,58 +27,58 @@ extension Trie { children.append(newValue) return } - + guard let newValue = newValue else { children.remove(at: index) return } - + let child = children[index] guard child.value == nil else { print("warning: inserted duplicate tokens into Trie.") return } - + child.value = newValue.value } } - + func insert(_ keypath: [UInt8], value: ValueType) { insert(value, for: keypath) } - + func insert(_ value: ValueType, for keypath: [UInt8]) { var current = self - + for (index, key) in keypath.enumerated() { guard let next = current[key] else { let next = Trie(key: key) current[key] = next current = next - + if index == keypath.endIndex - 1 { next.value = value } - + continue } - + if index == keypath.endIndex - 1 && next.value == nil { next.value = value } - + current = next } } - + func contains(_ keypath: [UInt8]) -> ValueType? { var current = self - + for key in keypath { guard let next = current[key] else { return nil } current = next } - + return current.value } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 999ddc3..afd7610 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,8 +2,8 @@ import XCTest @testable import StorageTests XCTMain([ - testCase(StorageTests.allTests), testCase(FileEntityTests.allTests), testCase(TemplateTests.allTests), - testCase(PathBuilderTests.allTests) + testCase(PathBuilderTests.allTests), + testCase(AWSSignerTestSuite.allTests) ]) diff --git a/Tests/StorageTests/AWSSignerTestSuite.swift b/Tests/StorageTests/AWSSignerTestSuite.swift new file mode 100644 index 0000000..c48d271 --- /dev/null +++ b/Tests/StorageTests/AWSSignerTestSuite.swift @@ -0,0 +1,323 @@ +/** + All tests are based off of Amazon's Signature Test Suite + See: http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html + + They also include the [`x-amz-content-sha256` header](http://docs.aws.amazon.com/AmazonS3/latest/API/bucket-policy-s3-sigv4-conditions.html). + */ + +import XCTest + +import HTTP +import Foundation + +@testable import Storage + +class AWSSignerTestSuite: XCTestCase { + static var allTests = [ + ("testGetUnreserved", testGetUnreserved), + ("testGetUTF8", testGetUTF8), + ("testGetVanilla", testGetVanilla), + ("testGetVanillaQuery", testGetVanillaQuery), + ("testGetVanillaEmptyQueryKey", testGetVanillaEmptyQueryKey), + ("testGetVanillaQueryUnreserved", testGetVanillaQueryUnreserved), + ("testGetVanillaQueryUTF8", testGetVanillaQueryUTF8), + ("testPostVanilla", testPostVanilla), + ("testPostVanillaQuery", testPostVanillaQuery), + ("testPostVanillaQueryNonunreserved", testPostVanillaQueryNonunreserved) + ] + + static let dateFormatter: DateFormatter = { + let _dateFormatter = DateFormatter() + _dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + _dateFormatter.dateFormat = "YYYYMMdd'T'HHmmss'Z'" + return _dateFormatter + }() + + func testGetUnreserved() { + let expectedCanonicalRequest = "GET\n/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=feae8f2b49f6807d4ca43941e2d6c7aacaca499df09935d14e97eed7647da5dc" + ] + + let result = sign( + method: .get, + path: "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + ) + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testGetUTF8() { + let expectedCanonicalRequest = "GET\n/%E1%88%B4\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=29d69532444b4f32a4c1b19af2afc116589685058ece54d8e43f0be05aeff6c0" + ] + + let result = sign(method: .get, path: "/ሴ") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testGetVanilla() { + let expectedCanonicalRequest = "GET\n/\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=726c5c4879a6b4ccbbd3b24edbd6b8826d34f87450fbbf4e85546fc7ba9c1642" + ] + + let result = sign(method: .get, path: "/") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + //duplicate as `testGetVanilla`, but is in Amazon Test Suite + //will keep until I figure out why there's a duplicate test + func testGetVanillaQuery() { + let expectedCanonicalRequest = "GET\n/\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=726c5c4879a6b4ccbbd3b24edbd6b8826d34f87450fbbf4e85546fc7ba9c1642" + ] + + let result = sign(method: .get, path: "/") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testGetVanillaEmptyQueryKey() { + let expectedCanonicalRequest = "GET\n/\nParam1=value1\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2287c0f96af21b7ccf3ee4a2905bcbb2d6f9a94c68d0849f3d1715ef003f2a05" + ] + + let result = sign(method: .get, path: "/", query: "Param1=value1") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testGetVanillaQueryUnreserved() { + let expectedCanonicalRequest = "GET\n/\n-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=e86fe49a4c0dda9163bed3b1b40d530d872eb612e2c366de300bfefdf356fd6a" + ] + + let result = sign( + method: .get, + path: "/", + query:"-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + ) + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testGetVanillaQueryUTF8() { + let expectedCanonicalRequest = "GET\n/\n%E1%88%B4=bar\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=6753d65781ac8f6964cb6fb90445ee138d65d9663df21f28f478bd09add64fd8" + ] + + let result = sign(method: .get, path: "/", query: "ሴ=bar") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testPostVanilla() { + let expectedCanonicalRequest = "POST\n/\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=3ad5e249949a59b862eedd9f1bf1ece4693c3042bf860ef5e3351b8925316f98" + ] + + let result = sign(method: .post, path: "/") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + func testPostVanillaQuery() { + let expectedCanonicalRequest = "POST\n/\nParam1=value1\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=d43fd95e1dfefe02247ce8858649e1a063f9dd10f25f170f7ebda6ee3e9b6fbc" + ] + + let result = sign(method: .post, path: "/", query: "Param1=value1") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + } + + /** + This test isn't based on the test suite, but tracks handling of special characters. + */ + func testPostVanillaQueryNonunreserved() { + let expectedCanonicalRequest = "POST\n/\n%40%23%24%25%5E&%2B=%2F%2C%3F%3E%3C%60%22%3B%3A%5C%7C%5D%5B%7B%7D\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" + + let expectedCanonicalHeaders: [String : String] = [ + "X-Amz-Date": "20150830T123600Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=3db24d76713a5ccb9afe4a26acb83ae4cfa3e67d9e10f165bdf99bda199c625d" + ] + + let result = sign(method: .post, path: "/", query: "@#$%^&+=/,?><`\";:\\|][{}") + result.expect( + canonicalRequest: expectedCanonicalRequest, + credentialScope: expectedCredentialScope, + canonicalHeaders: expectedCanonicalHeaders + ) + + } +} + +extension AWSSignerTestSuite { + var testDate: Date { + return AWSSignerTestSuite.dateFormatter.date(from: "20150830T123600Z")! + } + + + /** + Preparation of data to sign a canonical request. + + Intended to handle the preparation in the AWSSignatureV4's `sign` function + + - returns: + Hash value and multiple versions of headers + + - parameters: + - auth: Signature struct to use for calculations + - host: Hostname to sign for + */ + func prepCanonicalRequest(auth: AWSSignatureV4, host: String) -> (String, String, String) { + let payloadHash = try! Payload.none.hashed() + var headers = [String:String]() + auth.generateHeadersToSign(headers: &headers, host: host, hash: payloadHash) + + let sortedHeaders = auth.alphabetize(headers) + let signedHeaders = sortedHeaders.map { $0.key.lowercased() }.joined(separator: ";") + let canonicalHeaders = auth.createCanonicalHeaders(sortedHeaders) + return (payloadHash, signedHeaders, canonicalHeaders) + } + + func sign( + method: AWSSignatureV4.Method, + path: String, + query: String = "" + ) -> SignerResult { + let host = "example.amazonaws.com" + var auth = AWSSignatureV4( + service: "service", + host: host, + region: .usEast1, + accessKey: "AKIDEXAMPLE", + secretKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" + ) + + auth.unitTestDate = testDate + let (payloadHash, signedHeaders, preppedCanonicalHeaders) = prepCanonicalRequest(auth: auth, host: host) + let canonicalRequest = try! auth.getCanonicalRequest(payloadHash: payloadHash, method: method, path: path, query: query, canonicalHeaders: preppedCanonicalHeaders, signedHeaders: signedHeaders) + + + let credentialScope = auth.getCredentialScope() + + //FIXME(Brett): handle throwing + let canonicalHeaders = try! auth.sign( + payload: .none, + method: method, + path: path, + query: query + ) + + return SignerResult( + canonicalRequest: canonicalRequest, + credentialScope: credentialScope, + canonicalHeaders: canonicalHeaders + ) + } +} + +struct SignerResult { + let canonicalRequest: String + let credentialScope: String + let canonicalHeaders: [String: String] +} + +extension SignerResult { + func expect( + canonicalRequest: String, + credentialScope: String, + canonicalHeaders: [String: String], + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertEqual(self.canonicalRequest, canonicalRequest, file: file, line: line) + XCTAssertEqual(self.credentialScope, credentialScope, file: file, line: line) + + canonicalHeaders.forEach { + if $0.key == "Authorization" { + for (givenLine, expectedLine) in zip(self.canonicalHeaders[$0.key]!.components(separatedBy: " "), $0.value.components(separatedBy: " ")) { + XCTAssertEqual(givenLine, expectedLine) + } + } else { + XCTAssertEqual(self.canonicalHeaders[$0.key], $0.value, file: file, line: line) + } + } + } +} diff --git a/Tests/StorageTests/StorageTests.swift b/Tests/StorageTests/StorageTests.swift deleted file mode 100644 index 2ba2ea9..0000000 --- a/Tests/StorageTests/StorageTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XCTest -@testable import Storage - -class StorageTests: XCTestCase { - static var allTests = [ - ("testExample", testExample) - ] - - func testExample() { - XCTAssertEqual(2+2, 4) - } -} diff --git a/Tests/StorageTests/Utilities/Expect.swift b/Tests/StorageTests/Utilities/Expect.swift index 6a915aa..34dc1a9 100644 --- a/Tests/StorageTests/Utilities/Expect.swift +++ b/Tests/StorageTests/Utilities/Expect.swift @@ -4,7 +4,7 @@ func expect( toThrow expectedError: E, file: StaticString = #file, line: UInt = #line, - from closure: (Void) throws -> ReturnType + from closure: () throws -> ReturnType ) where E: Equatable { do { let _ = try closure() @@ -23,7 +23,7 @@ func expect( func expectNoThrow( file: StaticString = #file, line: UInt = #line, - _ closure: (Void) throws -> ReturnType + _ closure: () throws -> ReturnType ) { do { let _ = try closure() @@ -33,7 +33,7 @@ func expectNoThrow( } func expect( - _ closure: (Void) throws -> ReturnType, + _ closure: () throws -> ReturnType, file: StaticString = #file, line: UInt = #line, toReturn expectedResult: ReturnType