Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dinsen committed Feb 15, 2020
0 parents commit 1ab4f6d
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
/.build
Package.resolved
.swiftpm
/*.xcodeproj
xcuserdata/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 28 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"]),
]
)
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# FTPPublishDeploy

![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg)
<a href="https://swift.org/package-manager">
<img src="https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg?style=flat" alt="Swift Package Manager" />
</a>
![Mac & Linux](https://img.shields.io/badge/platforms-mac+linux-brightgreen.svg?style=flat)
<a href="https://github.com/JohnSundell/Publish">
<img src="https://img.shields.io/badge/Publish-Deploy-orange.svg?style=flat" alt="Publish Deploy" />
</a>
<a href="https://twitter.com/BrianDinsen">
<img src="https://img.shields.io/badge/[email protected]?style=flat" alt="Twitter: @BrianDinsen" />
</a>

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
60 changes: 60 additions & 0 deletions Sources/FTPPublishDeploy/API/FTPConnection.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
48 changes: 48 additions & 0 deletions Sources/FTPPublishDeploy/API/FTPPublishDeploy.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
74 changes: 74 additions & 0 deletions Sources/FTPPublishDeploy/Internal/FTP.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* FTPPublishDeploy
* Copyright (c) Brian Dinsen 2020
* MIT license, see LICENSE file for details
*/

import Publish
import ShellOut

struct FTP<Site: Website> {
let connection: FTPConnection
let context: PublishingContext<Site>
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
}
}
}
31 changes: 31 additions & 0 deletions Sources/FTPPublishDeploy/Internal/FTPError.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

23 changes: 23 additions & 0 deletions Sources/FTPPublishDeploy/Internal/File+FTPConnection.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 1ab4f6d

Please sign in to comment.