From 1ab4f6d7eff3256b44f69e3733fd1e747329120a Mon Sep 17 00:00:00 2001 From: Dinsen Date: Sat, 15 Feb 2020 11:08:19 +0100 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ LICENSE | 21 ++++ Package.swift | 28 ++++++ README.md | 95 +++++++++++++++++++ .../FTPPublishDeploy/API/FTPConnection.swift | 60 ++++++++++++ .../API/FTPPublishDeploy.swift | 48 ++++++++++ Sources/FTPPublishDeploy/Internal/FTP.swift | 74 +++++++++++++++ .../FTPPublishDeploy/Internal/FTPError.swift | 31 ++++++ .../Internal/File+FTPConnection.swift | 23 +++++ .../FTPPublishDeployTests.swift | 7 ++ .../XCTestManifests.swift | 9 ++ Tests/LinuxMain.swift | 7 ++ 12 files changed, 409 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/FTPPublishDeploy/API/FTPConnection.swift create mode 100644 Sources/FTPPublishDeploy/API/FTPPublishDeploy.swift create mode 100644 Sources/FTPPublishDeploy/Internal/FTP.swift create mode 100644 Sources/FTPPublishDeploy/Internal/FTPError.swift create mode 100644 Sources/FTPPublishDeploy/Internal/File+FTPConnection.swift create mode 100644 Tests/FTPPublishDeployTests/FTPPublishDeployTests.swift create mode 100644 Tests/FTPPublishDeployTests/XCTestManifests.swift create mode 100644 Tests/LinuxMain.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..111c2a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +Package.resolved +.swiftpm +/*.xcodeproj +xcuserdata/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..91a8975 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Brian Dinsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..deb27bb --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.1 + +/** +* FTPPublishDeploy +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +*/ + +import PackageDescription + +let package = Package( + name: "FTPPublishDeploy", + products: [ + .library( + name: "FTPPublishDeploy", + targets: ["FTPPublishDeploy"]), + ], + dependencies: [ + .package(url: "https://github.com/johnsundell/files.git", from: "4.0.0"), + .package(url: "https://github.com/johnsundell/publish.git", from: "0.5.0"), + .package(url: "https://github.com/johnsundell/shellout.git", from: "2.3.0"), + ], + targets: [ + .target( + name: "FTPPublishDeploy", + dependencies: ["Files", "Publish", "ShellOut"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d101769 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# FTPPublishDeploy + +![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg) + + Swift Package Manager + +![Mac & Linux](https://img.shields.io/badge/platforms-mac+linux-brightgreen.svg?style=flat) + + Publish Deploy + + + Twitter: @BrianDinsen + + +A deployment method for [Publish](https://github.com/johnsundell/publish) to upload files using FTP. + +## Installation + +Add FTPPublishDeploy to your `Package.swift` file. + +```swift +let package = Package( + ... + dependencies: [ + .package(url: "https://github.com/dinsen/ftppublishdeploy", from: "0.1.0") + ], + targets: [ + .target( + ... + dependencies: [ + ... + "FTPPublishDeploy" + ] + ) + ] + ... +) +``` + +## Usage + +There is 3 ways to declare your connection information. + +1. Declare it directly into the `FTPConnection` struct: + +```swift +let ftpConnection = FTPConnection(username: "batman", + password: "robin", + host: "my.host.com", + port: 21) +``` +2. Create a JSON file called `ftp.json` in the root of your project: +```json +{ + "username": "batman", + "password": "robin", + "host": "my.host.com", + "port": 21 +} +``` +*Remember to add the file to .gitignore to prevent your connection information being exposed!* + +Then use Files to locate the file: +```swift +import Files +... +let file = try File(path: #file) +guard let ftpConnection = try FTPConnection(file: file) else { + throw FilesError(path: file.path, reason: LocationErrorReason.missing) +} +``` +3. Use environment declared in your CI service like [Bitrise](https://www.bitrise.io) +```swift +let environment = ProcessInfo.processInfo.environment +guard let ftpConnection = try FTPConnection(environment: environment) else { + return +} +``` + +Last you can then declare your deployment method in your pipeline: +```swift +import FTPPublishDeploy +... +try Website().publish(using: [ + ... + .deploy(using: .ftp(connection: ftpConnection, sourcePath: "public_html/brian")) +]) +``` + +## Acknowledgement + +Thanks to John Sundell (@johnsundell) for creating [Publish](https://github.com/johnsundell/publish) + +## License +MIT License diff --git a/Sources/FTPPublishDeploy/API/FTPConnection.swift b/Sources/FTPPublishDeploy/API/FTPConnection.swift new file mode 100644 index 0000000..870c9c4 --- /dev/null +++ b/Sources/FTPPublishDeploy/API/FTPConnection.swift @@ -0,0 +1,60 @@ +/** +* FTPPublishDeploy +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +*/ + +import Files +import Foundation + +/// Type used to configure an FTP connection. +public struct FTPConnection: Codable { + public let username: String + public let password: String + public let host: String + public let port: Int + + /// Initialize a new connection instance. + /// - Parameter username: The username for login. + /// - Parameter password: The password for login. + /// - Parameter host: The address to etablish connection for. + /// - Parameter port: The port the connection should use. + public init(username: String, + password: String, + host: String, + port: Int) { + + self.username = username + self.password = password + self.host = host + self.port = port + } + + /// Initialize a new connection instance. + /// - Parameter file: The file containing credentials. + public init?(file: File) throws { + let connectionJson = try file.resolveFTPConnection() + let data = try connectionJson.read() + let json = try data.decoded() as Self + + self.init(username: json.username, + password: json.password, + host: json.host, + port: json.port) + } + + /// Initialize a new connection instance. + /// - Parameter environment: The processinfo environment. + public init?(environment: [String : String]) throws { + guard let ftpUsername = environment["FTP_USERNAME"] else { throw FTPError.missingUsername } + guard let ftpPassword = environment["FTP_PASSWORD"] else { throw FTPError.missingPassword } + guard let ftpHost = environment["FTP_HOST"] else { throw FTPError.missingHost } + guard let port = environment["FTP_PORT"], + let ftpPort = Int(port) else { throw FTPError.missingPort } + + self.init(username: ftpUsername, + password: ftpPassword, + host: ftpHost, + port: ftpPort) + } +} diff --git a/Sources/FTPPublishDeploy/API/FTPPublishDeploy.swift b/Sources/FTPPublishDeploy/API/FTPPublishDeploy.swift new file mode 100644 index 0000000..12953c5 --- /dev/null +++ b/Sources/FTPPublishDeploy/API/FTPPublishDeploy.swift @@ -0,0 +1,48 @@ +/** +* FTPPublishDeploy +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +*/ + +import Publish +import ShellOut + +public extension DeploymentMethod { + /// Deploy a website to a given FTP host. + /// - parameter connection: The connection information. + /// - parameter sourcePath: The path on the host where the site will be uploaded. + /// - parameter useSSL: Whether an SSL connection should be used (preferred). + static func ftp(connection: FTPConnection, sourcePath: String, useSSL: Bool = true) -> Self { + Self(name: "FTP") { context in + let deploymentFolder = try context.createDeploymentFolder(withPrefix: "FTP-", configure: { _ in }) + + // Run through files in subfolders + try deploymentFolder.subfolders.recursive.forEach { folder in + let folderPath = folder.path(relativeTo: deploymentFolder) + + try folder.files.forEach { file in + let ftp = FTP(connection: connection, + context: context, + path: file.path, + sourcePath: sourcePath, + subfolderPath: folderPath, + useSSL: useSSL) + + try ftp.uploadFile() + } + } + + // Run through files in root folder + try deploymentFolder.files.forEach { file in + let ftp = FTP(connection: connection, + context: context, + path: file.path, + sourcePath: sourcePath, + subfolderPath: nil, + useSSL: useSSL) + + try ftp.uploadFile() + } + } + } +} diff --git a/Sources/FTPPublishDeploy/Internal/FTP.swift b/Sources/FTPPublishDeploy/Internal/FTP.swift new file mode 100644 index 0000000..5317221 --- /dev/null +++ b/Sources/FTPPublishDeploy/Internal/FTP.swift @@ -0,0 +1,74 @@ +/** +* FTPPublishDeploy +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +*/ + +import Publish +import ShellOut + +struct FTP { + let connection: FTPConnection + let context: PublishingContext + let path: String + let sourcePath: String + let subfolderPath: String? + let useSSL: Bool + + func uploadFile() throws { + if let subfolderPath = self.subfolderPath { + try uploadSubFile(filePath: self.path, + sourcePath: self.sourcePath, + subfolderPath: subfolderPath, + useSSL: self.useSSL) + } else { + try uploadRootFile(filePath: self.path, + sourcePath: self.sourcePath, + useSSL: self.useSSL) + } + } +} + +private extension FTP { + func uploadSubFile(filePath: String, + sourcePath: String, + subfolderPath: String, + useSSL: Bool = true) throws { + let usingSSL = useSSL ? "--ftp-ssl" : "" + do { + try shellOut( + to: """ + curl --ftp-ssl -T \(filePath) \ + \(usingSSL) \ + -u \(connection.username):\(connection.password) \ + --ftp-create-dirs \ + ftp://\(connection.host):\(connection.port)/\(sourcePath)/\(subfolderPath)/ + """ + ) + } catch let error as ShellOutError { + throw PublishingError(infoMessage: error.message) + } catch { + throw error + } + } + + func uploadRootFile(filePath: String, + sourcePath: String, + useSSL: Bool = true) throws { + let usingSSL = useSSL ? "--ftp-ssl" : "" + do { + try shellOut( + to: """ + curl --ftp-ssl -T \(filePath) \ + \(usingSSL) \ + -u \(connection.username):\(connection.password) \ + ftp://\(connection.host):\(connection.port)/\(sourcePath)/ + """ + ) + } catch let error as ShellOutError { + throw PublishingError(infoMessage: error.message) + } catch { + throw error + } + } +} diff --git a/Sources/FTPPublishDeploy/Internal/FTPError.swift b/Sources/FTPPublishDeploy/Internal/FTPError.swift new file mode 100644 index 0000000..6e4d167 --- /dev/null +++ b/Sources/FTPPublishDeploy/Internal/FTPError.swift @@ -0,0 +1,31 @@ +/** +* FTPPublishDeploy +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +*/ + +internal enum FTPError: Error { + case missingFile + case missingHost + case missingPassword + case missingPort + case missingUsername +} + +extension FTPError: CustomStringConvertible { + var description: String { + switch self { + case .missingFile: + return "Could not find ftp.json file" + case .missingHost: + return "Could not get host" + case .missingPassword: + return "Could not get password" + case .missingPort: + return "Could not get port" + case .missingUsername: + return "Could not get username" + } + } +} + diff --git a/Sources/FTPPublishDeploy/Internal/File+FTPConnection.swift b/Sources/FTPPublishDeploy/Internal/File+FTPConnection.swift new file mode 100644 index 0000000..ff05613 --- /dev/null +++ b/Sources/FTPPublishDeploy/Internal/File+FTPConnection.swift @@ -0,0 +1,23 @@ +/** +* FTPPublishDeploy +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +*/ + +import Files + +internal extension File { + func resolveFTPConnection() throws -> File { + var nextFolder = parent + + while let currentFolder = nextFolder { + + if currentFolder.containsFile(named: "ftp.json") { + return try currentFolder.file(named: "ftp.json") + } + + nextFolder = currentFolder.parent + } + throw FTPError.missingFile + } +} diff --git a/Tests/FTPPublishDeployTests/FTPPublishDeployTests.swift b/Tests/FTPPublishDeployTests/FTPPublishDeployTests.swift new file mode 100644 index 0000000..11b1d11 --- /dev/null +++ b/Tests/FTPPublishDeployTests/FTPPublishDeployTests.swift @@ -0,0 +1,7 @@ +import XCTest +@testable import FTPPublishDeploy + +final class FTPPublishDeployTests: XCTestCase { + + static var allTests = [] +} diff --git a/Tests/FTPPublishDeployTests/XCTestManifests.swift b/Tests/FTPPublishDeployTests/XCTestManifests.swift new file mode 100644 index 0000000..2d2e3b6 --- /dev/null +++ b/Tests/FTPPublishDeployTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(FTPPublishDeployTests.allTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..6c7e1a9 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import FTPPublishDeployTests + +var tests = [XCTestCaseEntry]() +tests += FTPPublishDeployTests.allTests() +XCTMain(tests)