From d72284bec4e2df802bb2762e7f91f1736022a7e9 Mon Sep 17 00:00:00 2001 From: Marcel Voss Date: Fri, 17 Apr 2020 09:05:58 +0200 Subject: [PATCH] Allow cancellation for network requests --- SwiftIpfsApi.xcodeproj/project.pbxproj | 8 ++ SwiftIpfsApi/CancellableRequest.swift | 28 ++++++ SwiftIpfsApi/HttpIo.swift | 18 ++-- SwiftIpfsApi/IpfsApi.swift | 95 +++++++++++-------- SwiftIpfsApi/Multipart.swift | 4 +- SwiftIpfsApi/NetworkIo.swift | 14 +-- SwiftIpfsApi/Subcommands/Block.swift | 18 ++-- SwiftIpfsApi/Subcommands/Bootstrap.swift | 33 ++++--- SwiftIpfsApi/Subcommands/Config.swift | 22 +++-- SwiftIpfsApi/Subcommands/Dht.swift | 18 ++-- SwiftIpfsApi/Subcommands/Diag.swift | 6 +- SwiftIpfsApi/Subcommands/File.swift | 3 +- SwiftIpfsApi/Subcommands/IpfsObject.swift | 44 ++++----- SwiftIpfsApi/Subcommands/Name.swift | 26 ++--- SwiftIpfsApi/Subcommands/Pin.swift | 26 ++--- SwiftIpfsApi/Subcommands/Refs.swift | 5 +- SwiftIpfsApi/Subcommands/Repo.swift | 3 +- SwiftIpfsApi/Subcommands/Stats.swift | 3 +- SwiftIpfsApi/Subcommands/Swarm.swift | 15 +-- SwiftIpfsApi/Subcommands/Update.swift | 12 +-- .../CancellableRequestTests.swift | 35 +++++++ 21 files changed, 275 insertions(+), 161 deletions(-) create mode 100644 SwiftIpfsApi/CancellableRequest.swift create mode 100644 SwiftIpfsApiTests/CancellableRequestTests.swift diff --git a/SwiftIpfsApi.xcodeproj/project.pbxproj b/SwiftIpfsApi.xcodeproj/project.pbxproj index 2e4f7aa..729dd1c 100644 --- a/SwiftIpfsApi.xcodeproj/project.pbxproj +++ b/SwiftIpfsApi.xcodeproj/project.pbxproj @@ -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 */; }; @@ -43,6 +45,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1F47CB9D244988B9006F251C /* CancellableRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableRequest.swift; sourceTree = ""; }; + 1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableRequestTests.swift; sourceTree = ""; }; 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 = ""; }; AB0A3AB71BD6705B0090C97A /* SwiftIpfsApiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftIpfsApiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -120,6 +124,7 @@ AB0A3AB21BD6705B0090C97A /* Info.plist */, AB0A3AC71BD671320090C97A /* MerkleNode.swift */, ABEDF91D1BF0BE58007A1B2B /* JsonType.swift */, + 1F47CB9D244988B9006F251C /* CancellableRequest.swift */, AB490F8B1BF232F3005C5F57 /* Subcommands */, ); path = SwiftIpfsApi; @@ -129,6 +134,7 @@ isa = PBXGroup; children = ( AB0A3ABC1BD6705B0090C97A /* SwiftIpfsApiTests.swift */, + 1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */, AB0A3ABE1BD6705B0090C97A /* Info.plist */, ); path = SwiftIpfsApiTests; @@ -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 */, @@ -324,6 +331,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1F47CBA024498C67006F251C /* CancellableRequestTests.swift in Sources */, AB0A3ABD1BD6705B0090C97A /* SwiftIpfsApiTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SwiftIpfsApi/CancellableRequest.swift b/SwiftIpfsApi/CancellableRequest.swift new file mode 100644 index 0000000..560053a --- /dev/null +++ b/SwiftIpfsApi/CancellableRequest.swift @@ -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() + } +} diff --git a/SwiftIpfsApi/HttpIo.swift b/SwiftIpfsApi/HttpIo.swift index 39609c5..f985129 100644 --- a/SwiftIpfsApi/HttpIo.swift +++ b/SwiftIpfsApi/HttpIo.swift @@ -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 @@ -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 @@ -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) } diff --git a/SwiftIpfsApi/IpfsApi.swift b/SwiftIpfsApi/IpfsApi.swift index f886507..c2eda21 100644 --- a/SwiftIpfsApi/IpfsApi.swift +++ b/SwiftIpfsApi/IpfsApi.swift @@ -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) } @@ -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. @@ -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 @@ -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 { @@ -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 { @@ -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 @@ -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 } @@ -338,11 +341,13 @@ 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 @@ -350,8 +355,9 @@ public class IpfsApi : IpfsApiClient { 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 @@ -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 ?? "" @@ -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 @@ -426,7 +436,7 @@ public class IpfsApi : IpfsApiClient { return true } - try fetchStreamJson("log/tail", updateHandler: update, completionHandler: comp) + return try fetchStreamJson("log/tail", updateHandler: update, completionHandler: comp) } } @@ -434,8 +444,9 @@ public class IpfsApi : IpfsApiClient { /** 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) } } diff --git a/SwiftIpfsApi/Multipart.swift b/SwiftIpfsApi/Multipart.swift index edba7a6..4ddf07b 100644 --- a/SwiftIpfsApi/Multipart.swift +++ b/SwiftIpfsApi/Multipart.swift @@ -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 @@ -167,5 +167,7 @@ extension Multipart { } task.resume() + + return CancellableDataTask(request: task) } } diff --git a/SwiftIpfsApi/NetworkIo.swift b/SwiftIpfsApi/NetworkIo.swift index 18988dc..b26e8e0 100644 --- a/SwiftIpfsApi/NetworkIo.swift +++ b/SwiftIpfsApi/NetworkIo.swift @@ -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 } diff --git a/SwiftIpfsApi/Subcommands/Block.swift b/SwiftIpfsApi/Subcommands/Block.swift index a4a3376..c3eee7c 100644 --- a/SwiftIpfsApi/Subcommands/Block.swift +++ b/SwiftIpfsApi/Subcommands/Block.swift @@ -13,15 +13,17 @@ import Foundation public class Block : ClientSubCommand { var parent: IpfsApiClient? - - public func get(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws { + + @discardableResult + public func get(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws -> CancellableRequest { try parent!.fetchBytes("block/get?stream-channels=true&arg=\(b58String(hash))", completionHandler: completionHandler) } - - public func put(_ data: [UInt8], completionHandler: @escaping (MerkleNode) -> Void) throws { + + @discardableResult + public func put(_ data: [UInt8], completionHandler: @escaping (MerkleNode) -> Void) throws -> CancellableRequest { let data2 = Data(bytes: UnsafePointer(data), count: data.count) - try parent!.net.sendTo(parent!.baseUrl+"block/put?stream-channels=true", content: data2) { + return try parent!.net.sendTo(parent!.baseUrl+"block/put?stream-channels=true", content: data2) { result in do { @@ -35,9 +37,9 @@ public class Block : ClientSubCommand { } } } - - public func stat(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { - + + @discardableResult + public func stat(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("block/stat?stream-channels=true&arg=" + b58String(hash), completionHandler: completionHandler) } } diff --git a/SwiftIpfsApi/Subcommands/Bootstrap.swift b/SwiftIpfsApi/Subcommands/Bootstrap.swift index 79ec4ad..8798e53 100644 --- a/SwiftIpfsApi/Subcommands/Bootstrap.swift +++ b/SwiftIpfsApi/Subcommands/Bootstrap.swift @@ -21,27 +21,26 @@ public class Bootstrap : ClientSubCommand { var parent: IpfsApiClient? - - public func list(_ completionHandler: @escaping ([Multiaddr]) throws -> Void) throws { - + @discardableResult + public func list(_ completionHandler: @escaping ([Multiaddr]) throws -> Void) throws -> CancellableRequest { try fetchPeers("bootstrap/", completionHandler: completionHandler) } - - public func add(_ addresses: [Multiaddr], completionHandler: @escaping ([Multiaddr]) throws -> Void) throws { - + + @discardableResult + public func add(_ addresses: [Multiaddr], completionHandler: @escaping ([Multiaddr]) throws -> Void) throws -> CancellableRequest { let multiaddresses = try addresses.map { try $0.string() } let request = "bootstrap/add?" + buildArgString(multiaddresses) - try fetchPeers(request, completionHandler: completionHandler) + return try fetchPeers(request, completionHandler: completionHandler) } - - public func rm(_ addresses: [Multiaddr], completionHandler: @escaping ([Multiaddr]) throws -> Void) throws { - + + @discardableResult + public func rm(_ addresses: [Multiaddr], completionHandler: @escaping ([Multiaddr]) throws -> Void) throws -> CancellableRequest { try self.rm(addresses, all: false, completionHandler: completionHandler) } - - public func rm(_ addresses: [Multiaddr], all: Bool, completionHandler: @escaping ([Multiaddr]) throws -> Void) throws { - + + @discardableResult + public func rm(_ addresses: [Multiaddr], all: Bool, completionHandler: @escaping ([Multiaddr]) throws -> Void) throws -> CancellableRequest { let multiaddresses = try addresses.map { try $0.string() } var request = "bootstrap/rm?" @@ -49,11 +48,11 @@ public class Bootstrap : ClientSubCommand { request += buildArgString(multiaddresses) - try fetchPeers(request, completionHandler: completionHandler) + return try fetchPeers(request, completionHandler: completionHandler) } - - private func fetchPeers(_ request: String, completionHandler: @escaping ([Multiaddr]) throws -> Void) throws { - + + @discardableResult + private func fetchPeers(_ request: String, completionHandler: @escaping ([Multiaddr]) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson(request) { result in diff --git a/SwiftIpfsApi/Subcommands/Config.swift b/SwiftIpfsApi/Subcommands/Config.swift index 05b3594..5f24a59 100644 --- a/SwiftIpfsApi/Subcommands/Config.swift +++ b/SwiftIpfsApi/Subcommands/Config.swift @@ -13,19 +13,21 @@ public class Config : ClientSubCommand { var parent: IpfsApiClient? - - public func show(_ completionHandler: @escaping (JsonType) -> Void) throws { - + + @discardableResult + public func show(_ completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest{ try parent!.fetchJson("config/show",completionHandler: completionHandler ) } - - public func replace(_ filePath: String, completionHandler: (Bool) -> Void) throws { + + @discardableResult + public func replace(_ filePath: String, completionHandler: (Bool) -> Void) throws -> CancellableRequest { try parent!.net.sendTo(parent!.baseUrl+"config/replace?stream-channels=true", filePath: filePath) { _ in } } - - public func get(_ key: String, completionHandler: @escaping (JsonType) throws -> Void) throws { + + @discardableResult + public func get(_ key: String, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("config?arg=" + key) { result in guard let value = result.object?[IpfsCmdString.Value.rawValue] else { @@ -36,9 +38,9 @@ public class Config : ClientSubCommand { } } - - public func set(_ key: String, value: String, completionHandler: @escaping (JsonType) throws -> Void) throws { - + + @discardableResult + public func set(_ key: String, value: String, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("config?arg=\(key)&arg=\(value)", completionHandler: completionHandler ) } } diff --git a/SwiftIpfsApi/Subcommands/Dht.swift b/SwiftIpfsApi/Subcommands/Dht.swift index 17d8b5c..74c2b6c 100644 --- a/SwiftIpfsApi/Subcommands/Dht.swift +++ b/SwiftIpfsApi/Subcommands/Dht.swift @@ -19,7 +19,9 @@ public class Dht : ClientSubCommand { // public func findProvs(_ hash: Multihash, numProviders: Int = 20, completionHandler: @escaping (JsonType) -> Void) throws { // try parent!.fetchJson("dht/findprovs?arg=\(b58String(hash))&num-providers=\(numProviders)", completionHandler: completionHandler) // } - public func findProvs(_ hash: Multihash, numProviders: Int = 20, completionHandler: @escaping (JsonType) -> Void) throws { + + @discardableResult + public func findProvs(_ hash: Multihash, numProviders: Int = 20, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { /// Two test closures to be passed to the fetchStreamJson as parameters. let comp = { (result: AnyObject) -> Void in print("Job done") @@ -99,26 +101,30 @@ public class Dht : ClientSubCommand { return true } - try parent!.fetchStreamJson("dht/findprovs?arg=\(b58String(hash))&num-providers=\(numProviders)", updateHandler: update, completionHandler: comp) + return try parent!.fetchStreamJson("dht/findprovs?arg=\(b58String(hash))&num-providers=\(numProviders)", updateHandler: update, completionHandler: comp) } /** Run a 'findClosestPeers' query through the DHT */ - public func query(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { + @discardableResult + public func query(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("dht/query?arg=" + b58String(hash) , completionHandler: completionHandler) } /** Run a 'FindPeer' query through the DHT */ - public func findpeer(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { + @discardableResult + public func findpeer(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("dht/findpeer?arg=" + b58String(hash), completionHandler: completionHandler) } /** Will return the value stored in the dht at the given key */ - public func get(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { + @discardableResult + public func get(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("dht/get?arg=" + b58String(hash), completionHandler: completionHandler) } /** Will store the given key value pair in the dht. */ - public func put(_ key: String, value: String, completionHandler: @escaping (JsonType) -> Void) throws { + @discardableResult + public func put(_ key: String, value: String, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("dht/put?arg=\(key)&arg=\(value)", completionHandler: completionHandler) } } diff --git a/SwiftIpfsApi/Subcommands/Diag.swift b/SwiftIpfsApi/Subcommands/Diag.swift index bf67a4f..db39927 100644 --- a/SwiftIpfsApi/Subcommands/Diag.swift +++ b/SwiftIpfsApi/Subcommands/Diag.swift @@ -14,7 +14,8 @@ public class Diag : ClientSubCommand { var parent: IpfsApiClient? /** Generates a network diagnostics report */ - public func net(_ completionHandler: @escaping (String) -> Void) throws { + @discardableResult + public func net(_ completionHandler: @escaping (String) -> Void) throws -> CancellableRequest { try parent!.fetchBytes("diag/net?stream-channels=true") { bytes in completionHandler(String(bytes: bytes, encoding: String.Encoding.utf8)!) @@ -22,7 +23,8 @@ public class Diag : ClientSubCommand { } /* Prints out system diagnostic information. */ - public func sys(_ completionHandler: @escaping (String) -> Void) throws { + @discardableResult + public func sys(_ completionHandler: @escaping (String) -> Void) throws -> CancellableRequest { try parent!.fetchBytes("diag/sys?stream-channels=true") { bytes in completionHandler(String(bytes: bytes, encoding: String.Encoding.utf8)!) diff --git a/SwiftIpfsApi/Subcommands/File.swift b/SwiftIpfsApi/Subcommands/File.swift index 92d5bbe..2b15d9f 100644 --- a/SwiftIpfsApi/Subcommands/File.swift +++ b/SwiftIpfsApi/Subcommands/File.swift @@ -20,7 +20,8 @@ public class File : ClientSubCommand { The JSON output contains size information. For files, the child size is the total size of the file contents. For directories, the child size is the IPFS link size. */ - public func ls(_ path: String, completionHandler: @escaping (JsonType) -> Void) throws { + @discardableResult + public func ls(_ path: String, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("file/ls?arg=" + path, completionHandler: completionHandler) } } diff --git a/SwiftIpfsApi/Subcommands/IpfsObject.swift b/SwiftIpfsApi/Subcommands/IpfsObject.swift index dadca63..04dcb48 100644 --- a/SwiftIpfsApi/Subcommands/IpfsObject.swift +++ b/SwiftIpfsApi/Subcommands/IpfsObject.swift @@ -34,11 +34,11 @@ public class IpfsObject : ClientSubCommand { Available templates: * unixfs-dir */ - public func new(_ template: ObjectTemplates? = nil, completionHandler: @escaping (MerkleNode) throws -> Void) throws { + @discardableResult + public func new(_ template: ObjectTemplates? = nil, completionHandler: @escaping (MerkleNode) throws -> Void) throws -> CancellableRequest { var request = "object/new?stream-channels=true" if template != nil { request += "&arg=\(template!.rawValue)" } - try parent!.fetchJson(request) { - result in + return try parent!.fetchJson(request) { result in try completionHandler( try merkleNodeFromJson2(result) ) } } @@ -46,10 +46,11 @@ public class IpfsObject : ClientSubCommand { /** IpfsObject put is a plumbing command for storing DAG nodes. Its input is a byte array, and the output is a base58 encoded multihash. */ - public func put(_ data: [UInt8], completionHandler: @escaping (MerkleNode) -> Void) throws { + @discardableResult + public func put(_ data: [UInt8], completionHandler: @escaping (MerkleNode) -> Void) throws -> CancellableRequest { let data2 = Data(bytes: UnsafePointer(data), count: data.count) - try parent!.net.sendTo(parent!.baseUrl+"object/put?stream-channels=true", content: data2) { + return try parent!.net.sendTo(parent!.baseUrl+"object/put?stream-channels=true", content: data2) { result in do { @@ -68,32 +69,30 @@ public class IpfsObject : ClientSubCommand { /** IpfsObject get is a plumbing command for retreiving DAG nodes. Its input is a base58 encoded Multihash and it returns a MerkleNode. */ - public func get(_ hash: Multihash, completionHandler: @escaping (MerkleNode) -> Void) throws { - - try parent!.fetchJson("object/get?stream-channels=true&arg=" + b58String(hash)){ - result in + @discardableResult + public func get(_ hash: Multihash, completionHandler: @escaping (MerkleNode) -> Void) throws -> CancellableRequest { + try parent!.fetchJson("object/get?stream-channels=true&arg=" + b58String(hash)){ result in guard var res = result.object else { throw IpfsApiError.resultMissingData("No object found!")} res["Hash"] = .String(b58String(hash)) completionHandler(try merkleNodeFromJson2(.Object(res))) } } - - public func links(_ hash: Multihash, completionHandler: @escaping (MerkleNode) throws -> Void) throws { - - try parent!.fetchJson("object/links?stream-channels=true&arg=" + b58String(hash)){ - result in + + @discardableResult + public func links(_ hash: Multihash, completionHandler: @escaping (MerkleNode) throws -> Void) throws -> CancellableRequest { + try parent!.fetchJson("object/links?stream-channels=true&arg=" + b58String(hash)) { result in try completionHandler(try merkleNodeFromJson2(result)) } } - - public func stat(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { - + + @discardableResult + public func stat(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("object/stat?stream-channels=true&arg=" + b58String(hash), completionHandler: completionHandler) } - - public func data(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws { - + + @discardableResult + public func data(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws -> CancellableRequest { try parent!.fetchBytes("object/data?stream-channels=true&arg=" + b58String(hash), completionHandler: completionHandler) } @@ -113,7 +112,8 @@ public class IpfsObject : ClientSubCommand { // } // } // change root to String ? - public func patch(_ root: Multihash, cmd: ObjectPatchCommand, args: String..., completionHandler: @escaping (MerkleNode) throws -> Void) throws { + @discardableResult + public func patch(_ root: Multihash, cmd: ObjectPatchCommand, args: String..., completionHandler: @escaping (MerkleNode) throws -> Void) throws -> CancellableRequest { var request: String = "object/patch" switch cmd { @@ -138,7 +138,7 @@ public class IpfsObject : ClientSubCommand { request += buildArgString(args) - try parent!.fetchJson(request) { + return try parent!.fetchJson(request) { result in try completionHandler(try merkleNodeFromJson2(result)) } diff --git a/SwiftIpfsApi/Subcommands/Name.swift b/SwiftIpfsApi/Subcommands/Name.swift index 5242d65..0b23549 100644 --- a/SwiftIpfsApi/Subcommands/Name.swift +++ b/SwiftIpfsApi/Subcommands/Name.swift @@ -24,19 +24,22 @@ public class Name : ClientSubCommand { case ttl case key } - - public func publish(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { - try self.publish(nil, hash: hash, completionHandler: completionHandler) + + @discardableResult + public func publish(_ hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { + return try self.publish(nil, hash: hash, completionHandler: completionHandler) } - - public func publish(_ id: String? = nil, hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws { + + @discardableResult + public func publish(_ id: String? = nil, hash: Multihash, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { var request = "name/publish?arg=" if id != nil { request += id! + "&arg=" } // try parent!.fetchJson(request + "/ipfs/" + b58String(hash), completionHandler: completionHandler) - try parent!.fetchJson(request + b58String(hash), completionHandler: completionHandler) + return try parent!.fetchJson(request + b58String(hash), completionHandler: completionHandler) } - - public func publish(ipfsPath: String, args: [NamePublishArgType : Any]? = nil, completionHandler: @escaping (JsonType) -> Void) throws { + + @discardableResult + public func publish(ipfsPath: String, args: [NamePublishArgType : Any]? = nil, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { // strip the prefix // let path = ipfsPath.replacingOccurrences(of: "/ipfs/", with: "") let path = ipfsPath.replacingOccurrences(of: "/", with: "%2F") @@ -48,15 +51,16 @@ public class Name : ClientSubCommand { request += "&lifetime=\(lifetime)&resolve=\(resolve)" - try parent!.fetchJson(request, completionHandler: completionHandler) + return try parent!.fetchJson(request, completionHandler: completionHandler) } - public func resolve(_ hash: Multihash? = nil, completionHandler: @escaping (String) -> Void) throws { + @discardableResult + public func resolve(_ hash: Multihash? = nil, completionHandler: @escaping (String) -> Void) throws -> CancellableRequest { var request = "name/resolve" if hash != nil { request += "?arg=" + b58String(hash!) } - try parent!.fetchJson(request) { + return try parent!.fetchJson(request) { result in let resolvedName = result.object?[IpfsCmdString.Path.rawValue]?.string ?? "" diff --git a/SwiftIpfsApi/Subcommands/Pin.swift b/SwiftIpfsApi/Subcommands/Pin.swift index 1a597e5..dafe4b2 100644 --- a/SwiftIpfsApi/Subcommands/Pin.swift +++ b/SwiftIpfsApi/Subcommands/Pin.swift @@ -13,9 +13,9 @@ import SwiftMultihash public class Pin : ClientSubCommand { var parent: IpfsApiClient? - - public func add(_ hash: Multihash, completionHandler: @escaping ([Multihash]) -> Void) throws { - + + @discardableResult + public func add(_ hash: Multihash, completionHandler: @escaping ([Multihash]) -> Void) throws -> CancellableRequest { try parent!.fetchJson("pin/add?stream-channels=true&arg=\(b58String(hash))") { result in @@ -30,7 +30,8 @@ public class Pin : ClientSubCommand { } /** List objects pinned to local storage */ - public func ls(_ completionHandler: @escaping ([Multihash : JsonType]) -> Void) throws { + @discardableResult + public func ls(_ completionHandler: @escaping ([Multihash : JsonType]) -> Void) throws -> CancellableRequest { /// The default is .Recursive try self.ls(.Recursive) { @@ -47,9 +48,9 @@ public class Pin : ClientSubCommand { completionHandler(multihashes) } } - - public func ls(_ pinType: PinType, completionHandler: @escaping (JsonType) throws -> Void) throws { - + + @discardableResult + public func ls(_ pinType: PinType, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("pin/ls?stream-channels=true&t=" + pinType.rawValue) { result in @@ -60,13 +61,14 @@ public class Pin : ClientSubCommand { try completionHandler(objects) } } - - public func rm(_ hash: Multihash, completionHandler: @escaping ([Multihash]) -> Void) throws { + + @discardableResult + public func rm(_ hash: Multihash, completionHandler: @escaping ([Multihash]) -> Void) throws -> CancellableRequest { try self.rm(hash, recursive: true, completionHandler: completionHandler) } - - public func rm(_ hash: Multihash, recursive: Bool, completionHandler: @escaping ([Multihash]) -> Void) throws { - + + @discardableResult + public func rm(_ hash: Multihash, recursive: Bool, completionHandler: @escaping ([Multihash]) -> Void) throws -> CancellableRequest { try parent!.fetchJson("pin/rm?stream-channels=true&r=\(recursive)&arg=\(b58String(hash))") { result in diff --git a/SwiftIpfsApi/Subcommands/Refs.swift b/SwiftIpfsApi/Subcommands/Refs.swift index 3080545..ab3fb1a 100644 --- a/SwiftIpfsApi/Subcommands/Refs.swift +++ b/SwiftIpfsApi/Subcommands/Refs.swift @@ -36,8 +36,9 @@ public class Refs : ClientSubCommand { let Ref: String let Err: String } - - public func local(_ completionHandler: @escaping ([Multihash]) -> Void) throws { + + @discardableResult + public func local(_ completionHandler: @escaping ([Multihash]) -> Void) throws -> CancellableRequest { try parent!.fetchData("refs/local") { (data: Data) in let fixedJsonData = fixStreamJson(data) diff --git a/SwiftIpfsApi/Subcommands/Repo.swift b/SwiftIpfsApi/Subcommands/Repo.swift index 3e88839..251d959 100644 --- a/SwiftIpfsApi/Subcommands/Repo.swift +++ b/SwiftIpfsApi/Subcommands/Repo.swift @@ -13,7 +13,8 @@ public class Repo : ClientSubCommand { /** gc is a plumbing command that will sweep the local set of stored objects and remove ones that are not pinned in order to reclaim hard disk space. */ - public func gc(_ completionHandler: @escaping (JsonType) -> Void) throws { + @discardableResult + public func gc(_ completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("repo/gc", completionHandler: completionHandler) } // public func gc(completionHandler: ([[String : AnyObject]]) -> Void) throws { diff --git a/SwiftIpfsApi/Subcommands/Stats.swift b/SwiftIpfsApi/Subcommands/Stats.swift index dca80d5..938caa4 100644 --- a/SwiftIpfsApi/Subcommands/Stats.swift +++ b/SwiftIpfsApi/Subcommands/Stats.swift @@ -12,11 +12,12 @@ public class Stats : ClientSubCommand { var parent: IpfsApiClient? /** Print ipfs bandwidth information. Currently ignores flags.*/ + @discardableResult public func bw( _ peer: String? = nil, proto: String? = nil, poll: Bool = false, interval: String? = nil, - completionHandler: @escaping (JsonType) -> Void) throws { + completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("stats/bw", completionHandler: completionHandler) } } diff --git a/SwiftIpfsApi/Subcommands/Swarm.swift b/SwiftIpfsApi/Subcommands/Swarm.swift index 92ca9e1..5f31fb4 100644 --- a/SwiftIpfsApi/Subcommands/Swarm.swift +++ b/SwiftIpfsApi/Subcommands/Swarm.swift @@ -16,7 +16,8 @@ public class Swarm : ClientSubCommand { /** Lists the set of peers this node is connected to. The completionHandler is passed an array of Multiaddr that represent the peers. */ - public func peers(_ completionHandler: @escaping ([Multiaddr]) throws -> Void) throws { + @discardableResult + public func peers(_ completionHandler: @escaping ([Multiaddr]) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("swarm/peers?stream-channels=true") { result in @@ -36,8 +37,8 @@ public class Swarm : ClientSubCommand { } /** lists all addresses this node is aware of. */ - public func addrs(_ completionHandler: @escaping (JsonType) throws -> Void) throws { - + @discardableResult + public func addrs(_ completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("swarm/addrs?stream-channels=true") { result in guard let addrsData = result.object?[IpfsCmdString.Addrs.rawValue] else { @@ -48,11 +49,13 @@ public class Swarm : ClientSubCommand { } /** opens a new direct connection to a peer address. */ - public func connect(_ multiaddr: String, completionHandler: @escaping (JsonType) throws -> Void) throws { + @discardableResult + public func connect(_ multiaddr: String, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("swarm/connect?arg=" + multiaddr, completionHandler: completionHandler) } - - public func disconnect(_ multiaddr: String, completionHandler: @escaping (JsonType) throws -> Void) throws { + + @discardableResult + public func disconnect(_ multiaddr: String, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest { try parent!.fetchJson("swarm/disconnect?arg=" + multiaddr, completionHandler: completionHandler) } } diff --git a/SwiftIpfsApi/Subcommands/Update.swift b/SwiftIpfsApi/Subcommands/Update.swift index 53474b5..4399eef 100644 --- a/SwiftIpfsApi/Subcommands/Update.swift +++ b/SwiftIpfsApi/Subcommands/Update.swift @@ -10,14 +10,14 @@ public class Update : ClientSubCommand { var parent: IpfsApiClient? - - public func check(_ completionHandler: @escaping (JsonType) -> Void) throws { - + + @discardableResult + public func check(_ completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("update/check", completionHandler: completionHandler ) } - - public func log(_ completionHandler: @escaping (JsonType) -> Void) throws { - + + @discardableResult + public func log(_ completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest { try parent!.fetchJson("update/log", completionHandler: completionHandler ) } } diff --git a/SwiftIpfsApiTests/CancellableRequestTests.swift b/SwiftIpfsApiTests/CancellableRequestTests.swift new file mode 100644 index 0000000..92dd235 --- /dev/null +++ b/SwiftIpfsApiTests/CancellableRequestTests.swift @@ -0,0 +1,35 @@ +// +// CancellableRequestTests.swift +// SwiftIpfsApiTests +// +// Created by Marcel Voß on 17.04.20. +// Copyright © 2020 Teo Sartori. All rights reserved. +// + +import XCTest + +class CancellableRequestTests: XCTestCase { + + func testCancellableRequest() { + let task = MockURLSessionDataTask() + + let cancellationExpectation = expectation(description: "expected to cancel network request") + + task.onCancel = { + cancellationExpectation.fulfill() + } + + task.cancel() + + waitForExpectations(timeout: 1.0) + } + +} + +private class MockURLSessionDataTask: URLSessionDataTask { + var onCancel: (() -> Void)? + + override func cancel() { + onCancel?() + } +}