Skip to content

Allow cancellation for network requests #30

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions SwiftIpfsApi.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
1F47CB9E244988B9006F251C /* CancellableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F47CB9D244988B9006F251C /* CancellableRequest.swift */; };
1F47CBA024498C67006F251C /* CancellableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */; };
AB0A3AB81BD6705B0090C97A /* SwiftIpfsApi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB0A3AAD1BD6705B0090C97A /* SwiftIpfsApi.framework */; };
AB0A3ABD1BD6705B0090C97A /* SwiftIpfsApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0A3ABC1BD6705B0090C97A /* SwiftIpfsApiTests.swift */; };
AB0A3AC81BD671320090C97A /* MerkleNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0A3AC71BD671320090C97A /* MerkleNode.swift */; };
Expand Down Expand Up @@ -43,6 +45,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
1F47CB9D244988B9006F251C /* CancellableRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableRequest.swift; sourceTree = "<group>"; };
1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableRequestTests.swift; sourceTree = "<group>"; };
AB0A3AAD1BD6705B0090C97A /* SwiftIpfsApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftIpfsApi.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AB0A3AB21BD6705B0090C97A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AB0A3AB71BD6705B0090C97A /* SwiftIpfsApiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftIpfsApiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -120,6 +124,7 @@
AB0A3AB21BD6705B0090C97A /* Info.plist */,
AB0A3AC71BD671320090C97A /* MerkleNode.swift */,
ABEDF91D1BF0BE58007A1B2B /* JsonType.swift */,
1F47CB9D244988B9006F251C /* CancellableRequest.swift */,
AB490F8B1BF232F3005C5F57 /* Subcommands */,
);
path = SwiftIpfsApi;
Expand All @@ -129,6 +134,7 @@
isa = PBXGroup;
children = (
AB0A3ABC1BD6705B0090C97A /* SwiftIpfsApiTests.swift */,
1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */,
AB0A3ABE1BD6705B0090C97A /* Info.plist */,
);
path = SwiftIpfsApiTests;
Expand Down Expand Up @@ -302,6 +308,7 @@
AB490F8A1BF232D4005C5F57 /* Refs.swift in Sources */,
ABEDF91E1BF0BE58007A1B2B /* JsonType.swift in Sources */,
AB490F9F1BF23516005C5F57 /* Config.swift in Sources */,
1F47CB9E244988B9006F251C /* CancellableRequest.swift in Sources */,
AB490F9D1BF234F1005C5F57 /* Diag.swift in Sources */,
AB2C9EE01BD79F9B00BCCF01 /* HttpIo.swift in Sources */,
AB490F881BF231F7005C5F57 /* Pin.swift in Sources */,
Expand All @@ -324,6 +331,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1F47CBA024498C67006F251C /* CancellableRequestTests.swift in Sources */,
AB0A3ABD1BD6705B0090C97A /* SwiftIpfsApiTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
28 changes: 28 additions & 0 deletions SwiftIpfsApi/CancellableRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// CancellableRequest.swift
// SwiftIpfsApi
//
// Created by Marcel Voß on 17.04.20.
// Copyright © 2020 Teo Sartori. All rights reserved.
//

import Foundation

/// A protocol that represents any network request that can be cancelled during execution.
public protocol CancellableRequest {
func cancel()
}

/// A concrete type conforming to the `CancellableRequest` protocol that can be used for abstracting inner implementation
/// details of the networking layer away and not leaking this kind of information to any integrators.
struct CancellableDataTask: CancellableRequest {
private let request: URLSessionDataTask

init(request: URLSessionDataTask) {
self.request = request
}

func cancel() {
request.cancel()
}
}
18 changes: 11 additions & 7 deletions SwiftIpfsApi/HttpIo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ enum HttpIoError : Error {

public struct HttpIo : NetworkIo {

public func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws {
public func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws -> CancellableRequest {
guard let url = URL(string: source) else { throw HttpIoError.urlError("Invalid URL") }
print("HttpIo receiveFrom url is \(url)")

let task = URLSession.shared.dataTask(with: url) {
(data: Data?, response: URLResponse?, error: Error?) in

Expand All @@ -35,14 +36,16 @@ public struct HttpIo : NetworkIo {
print("Error ", error, "in completionHandler passed to fetchData ")
}
}

task.resume()

return CancellableDataTask(request: task)
}


public func streamFrom( _ source: String,
updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool,
completionHandler: @escaping (AnyObject) throws -> Void) throws {
completionHandler: @escaping (AnyObject) throws -> Void) throws -> CancellableRequest {

guard let url = URL(string: source) else { throw HttpIoError.urlError("Invalid URL") }
let config = URLSessionConfiguration.default
Expand All @@ -51,23 +54,24 @@ public struct HttpIo : NetworkIo {
let task = session.dataTask(with: url)

task.resume()
return CancellableDataTask(request: task)
}

public func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws {
public func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest {

var multipart = try Multipart(targetUrl: target, encoding: .utf8)
multipart = try Multipart.addFilePart(multipart, fileName: nil , fileData: content)
Multipart.finishMultipart(multipart, completionHandler: completionHandler)
return Multipart.finishMultipart(multipart, completionHandler: completionHandler)
}


public func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws {
public func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest {

var multipart = try Multipart(targetUrl: target, encoding: .utf8)

multipart = try handle(oldMultipart: multipart, files: [filePath])

Multipart.finishMultipart(multipart, completionHandler: completionHandler)
return Multipart.finishMultipart(multipart, completionHandler: completionHandler)

}

Expand Down
95 changes: 53 additions & 42 deletions SwiftIpfsApi/IpfsApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension IpfsApiClient {

func fetchStreamJson( _ path: String,
updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool,
completionHandler: @escaping (AnyObject) throws -> Void) throws {
completionHandler: @escaping (AnyObject) throws -> Void) throws -> CancellableRequest {
/// We need to use the passed in completionHandler
try net.streamFrom(baseUrl + path, updateHandler: updateHandler, completionHandler: completionHandler)
}
Expand All @@ -69,9 +69,10 @@ extension IpfsApiClient {
// }
// stream.close()


func fetchJson(_ path: String, completionHandler: @escaping (JsonType) throws -> Void) throws {
try fetchData(path) {

@discardableResult
func fetchJson(_ path: String, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest {
return try fetchData(path) {
(data: Data) in

/// If there was no data fetched pass an empty dictionary and return.
Expand Down Expand Up @@ -99,13 +100,14 @@ extension IpfsApiClient {
try completionHandler(JsonType.parse(json as AnyObject))
}
}
func fetchData(_ path: String, completionHandler: @escaping (Data) throws -> Void) throws {

@discardableResult
func fetchData(_ path: String, completionHandler: @escaping (Data) throws -> Void) throws -> CancellableRequest {
try net.receiveFrom(baseUrl + path, completionHandler: completionHandler)
}

func fetchBytes(_ path: String, completionHandler: @escaping ([UInt8]) throws -> Void) throws {

@discardableResult
func fetchBytes(_ path: String, completionHandler: @escaping ([UInt8]) throws -> Void) throws -> CancellableRequest {
try fetchData(path) {
(data: Data) in

Expand Down Expand Up @@ -239,9 +241,9 @@ public class IpfsApi : IpfsApiClient {


/// base commands
public func add(_ filePath: String, completionHandler: @escaping ([MerkleNode]) -> Void) throws {

@discardableResult
public func add(_ filePath: String, completionHandler: @escaping ([MerkleNode]) -> Void) throws -> CancellableRequest {
try net.sendTo(baseUrl+"add?s", filePath: filePath) {
data in
do {
Expand All @@ -265,9 +267,9 @@ public class IpfsApi : IpfsApiClient {
}

// Store binary data
public func add(_ fileData: Data, completionHandler: @escaping ([MerkleNode]) -> Void) throws {

@discardableResult
public func add(_ fileData: Data, completionHandler: @escaping ([MerkleNode]) -> Void) throws -> CancellableRequest {
try net.sendTo(baseUrl+"add?stream-channels=true", content: fileData) {
data in
do {
Expand All @@ -291,9 +293,9 @@ public class IpfsApi : IpfsApiClient {
}
}
}
public func ls(_ hash: Multihash, completionHandler: @escaping ([MerkleNode]) -> Void) throws {

@discardableResult
public func ls(_ hash: Multihash, completionHandler: @escaping ([MerkleNode]) -> Void) throws -> CancellableRequest {
try fetchJson("ls/\(b58String(hash))") {
json in

Expand All @@ -310,17 +312,18 @@ public class IpfsApi : IpfsApiClient {
}
}

public func cat(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws {
@discardableResult
public func cat(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws -> CancellableRequest {
try fetchBytes("cat/\(b58String(hash))", completionHandler: completionHandler)
}

public func get(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws {

@discardableResult
public func get(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws -> CancellableRequest {
try self.cat(hash, completionHandler: completionHandler)
}


public func refs(_ hash: Multihash, recursive: Bool, completionHandler: @escaping ([Multihash]) -> Void) throws {

@discardableResult
public func refs(_ hash: Multihash, recursive: Bool, completionHandler: @escaping ([Multihash]) -> Void) throws -> CancellableRequest {
try fetchJson("refs?arg=" + b58String(hash) + "&r=\(recursive)") {
result in
guard let results = result.array else { throw IpfsApiError.unexpectedReturnType }
Expand All @@ -338,20 +341,23 @@ public class IpfsApi : IpfsApiClient {
}
}

public func resolve(_ scheme: String, hash: Multihash, recursive: Bool, completionHandler: @escaping (JsonType) -> Void) throws {
@discardableResult
public func resolve(_ scheme: String, hash: Multihash, recursive: Bool, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
try fetchJson("resolve?arg=/\(scheme)/\(b58String(hash))&r=\(recursive)", completionHandler: completionHandler)
}

public func dns(_ domain: String, completionHandler: @escaping (String) -> Void) throws {

@discardableResult
public func dns(_ domain: String, completionHandler: @escaping (String) -> Void) throws -> CancellableRequest {
try fetchJson("dns?arg=" + domain) {
result in

guard let path = result.object?[IpfsCmdString.Path.rawValue]?.string else { throw IpfsApiError.resultMissingData("No Path found") }
completionHandler(path)
}
}

public func mount(_ ipfsRootPath: String = "/ipfs", ipnsRootPath: String = "/ipns", completionHandler: @escaping (JsonType) -> Void) throws {

@discardableResult
public func mount(_ ipfsRootPath: String = "/ipfs", ipnsRootPath: String = "/ipns", completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {

let fileManager = FileManager.default

Expand All @@ -363,25 +369,27 @@ public class IpfsApi : IpfsApiClient {
try fileManager.createDirectory(atPath: ipnsRootPath, withIntermediateDirectories: false, attributes: nil)
}

try fetchJson("mount?arg=" + ipfsRootPath + "&arg=" + ipnsRootPath, completionHandler: completionHandler)
return try fetchJson("mount?arg=" + ipfsRootPath + "&arg=" + ipnsRootPath, completionHandler: completionHandler)
}

/** ping is a tool to test sending data to other nodes.
It finds nodes via the routing system, send pings, wait for pongs,
and prints out round- trip latency information. */
public func ping(_ target: String, completionHandler: @escaping (JsonType) -> Void) throws {
@discardableResult
public func ping(_ target: String, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
try fetchJson("ping/" + target, completionHandler: completionHandler)
}


public func id(_ target: String? = nil, completionHandler: @escaping (JsonType) -> Void) throws {
@discardableResult
public func id(_ target: String? = nil, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
var request = "id"
if target != nil { request += "/\(target!)" }

try fetchJson(request, completionHandler: completionHandler)
return try fetchJson(request, completionHandler: completionHandler)
}

public func version(_ completionHandler: @escaping (String) -> Void) throws {

@discardableResult
public func version(_ completionHandler: @escaping (String) -> Void) throws -> CancellableRequest {
try fetchJson("version") {
json in
let version = json.object?[IpfsCmdString.Version.rawValue]?.string ?? ""
Expand All @@ -390,19 +398,21 @@ public class IpfsApi : IpfsApiClient {
}

/** List all available commands. */
public func commands(_ showOptions: Bool = false, completionHandler: @escaping (JsonType) -> Void) throws {
@discardableResult
public func commands(_ showOptions: Bool = false, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {

var request = "commands" //+ (showOptions ? "?flags=true&" : "")
if showOptions { request += "?flags=true&" }

try fetchJson(request, completionHandler: completionHandler)
return try fetchJson(request, completionHandler: completionHandler)
}

/** This method should take both a completion handler and an update handler.
Since the log tail won't stop until interrupted, the update handler
should return false when it wants the updates to stop.
*/
public func log(_ updateHandler: (Data) throws -> Bool, completionHandler: @escaping ([[String : AnyObject]]) -> Void) throws {
@discardableResult
public func log(_ updateHandler: (Data) throws -> Bool, completionHandler: @escaping ([[String : AnyObject]]) -> Void) throws -> CancellableRequest {

/// Two test closures to be passed to the fetchStreamJson as parameters.
let comp = { (result: AnyObject) -> Void in
Expand All @@ -426,16 +436,17 @@ public class IpfsApi : IpfsApiClient {
return true
}

try fetchStreamJson("log/tail", updateHandler: update, completionHandler: comp)
return try fetchStreamJson("log/tail", updateHandler: update, completionHandler: comp)
}
}



/** Show or edit the list of bootstrap peers */
extension IpfsApiClient {

public func bootstrap(_ completionHandler: @escaping ([Multiaddr]) -> Void) throws {

@discardableResult
public func bootstrap(_ completionHandler: @escaping ([Multiaddr]) -> Void) throws -> CancellableRequest {
try bootstrap.list(completionHandler)
}
}
Expand Down
4 changes: 3 additions & 1 deletion SwiftIpfsApi/Multipart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ extension Multipart {
return oldMultipart
}

public static func finishMultipart(_ multipart: Multipart, completionHandler: @escaping (Data) -> Void) {
public static func finishMultipart(_ multipart: Multipart, completionHandler: @escaping (Data) -> Void) -> CancellableRequest {

let outString = "--" + multipart.boundary + "--" + lineFeed

Expand All @@ -167,5 +167,7 @@ extension Multipart {
}

task.resume()

return CancellableDataTask(request: task)
}
}
14 changes: 8 additions & 6 deletions SwiftIpfsApi/NetworkIo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import Foundation

public protocol NetworkIo {

func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws

func streamFrom(_ source: String, updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool, completionHandler: @escaping (AnyObject) throws -> Void) throws

func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws
func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws -> CancellableRequest

func streamFrom(_ source: String,
updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool,
completionHandler: @escaping (AnyObject) throws -> Void) throws -> CancellableRequest

func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest

/// If we want to send location addressed content
func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws
func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest
}
Loading