Lots of improvements to get a useable performance
This commit is contained in:
@@ -21,6 +21,11 @@
|
||||
B514DD8F1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */; };
|
||||
B514DD911F40D15C00C932F8 /* StringUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD901F40D15C00C932F8 /* StringUtilities.swift */; };
|
||||
B514DD931F40D27500C932F8 /* TorrentInfoRowData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */; };
|
||||
B514DD951F40DB3100C932F8 /* TorrentFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD941F40DB3100C932F8 /* TorrentFileManagerTests.swift */; };
|
||||
B514DD9A1F40DCCA00C932F8 /* BigTorrentTest.torrent in Resources */ = {isa = PBXBuildFile; fileRef = B514DD991F40DCCA00C932F8 /* BigTorrentTest.torrent */; };
|
||||
B514DD9C1F40DD1B00C932F8 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD9B1F40DD1B00C932F8 /* URL.swift */; };
|
||||
B514DDA01F40DFC000C932F8 /* Data.bin in Resources */ = {isa = PBXBuildFile; fileRef = B514DD9F1F40DFC000C932F8 /* Data.bin */; };
|
||||
B514DDA41F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DDA31F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift */; };
|
||||
B51638931F0EE9B6009E563E /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638921F0EE9B6009E563E /* TCPConnection.swift */; };
|
||||
B51638951F0EEAA4009E563E /* TCPConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638941F0EEAA4009E563E /* TCPConnectionTests.swift */; };
|
||||
B51638971F0EEC2B009E563E /* GCDAsyncSocketStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */; };
|
||||
@@ -173,6 +178,12 @@
|
||||
B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedTrackerTests.swift; sourceTree = "<group>"; };
|
||||
B514DD901F40D15C00C932F8 /* StringUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilities.swift; sourceTree = "<group>"; };
|
||||
B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentInfoRowData.swift; sourceTree = "<group>"; };
|
||||
B514DD941F40DB3100C932F8 /* TorrentFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentFileManagerTests.swift; sourceTree = "<group>"; };
|
||||
B514DD961F40DC8300C932F8 /* LancasterPics.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = LancasterPics.torrent; sourceTree = "<group>"; };
|
||||
B514DD991F40DCCA00C932F8 /* BigTorrentTest.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = BigTorrentTest.torrent; sourceTree = "<group>"; };
|
||||
B514DD9B1F40DD1B00C932F8 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
||||
B514DD9F1F40DFC000C932F8 /* Data.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = Data.bin; path = "../../../../../../Library/Developer/CoreSimulator/Devices/BB5761CA-9F0E-4CAF-97A8-22BA0F3CA49D/data/Documents/Data.bin"; sourceTree = "<group>"; };
|
||||
B514DDA31F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedNetworkSpeedTracker.swift; sourceTree = "<group>"; };
|
||||
B51638921F0EE9B6009E563E /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = "<group>"; };
|
||||
B51638941F0EEAA4009E563E /* TCPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnectionTests.swift; sourceTree = "<group>"; };
|
||||
B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCDAsyncSocketStub.swift; sourceTree = "<group>"; };
|
||||
@@ -393,7 +404,9 @@
|
||||
children = (
|
||||
B54D0C2B1CA5787E004343BD /* Data+sha1.swift */,
|
||||
B514DD8A1F40C40500C932F8 /* NetworkSpeedTracker.swift */,
|
||||
B514DDA31F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift */,
|
||||
B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */,
|
||||
B514DD9B1F40DD1B00C932F8 /* URL.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
@@ -507,6 +520,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5C5F9661F250CC4007623B2 /* TorrentFileManager.swift */,
|
||||
B514DD941F40DB3100C932F8 /* TorrentFileManagerTests.swift */,
|
||||
B514DD9F1F40DFC000C932F8 /* Data.bin */,
|
||||
B514DD991F40DCCA00C932F8 /* BigTorrentTest.torrent */,
|
||||
B5AF7AA51F252A66003FD66F /* MultiFileHandle.swift */,
|
||||
B5AF7AA81F252A70003FD66F /* MultFileHandleTests.swift */,
|
||||
B5AF7AAC1F253055003FD66F /* FileHandleFake.swift */,
|
||||
@@ -799,7 +815,9 @@
|
||||
files = (
|
||||
B5E9B0E51F02FAC600EF58E3 /* TestText.torrent in Resources */,
|
||||
B514DD891F40B58200C932F8 /* text.txt in Resources */,
|
||||
B514DD9A1F40DCCA00C932F8 /* BigTorrentTest.torrent in Resources */,
|
||||
B514DD831F40A2B800C932F8 /* TrackerManagerTests.torrent in Resources */,
|
||||
B514DDA01F40DFC000C932F8 /* Data.bin in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -953,10 +971,12 @@
|
||||
B5C5F9671F250CC4007623B2 /* TorrentFileManager.swift in Sources */,
|
||||
B5F81E491F0436D600B25C70 /* TorrentPeerInfo.swift in Sources */,
|
||||
B527F6311F1BF30E001F06AF /* TorrentClient.swift in Sources */,
|
||||
B514DD9C1F40DD1B00C932F8 /* URL.swift in Sources */,
|
||||
B5BFD9D01F3FB13E00CE0186 /* TorrentTracker.swift in Sources */,
|
||||
B55317DC1F02FC4D00909ADF /* TorrentHTTPTracker.swift in Sources */,
|
||||
B52EE3401F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift in Sources */,
|
||||
B5F27C621F3F93AC0040589C /* TorrentProgressManager.swift in Sources */,
|
||||
B514DDA41F40E96100C932F8 /* CombinedNetworkSpeedTracker.swift in Sources */,
|
||||
B514DD8B1F40C40500C932F8 /* NetworkSpeedTracker.swift in Sources */,
|
||||
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */,
|
||||
B558F4831F0A647D00438BB4 /* InternetProtocol.swift in Sources */,
|
||||
@@ -1002,6 +1022,7 @@
|
||||
B535534C1F125DB500A6DCBE /* TorrentPeerMessageBufferTests.swift in Sources */,
|
||||
B527F6271F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift in Sources */,
|
||||
B5BFD9CE1F3FAFA500CE0186 /* TorrentTrackerManagerTests.swift in Sources */,
|
||||
B514DD951F40DB3100C932F8 /* TorrentFileManagerTests.swift in Sources */,
|
||||
B56A8A071C83539300426AC8 /* TestHelpers.swift in Sources */,
|
||||
B53553401F112BDB00A6DCBE /* BitFieldTests.swift in Sources */,
|
||||
B514DD881F40B2EC00C932F8 /* TorrentClientManagerStubs.swift in Sources */,
|
||||
@@ -1324,7 +1345,7 @@
|
||||
DEVELOPMENT_TEAM = HT9AS2MWK9;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = BitTorrentExample/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "uk.co.-bendavisapps.BitTorrentExample";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1348,7 +1369,7 @@
|
||||
DEVELOPMENT_TEAM = HT9AS2MWK9;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = BitTorrentExample/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "uk.co.-bendavisapps.BitTorrentExample";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -82,4 +82,10 @@ class MultFileHandleTests: XCTestCase {
|
||||
XCTAssert(fileHandle.synchronizeFileCalled)
|
||||
}
|
||||
}
|
||||
|
||||
func test_canReadLastBytes() {
|
||||
sut.seek(toFileOffset: 8)
|
||||
let data = sut.readData(ofLength: 2)
|
||||
XCTAssertEqualData(data, Data(bytes: [9,10]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ class MultiFileHandle: FileHandleProtocol {
|
||||
}
|
||||
|
||||
private func incrementCurrentFile() {
|
||||
guard (fileIndex + 1) < files.count else { return }
|
||||
fileIndex += 1
|
||||
currentFile.handle.seek(toFileOffset: 0)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public class TorrentFileManager {
|
||||
let metaInfo: TorrentMetaInfo
|
||||
let rootDirectory: String
|
||||
|
||||
fileprivate let fileHandle: MultiFileHandle
|
||||
fileprivate let fileHandle: FileHandleProtocol
|
||||
|
||||
convenience init(metaInfo: TorrentMetaInfo, rootDirectory: String) {
|
||||
let fileHandles = TorrentFileManager.createFileHandles(for: metaInfo, in: rootDirectory)
|
||||
@@ -54,6 +54,22 @@ public class TorrentFileManager {
|
||||
fileHandle.seek(toFileOffset: UInt64(byteIndex))
|
||||
return fileHandle.readData(ofLength: length)
|
||||
}
|
||||
|
||||
// TODO: Multi-threaded check
|
||||
func reCheckProgress() -> BitField {
|
||||
var result = BitField(size: metaInfo.info.pieces.count)
|
||||
for (pieceIndex, _) in result {
|
||||
autoreleasepool {
|
||||
let correctSha1 = metaInfo.info.pieces[pieceIndex]
|
||||
let piece = getPiece(at: pieceIndex)
|
||||
let sha1 = piece.sha1()
|
||||
if sha1 == correctSha1 {
|
||||
result.set(at: pieceIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Prepare directory
|
||||
@@ -100,3 +116,30 @@ extension TorrentFileManager {
|
||||
close(fileDescriptor) // Now we have a file of the correct size we close it
|
||||
}
|
||||
}
|
||||
|
||||
// Save/Load progress
|
||||
extension TorrentFileManager {
|
||||
|
||||
static func saveProgressBitfield(_ bitfield: BitField, infoHash: Data) {
|
||||
let fileName = String(asciiData: infoHash.base64EncodedData())!
|
||||
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,
|
||||
.userDomainMask,
|
||||
true)[0] as String
|
||||
let documentsUrl = URL(fileURLWithPath: documentsPath, isDirectory: true)
|
||||
let fileURL = documentsUrl.appendingPathComponent("torrent_progress.bin", isDirectory: false)
|
||||
try? bitfield.toData().write(to: fileURL)
|
||||
}
|
||||
|
||||
static func loadSavedProgressBitfield(infoHash: Data) -> BitField? {
|
||||
let fileName = String(asciiData: infoHash.base64EncodedData())!
|
||||
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,
|
||||
.userDomainMask,
|
||||
true)[0] as String
|
||||
let documentsUrl = URL(fileURLWithPath: documentsPath, isDirectory: true)
|
||||
let fileURL = documentsUrl.appendingPathComponent("torrent_progress.bin", isDirectory: false)
|
||||
if let data = try? Data(contentsOf: fileURL) {
|
||||
return BitField(data: data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// TorrentFileManagerTests.swift
|
||||
// BitTorrentTests
|
||||
//
|
||||
// Created by Ben Davis on 13/08/2017.
|
||||
// Copyright © 2017 Ben Davis. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import BitTorrent
|
||||
|
||||
class TorrentFileManagerTests: XCTestCase {
|
||||
|
||||
let metaInfo: TorrentMetaInfo = {
|
||||
let path = Bundle(for: TorrentProgressManagerTests.self).path(forResource: "BigTorrentTest",
|
||||
ofType: "torrent")
|
||||
let data = try! Data(contentsOf: URL(fileURLWithPath: path!))
|
||||
return TorrentMetaInfo(data: data)!
|
||||
}()
|
||||
|
||||
let piece1: Data = {
|
||||
let path = Bundle(for: TorrentProgressManagerTests.self).path(forResource: "Data",
|
||||
ofType: "bin")
|
||||
return try! Data(contentsOf: URL(fileURLWithPath: path!))
|
||||
}()
|
||||
|
||||
var fileHandle: FileHandleFake!
|
||||
var sut: TorrentFileManager!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
fileHandle = FileHandleFake(data: Data(repeating: 0, count: metaInfo.info.length))
|
||||
sut = TorrentFileManager(metaInfo: metaInfo, rootDirectory: "/", fileHandles: [fileHandle])
|
||||
}
|
||||
|
||||
func test_canSetPiece() {
|
||||
// Given
|
||||
let pieceLength = metaInfo.info.pieceLength
|
||||
|
||||
// When
|
||||
sut.setPiece(at: 1, data: piece1)
|
||||
|
||||
// Then
|
||||
XCTAssertEqualData(fileHandle.data[pieceLength..<pieceLength*2], piece1)
|
||||
}
|
||||
|
||||
func test_canGetPiece() {
|
||||
// Given
|
||||
sut.setPiece(at: 1, data: piece1)
|
||||
|
||||
// When
|
||||
let result = sut.getPiece(at: 1)
|
||||
|
||||
// Then
|
||||
XCTAssertEqualData(result, piece1)
|
||||
}
|
||||
|
||||
// Really slow test (takes ~3.5 seconds)
|
||||
// TODO: Need to create a smaller torrent with multiple pieces to verify against
|
||||
// func test_canGetProgressFromFile() {
|
||||
//
|
||||
// var expected = BitField(size: metaInfo.info.pieces.count)
|
||||
// expected.set(at: 1)
|
||||
//
|
||||
// sut.setPiece(at: 1, data: piece1)
|
||||
//
|
||||
// let result = sut.reCheckProgress()
|
||||
//
|
||||
// XCTAssertEqual(result, expected)
|
||||
// }
|
||||
}
|
||||
@@ -16,6 +16,8 @@ protocol TorrentPeerManagerDelegate: class {
|
||||
|
||||
func torrentPeerManager(_ sender: TorrentPeerManager, failedToGetPieceAtIndex index: Int)
|
||||
|
||||
func torrentPeerManagerNeedsMorePeers(_ sender: TorrentPeerManager)
|
||||
|
||||
func torrentPeerManagerCurrentBitfieldForHandshake(_ sender: TorrentPeerManager) -> BitField
|
||||
|
||||
func torrentPeerManager(_ sender: TorrentPeerManager,
|
||||
@@ -32,7 +34,8 @@ class TorrentPeerManager {
|
||||
}
|
||||
}
|
||||
|
||||
var maximumNumberOfConnectedPeers = 20
|
||||
var maximumNumberOfConnectedPeers = 20 // variable for testing
|
||||
var minimumNumberOfConnectedPeers = 5 // variable for testing
|
||||
let maximumNumberOfPiecesPerPeer = 2
|
||||
|
||||
weak var delegate: TorrentPeerManagerDelegate?
|
||||
@@ -49,12 +52,15 @@ class TorrentPeerManager {
|
||||
return peers.filter({ $0.connected && $0.currentProgress.complete }).count
|
||||
}
|
||||
|
||||
private(set) var downloadSpeedTracker = NetworkSpeedTracker ()
|
||||
private(set) var downloadSpeedTracker: NetworkSpeedTrackable!
|
||||
|
||||
init(clientId: Data, infoHash: Data, bitFieldSize: Int) {
|
||||
self.clientId = clientId
|
||||
self.infoHash = infoHash
|
||||
self.bitFieldSize = bitFieldSize
|
||||
self.downloadSpeedTracker = CombinedNetworkSpeedTracker { [unowned self] in
|
||||
return self.peers.map { $0.downloadSpeedTracker }
|
||||
}
|
||||
}
|
||||
|
||||
// Exposed for testing
|
||||
@@ -80,8 +86,12 @@ class TorrentPeerManager {
|
||||
let bitField = delegate.torrentPeerManagerCurrentBitfieldForHandshake(self)
|
||||
let peersToConnectTo = peers[0 ..< numberToConnectTo]
|
||||
connectToPeers(peersToConnectTo, bitField: bitField)
|
||||
if numberOfConnectedPeers < minimumNumberOfConnectedPeers {
|
||||
delegate.torrentPeerManagerNeedsMorePeers(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Using sequence to allow array slices
|
||||
private func connectToPeers<T: Sequence>(_ peers: T, bitField: BitField) where T.Element == TorrentPeer {
|
||||
for peer in peers {
|
||||
peer.delegate = self
|
||||
@@ -118,7 +128,6 @@ extension TorrentPeerManager: TorrentPeerDelegate {
|
||||
}
|
||||
|
||||
func peer(_ sender: TorrentPeer, gotPieceAtIndex index: Int, piece: Data) {
|
||||
downloadSpeedTracker.increase(by: piece.count)
|
||||
delegate?.torrentPeerManager(self, downloadedPieceAtIndex: index, piece: piece)
|
||||
requestNextPiece(from: sender)
|
||||
// TODO: send have to peers
|
||||
@@ -133,8 +142,8 @@ extension TorrentPeerManager: TorrentPeerDelegate {
|
||||
private func requestNextPiece(from peer: TorrentPeer) {
|
||||
if let pieceRequest = delegate?.torrentPeerManager(self, nextPieceFromAvailable: peer.currentProgress) {
|
||||
peer.downloadPiece(atIndex: pieceRequest.pieceIndex, size: pieceRequest.size)
|
||||
} else {
|
||||
print("No available pieces for peer \(peer.peerInfo.ip):\(peer.peerInfo.port) to download")
|
||||
} else if enableLogging {
|
||||
print("No available pieces for peer \(peer.peerInfo.ip):\(peer.peerInfo.port) to download")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import XCTest
|
||||
|
||||
class TorrentPeerFake: TorrentPeer {
|
||||
|
||||
var testDownloadSpeedTracker = NetworkSpeedTracker()
|
||||
override var downloadSpeedTracker: NetworkSpeedTracker {
|
||||
return testDownloadSpeedTracker
|
||||
}
|
||||
|
||||
var connectCalled = false
|
||||
var connectHandshakeData: (clientId: Data, bitField: BitField)?
|
||||
override func connect(withHandshakeData handshakeData: (clientId: Data, bitField: BitField)) throws {
|
||||
@@ -34,6 +39,14 @@ class TorrentPeerFake: TorrentPeer {
|
||||
|
||||
class TorrentPeerManagerDelegateStub: TorrentPeerManagerDelegate {
|
||||
|
||||
var torrentPeerManagerNeedsMorePeersCalled = false
|
||||
var torrentPeerManagerNeedsMorePeersParameter: TorrentPeerManager?
|
||||
func torrentPeerManagerNeedsMorePeers(_ sender: TorrentPeerManager) {
|
||||
torrentPeerManagerNeedsMorePeersCalled = true
|
||||
torrentPeerManagerNeedsMorePeersParameter = sender
|
||||
}
|
||||
|
||||
|
||||
var downloadedPieceAtIndexCalled = false
|
||||
var downloadedPieceAtIndexParameters: (sender: TorrentPeerManager, pieceIndex: Int, piece: Data)?
|
||||
func torrentPeerManager(_ sender: TorrentPeerManager,
|
||||
@@ -283,7 +296,7 @@ class TorrentPeerManagerTests: XCTestCase {
|
||||
XCTAssertFalse(peer.downloadPieceCalled)
|
||||
}
|
||||
|
||||
func test_downloadSpeedRecordedOnGettingPiece() {
|
||||
func test_downloadSpeedSumsPeerDownloadSpeeds() {
|
||||
|
||||
// Given
|
||||
let peerInfo = TorrentPeerInfo(ip: "127.0.0.1", port: 123, peerId: nil)
|
||||
@@ -293,9 +306,23 @@ class TorrentPeerManagerTests: XCTestCase {
|
||||
let pieceSize = 100
|
||||
|
||||
// When
|
||||
sut.peer(peer, gotPieceAtIndex: 0, piece: Data(repeating: 0, count: pieceSize))
|
||||
peer.testDownloadSpeedTracker.increase(by: pieceSize)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.downloadSpeedTracker.totalNumberOfBytes, pieceSize)
|
||||
}
|
||||
|
||||
func test_morePeersRequestedWhenNumberDropsBelowMin() {
|
||||
|
||||
// Given
|
||||
sut.minimumNumberOfConnectedPeers = 2
|
||||
let peerInfo = TorrentPeerInfo(ip: "127.0.0.1", port: 123, peerId: nil)
|
||||
|
||||
// When
|
||||
sut.addPeers(withInfo: [peerInfo])
|
||||
|
||||
// Then
|
||||
XCTAssert(delegate.torrentPeerManagerNeedsMorePeersCalled)
|
||||
XCTAssert(delegate.torrentPeerManagerNeedsMorePeersParameter === sut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,14 @@ class TorrentPeer {
|
||||
}
|
||||
downloadPieceRequests.removeAll()
|
||||
}
|
||||
|
||||
private func onConnectionDropped() {
|
||||
keepAliveTimer?.invalidate()
|
||||
keepAliveTimer = nil
|
||||
killAllDownloads()
|
||||
connected = false
|
||||
delegate?.peerLost(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep alive
|
||||
@@ -142,8 +150,7 @@ extension TorrentPeer {
|
||||
}
|
||||
|
||||
@objc private func didntReceiveKeepAlive() {
|
||||
keepAliveTimer = nil
|
||||
delegate?.peerLost(self)
|
||||
onConnectionDropped()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +168,7 @@ extension TorrentPeer: TorrentPeerCommunicatorDelegate {
|
||||
}
|
||||
|
||||
func peerLost(_ sender: TorrentPeerCommunicator) {
|
||||
killAllDownloads()
|
||||
delegate?.peerLost(self)
|
||||
connected = false
|
||||
onConnectionDropped()
|
||||
}
|
||||
|
||||
func peerSentHandshake(_ sender: TorrentPeerCommunicator, sentHandshakeWithPeerId peerId: Data, onDHT: Bool) {
|
||||
|
||||
@@ -329,6 +329,7 @@ class TorrentPeerTests: XCTestCase {
|
||||
let e = expectation(description: "Keep alive sent")
|
||||
DispatchQueue.main.async {
|
||||
XCTAssert(self.delegate.peerLostCalled)
|
||||
XCTAssertFalse(self.sut.connected)
|
||||
e.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 0.1)
|
||||
|
||||
@@ -22,7 +22,13 @@ class TorrentProgressManager {
|
||||
convenience init(metaInfo: TorrentMetaInfo, rootDirectory: String) {
|
||||
let downloadDirectory = rootDirectory + "/" + metaInfo.sensibleDownloadDirectoryName()
|
||||
let fileManager = TorrentFileManager(metaInfo: metaInfo, rootDirectory: downloadDirectory)
|
||||
let progress = TorrentProgress(size: metaInfo.info.pieces.count)
|
||||
|
||||
let progress: TorrentProgress
|
||||
if let bitField = TorrentFileManager.loadSavedProgressBitfield(infoHash: metaInfo.infoHash) {
|
||||
progress = TorrentProgress(bitField: bitField)
|
||||
} else {
|
||||
progress = TorrentProgress(size: metaInfo.info.pieces.count)
|
||||
}
|
||||
self.init(fileManager: fileManager, progress: progress)
|
||||
}
|
||||
|
||||
@@ -31,6 +37,11 @@ class TorrentProgressManager {
|
||||
self.progress = progress
|
||||
}
|
||||
|
||||
public func forceReCheck() {
|
||||
let bitField = fileManager.reCheckProgress()
|
||||
progress = TorrentProgress(bitField: bitField)
|
||||
}
|
||||
|
||||
func getNextPieceToDownload(from availablePieces: BitField) -> TorrentPieceRequest? {
|
||||
for (i, isSet) in availablePieces where isSet {
|
||||
if !progress.hasPiece(i) && !progress.isCurrentlyDownloading(piece: i) {
|
||||
@@ -44,6 +55,7 @@ class TorrentProgressManager {
|
||||
func setDownloadedPiece(_ piece: Data, pieceIndex: Int) {
|
||||
progress.finishedDownloading(piece: pieceIndex)
|
||||
fileManager.setPiece(at: pieceIndex, data: piece)
|
||||
TorrentFileManager.saveProgressBitfield(progress.bitField, infoHash: metaInfo.infoHash)
|
||||
}
|
||||
|
||||
func setLostPiece(at index: Int) {
|
||||
|
||||
@@ -21,6 +21,11 @@ class TorrentProgressManagerTests: XCTestCase {
|
||||
return TorrentMetaInfo(data: data)!
|
||||
}()
|
||||
|
||||
let finalData: Data = {
|
||||
let path = Bundle(for: TorrentProgressManagerTests.self).path(forResource: "text", ofType: "txt")
|
||||
return try! Data(contentsOf: URL(fileURLWithPath: path!))
|
||||
}()
|
||||
|
||||
let completeBitField: BitField = {
|
||||
var result = BitField(size: 1)
|
||||
result.set(at: 0)
|
||||
@@ -42,6 +47,18 @@ class TorrentProgressManagerTests: XCTestCase {
|
||||
sut = TorrentProgressManager(fileManager: fileManager, progress: progress)
|
||||
}
|
||||
|
||||
func test_canForceReCheck() {
|
||||
|
||||
// Given
|
||||
fileHandle.data = finalData
|
||||
|
||||
// When
|
||||
sut.forceReCheck()
|
||||
|
||||
// Then
|
||||
XCTAssert(sut.progress.complete)
|
||||
}
|
||||
|
||||
func test_exampleMetaInfoOnlyHas1Piece() {
|
||||
XCTAssertEqual(metaInfo.info.pieces.count, 1)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class TorrentClient {
|
||||
public var progress: TorrentProgress { return progressManager.progress }
|
||||
public var numberOfConnectedPeers: Int { return peerManager.numberOfConnectedPeers }
|
||||
public var numberOfConnectedSeeds: Int { return peerManager.numberOfConnectedPeers }
|
||||
public var downloadSpeedTracker: NetworkSpeedTracker { return peerManager.downloadSpeedTracker }
|
||||
public var downloadSpeedTracker: NetworkSpeedTrackable { return peerManager.downloadSpeedTracker }
|
||||
|
||||
let progressManager: TorrentProgressManager
|
||||
let peerManager: TorrentPeerManager
|
||||
@@ -55,7 +55,6 @@ public class TorrentClient {
|
||||
|
||||
trackerManager.delegate = self
|
||||
peerManager.delegate = self
|
||||
peerManager.enableLogging = true
|
||||
}
|
||||
|
||||
// For testing
|
||||
@@ -69,6 +68,10 @@ public class TorrentClient {
|
||||
self.trackerManager = trackerManager
|
||||
}
|
||||
|
||||
public func forceReCheck() {
|
||||
progressManager.forceReCheck()
|
||||
}
|
||||
|
||||
public func start() {
|
||||
trackerManager.start()
|
||||
status = .started
|
||||
@@ -85,9 +88,9 @@ extension TorrentClient: TorrentTrackerManagerDelegate {
|
||||
|
||||
return TorrentTrackerManagerAnnonuceInfo(
|
||||
numberOfBytesRemaining: progress.remaining * metaInfo.info.pieceLength,
|
||||
numberOfBytesUploaded: progress.uploaded * metaInfo.info.pieceLength,
|
||||
numberOfBytesUploaded: 0,
|
||||
numberOfBytesDownloaded: progress.downloaded * metaInfo.info.pieceLength,
|
||||
numberOfPeersToFetch: 20)
|
||||
numberOfPeersToFetch: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +108,10 @@ extension TorrentClient: TorrentPeerManagerDelegate {
|
||||
progressManager.setLostPiece(at: index)
|
||||
}
|
||||
|
||||
func torrentPeerManagerNeedsMorePeers(_ sender: TorrentPeerManager) {
|
||||
trackerManager.forceRestart()
|
||||
}
|
||||
|
||||
func torrentPeerManagerCurrentBitfieldForHandshake(_ sender: TorrentPeerManager) -> BitField {
|
||||
return progressManager.progress.bitField
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ public struct TorrentProgress {
|
||||
public private(set) var bitField: BitField
|
||||
private var piecesBeingDownloaded: [Int] = []
|
||||
|
||||
public private(set) var uploaded: Int = 0
|
||||
public private(set) var downloaded: Int = 0
|
||||
|
||||
public var remaining: Int {
|
||||
@@ -32,6 +31,13 @@ public struct TorrentProgress {
|
||||
self.bitField = BitField(size: size)
|
||||
}
|
||||
|
||||
init(bitField: BitField) {
|
||||
self.bitField = bitField
|
||||
for (_, isSet) in bitField where isSet {
|
||||
downloaded += 1
|
||||
}
|
||||
}
|
||||
|
||||
func isCurrentlyDownloading(piece: Int) -> Bool {
|
||||
return piecesBeingDownloaded.contains(piece)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class TorrentProgressTests: XCTestCase {
|
||||
func test_torrentProgressIsInitialisedAsNoneDownloaded() {
|
||||
let result = TorrentProgress(size: 10)
|
||||
XCTAssertEqual(result.downloaded, 0)
|
||||
XCTAssertEqual(result.uploaded, 0)
|
||||
}
|
||||
|
||||
func test_canMarkPieceAsDownloading() {
|
||||
|
||||
@@ -30,6 +30,15 @@ class TorrentTrackerManager {
|
||||
let clientId: String
|
||||
let port: Int
|
||||
|
||||
var announceTimeInterval: TimeInterval = 600
|
||||
private lazy var announceTimer: Timer = {
|
||||
return Timer.scheduledTimer(timeInterval: self.announceTimeInterval,
|
||||
target: self,
|
||||
selector: #selector(announce),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
}()
|
||||
|
||||
init(metaInfo: TorrentMetaInfo, clientId: Data, port: Int) {
|
||||
self.metaInfo = metaInfo
|
||||
self.clientId = String(data: clientId, encoding: .utf8)!
|
||||
@@ -51,16 +60,17 @@ class TorrentTrackerManager {
|
||||
|
||||
private static func createTrackers(from metaInfo: TorrentMetaInfo) -> [TorrentTracker] {
|
||||
|
||||
let announceList = metaInfo.announceList?.first ?? [metaInfo.announce]
|
||||
let announceList = metaInfo.announceList ?? [[metaInfo.announce]]
|
||||
let flatAnnounceList = announceList.flatMap { return $0 }
|
||||
|
||||
var lastPortNumberUsed: UInt16 = 3475
|
||||
var result: [TorrentTracker] = []
|
||||
|
||||
for url in announceList {
|
||||
for url in flatAnnounceList {
|
||||
|
||||
if url.scheme == "http" {
|
||||
if url.scheme == "http" || url.scheme == "https" {
|
||||
|
||||
let tracker = TorrentHTTPTracker(announceURL: url)
|
||||
let tracker = TorrentHTTPTracker(announceURL: url.bySettingScheme(to: "https"))
|
||||
result.append(tracker)
|
||||
|
||||
} else if url.scheme == "udp" {
|
||||
@@ -76,10 +86,14 @@ class TorrentTrackerManager {
|
||||
}
|
||||
|
||||
func start() {
|
||||
announce()
|
||||
forceRestart()
|
||||
}
|
||||
|
||||
private func announce() {
|
||||
func forceRestart() {
|
||||
announceTimer.fire()
|
||||
}
|
||||
|
||||
@objc private func announce() {
|
||||
|
||||
guard let delegate = delegate else { return }
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class TorrentTrackerStub: TorrentTracker {
|
||||
numberOfBytesDownloaded: Int,
|
||||
numberOfPeersToFetch: Int)?
|
||||
|
||||
var onAnnounceClient: (()->Void)?
|
||||
func announceClient(with peerId: String,
|
||||
port: Int,
|
||||
event: TorrentTrackerEvent,
|
||||
@@ -40,6 +41,7 @@ class TorrentTrackerStub: TorrentTracker {
|
||||
numberOfBytesUploaded,
|
||||
numberOfBytesDownloaded,
|
||||
numberOfPeersToFetch)
|
||||
onAnnounceClient?()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +75,12 @@ class TorrentTrackerManagerTests: XCTestCase {
|
||||
let clientIdData = "-BD0000-bxa]N#IRKqv`".data(using: .ascii)!
|
||||
let listeningPort = 123
|
||||
|
||||
|
||||
let announceInfo = TorrentTrackerManagerAnnonuceInfo(numberOfBytesRemaining: 1,
|
||||
numberOfBytesUploaded: 2,
|
||||
numberOfBytesDownloaded: 3,
|
||||
numberOfPeersToFetch: 4)
|
||||
|
||||
func test_createsTrackers() {
|
||||
|
||||
let sut = TorrentTrackerManager(metaInfo: metaInfo, clientId: clientIdData, port: listeningPort)
|
||||
@@ -89,7 +97,8 @@ class TorrentTrackerManagerTests: XCTestCase {
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(httpTracker.announceURL, metaInfo.announceList![0][0])
|
||||
let httpsScheme = metaInfo.announceList![0][0].bySettingScheme(to: "https")
|
||||
XCTAssertEqual(httpTracker.announceURL, httpsScheme)
|
||||
XCTAssertEqual(udpTracker.announceURL, metaInfo.announceList![0][1])
|
||||
|
||||
XCTAssert(httpTracker.delegate === sut)
|
||||
@@ -101,18 +110,14 @@ class TorrentTrackerManagerTests: XCTestCase {
|
||||
// Given
|
||||
let tracker = TorrentTrackerStub()
|
||||
let delegate = TorrentTrackerManagerDelegateStub()
|
||||
let announceInfo = TorrentTrackerManagerAnnonuceInfo(numberOfBytesRemaining: 1,
|
||||
numberOfBytesUploaded: 2,
|
||||
numberOfBytesDownloaded: 3,
|
||||
numberOfPeersToFetch: 4)
|
||||
|
||||
let sut = TorrentTrackerManager(metaInfo: metaInfo,
|
||||
clientId: clientIdData,
|
||||
port: listeningPort,
|
||||
trackers: [tracker])
|
||||
sut.delegate = delegate
|
||||
|
||||
delegate.torrentTrackerManagerAnnonuceInfoResult = announceInfo
|
||||
sut.delegate = delegate
|
||||
|
||||
// When
|
||||
sut.start()
|
||||
@@ -133,4 +138,56 @@ class TorrentTrackerManagerTests: XCTestCase {
|
||||
XCTAssertEqual(announceClientParameters.numberOfBytesDownloaded, announceInfo.numberOfBytesDownloaded)
|
||||
XCTAssertEqual(announceClientParameters.numberOfPeersToFetch, announceInfo.numberOfPeersToFetch)
|
||||
}
|
||||
|
||||
func test_announceRepeats() {
|
||||
|
||||
// Given
|
||||
let tracker = TorrentTrackerStub()
|
||||
let delegate = TorrentTrackerManagerDelegateStub()
|
||||
|
||||
let sut = TorrentTrackerManager(metaInfo: metaInfo,
|
||||
clientId: clientIdData,
|
||||
port: listeningPort,
|
||||
trackers: [tracker])
|
||||
|
||||
sut.delegate = delegate
|
||||
sut.announceTimeInterval = 0
|
||||
|
||||
// When
|
||||
sut.start()
|
||||
|
||||
// Then
|
||||
let expectation = self.expectation(description: "Announce is repeatedly called")
|
||||
tracker.onAnnounceClient = {
|
||||
tracker.onAnnounceClient = nil
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_canForceReAnnounce_resetsAnnounceTimer() {
|
||||
|
||||
// Given
|
||||
let tracker = TorrentTrackerStub()
|
||||
let delegate = TorrentTrackerManagerDelegateStub()
|
||||
let sut = TorrentTrackerManager(metaInfo: metaInfo,
|
||||
clientId: clientIdData,
|
||||
port: listeningPort,
|
||||
trackers: [tracker])
|
||||
|
||||
sut.delegate = delegate
|
||||
sut.announceTimeInterval = 600
|
||||
sut.start()
|
||||
|
||||
// Then
|
||||
let expectation = self.expectation(description: "Announce is repeatedly called")
|
||||
tracker.onAnnounceClient = {
|
||||
tracker.onAnnounceClient = nil
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
// When
|
||||
sut.forceRestart()
|
||||
waitForExpectations(timeout: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// CombinedNetworkSpeedTracker.swift
|
||||
// BitTorrent
|
||||
//
|
||||
// Created by Ben Davis on 13/08/2017.
|
||||
// Copyright © 2017 Ben Davis. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class CombinedNetworkSpeedTracker: NetworkSpeedTrackable {
|
||||
|
||||
let trackers: () -> [NetworkSpeedTracker]
|
||||
|
||||
init(trackers: @escaping () -> [NetworkSpeedTracker]) {
|
||||
self.trackers = trackers
|
||||
}
|
||||
|
||||
// MARK: - NetworkSpeedTrackable
|
||||
|
||||
var totalNumberOfBytes: Int {
|
||||
var result = 0
|
||||
for tracker in trackers() {
|
||||
result += tracker.totalNumberOfBytes
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func numberOfBytesDownloaded(since date: Date) -> Int {
|
||||
var result = 0
|
||||
for tracker in trackers() {
|
||||
result += tracker.numberOfBytesDownloaded(since: date)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int {
|
||||
var result = 0
|
||||
for tracker in trackers() {
|
||||
result += tracker.numberOfBytesDownloaded(over: timeInterval)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,37 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Collection {
|
||||
func firstWhere(_ predicate: (Element) -> Bool) -> Element? {
|
||||
return first(where: predicate)
|
||||
public protocol NetworkSpeedTrackable {
|
||||
var totalNumberOfBytes: Int { get }
|
||||
func numberOfBytesDownloaded(since date: Date) -> Int
|
||||
func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int
|
||||
}
|
||||
|
||||
public struct NetworkSpeedTracker: NetworkSpeedTrackable {
|
||||
public var totalNumberOfBytes: Int = 0
|
||||
|
||||
// TODO: limit number of dataPoints stored
|
||||
private var dataPoints: [NetworkSpeedDataPoint] = [NetworkSpeedDataPoint(0)]
|
||||
|
||||
mutating func increase(by bytes: Int) {
|
||||
totalNumberOfBytes += bytes
|
||||
addDataPoint(NetworkSpeedDataPoint(totalNumberOfBytes))
|
||||
}
|
||||
|
||||
private mutating func addDataPoint(_ dataPoint: NetworkSpeedDataPoint) {
|
||||
dataPoints = [dataPoint] + dataPoints
|
||||
}
|
||||
|
||||
public func numberOfBytesDownloaded(since date: Date) -> Int {
|
||||
|
||||
guard let previouslyDataPoint = dataPoints.firstWhere({ $0.dateRecorded < date }) else {
|
||||
return totalNumberOfBytes
|
||||
}
|
||||
return totalNumberOfBytes - previouslyDataPoint.numberOfBytes
|
||||
}
|
||||
|
||||
public func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int {
|
||||
return numberOfBytesDownloaded(since: Date(timeIntervalSinceNow: -timeInterval))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +55,7 @@ public struct NetworkSpeedDataPoint: Equatable, Comparable {
|
||||
public static func ==(_ lhs: NetworkSpeedDataPoint, _ rhs: NetworkSpeedDataPoint) -> Bool {
|
||||
return (
|
||||
lhs.numberOfBytes == rhs.numberOfBytes &&
|
||||
lhs.dateRecorded == rhs.dateRecorded
|
||||
lhs.dateRecorded == rhs.dateRecorded
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,28 +63,9 @@ public struct NetworkSpeedDataPoint: Equatable, Comparable {
|
||||
return lhs.dateRecorded.compare(rhs.dateRecorded) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
public struct NetworkSpeedTracker {
|
||||
var totalNumberOfBytes: Int = 0
|
||||
private var dataPoints: [NetworkSpeedDataPoint] = [NetworkSpeedDataPoint(0)]
|
||||
|
||||
mutating func increase(by bytes: Int) {
|
||||
totalNumberOfBytes += bytes
|
||||
addDataPoint(NetworkSpeedDataPoint(totalNumberOfBytes))
|
||||
}
|
||||
|
||||
private mutating func addDataPoint(_ dataPoint: NetworkSpeedDataPoint) {
|
||||
dataPoints = [dataPoint] + dataPoints
|
||||
}
|
||||
|
||||
public func numberOfBytesDownloaded(since date: Date) -> Int {
|
||||
guard let previouslyDataPoint = dataPoints.firstWhere({ $0.dateRecorded < date }) else {
|
||||
return totalNumberOfBytes
|
||||
}
|
||||
return totalNumberOfBytes - previouslyDataPoint.numberOfBytes
|
||||
}
|
||||
|
||||
public func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int {
|
||||
return numberOfBytesDownloaded(since: Date(timeIntervalSinceNow: -timeInterval))
|
||||
// Not sure why I need this 🤷♂️
|
||||
extension Collection {
|
||||
func firstWhere(_ predicate: (Element) -> Bool) -> Element? {
|
||||
return first(where: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// URL.swift
|
||||
// BitTorrent
|
||||
//
|
||||
// Created by Ben Davis on 13/08/2017.
|
||||
// Copyright © 2017 Ben Davis. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
public func bySettingScheme(to scheme: String) -> URL {
|
||||
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
||||
components.scheme = scheme
|
||||
return components.url!
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
||||
@@ -10,16 +10,14 @@ import Foundation
|
||||
|
||||
extension TimeInterval {
|
||||
|
||||
var milliseconds: Int {
|
||||
return Int((self.truncatingRemainder(dividingBy: 1)) * 1000)
|
||||
}
|
||||
|
||||
var seconds: Int {
|
||||
return Int(self.remainder(dividingBy: 60))
|
||||
let remaining = self - Double(hours*60*60) - Double(minutes*60)
|
||||
return Int(remaining / 60)
|
||||
}
|
||||
|
||||
var minutes: Int {
|
||||
return Int((self/60).remainder(dividingBy: 60))
|
||||
let remaining = self - Double(hours*60*60)
|
||||
return Int(remaining / 60)
|
||||
}
|
||||
|
||||
var hours: Int {
|
||||
@@ -31,8 +29,6 @@ extension TimeInterval {
|
||||
return "\(self.hours)h \(self.minutes)m \(self.seconds)s"
|
||||
} else if self.minutes != 0 {
|
||||
return "\(self.minutes)m \(self.seconds)s"
|
||||
} else if self.milliseconds != 0 {
|
||||
return "\(self.seconds)s \(self.milliseconds)ms"
|
||||
} else {
|
||||
return "\(self.seconds)s"
|
||||
}
|
||||
|
||||
@@ -74,15 +74,16 @@ enum TorrentInfoRowData: Int {
|
||||
|
||||
case .eta:
|
||||
let speed = client.downloadSpeedTracker.numberOfBytesDownloaded(over: speedSampleSize)
|
||||
let speedPerSecond = Double(speed) / speedSampleSize
|
||||
guard speed > 0 else { return "∞" }
|
||||
|
||||
let remaining = client.progress.remaining * client.metaInfo.info.pieceLength
|
||||
guard speed > 0 else { return "n/a" }
|
||||
guard remaining > 0 else { return "n/a" }
|
||||
|
||||
return TimeInterval(remaining / speed).stringTime
|
||||
return (Double(remaining) / speedPerSecond).stringTime
|
||||
|
||||
case .uploaded:
|
||||
return bytesToString(client.progress.uploaded * client.metaInfo.info.pieceLength)
|
||||
return "????"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ import BitTorrent
|
||||
|
||||
class TorrentViewController: UIViewController {
|
||||
|
||||
static let refreshRate: TimeInterval = 1
|
||||
let refreshRate: TimeInterval = 1
|
||||
|
||||
let tableView = UITableView()
|
||||
let torrentClient: TorrentClient
|
||||
|
||||
lazy var refreshTimer: Timer = {
|
||||
return Timer.scheduledTimer(timeInterval: TorrentViewController.refreshRate,
|
||||
return Timer.scheduledTimer(timeInterval: self.refreshRate,
|
||||
target: self,
|
||||
selector: #selector(TorrentViewController.timerFired),
|
||||
selector: #selector(timerFired),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
}()
|
||||
@@ -44,7 +44,14 @@ class TorrentViewController: UIViewController {
|
||||
super.viewDidLayoutSubviews()
|
||||
tableView.frame = view.bounds
|
||||
if tableView.contentInset == .zero {
|
||||
tableView.contentInset = view.safeAreaInsets
|
||||
if #available(iOS 11, *) {
|
||||
tableView.contentInset = view.safeAreaInsets
|
||||
} else {
|
||||
tableView.contentInset = UIEdgeInsetsMake(topLayoutGuide.length,
|
||||
0,
|
||||
bottomLayoutGuide.length,
|
||||
0)
|
||||
}
|
||||
tableView.contentOffset = .zero
|
||||
}
|
||||
}
|
||||
@@ -69,7 +76,7 @@ extension TorrentViewController: UITableViewDataSource {
|
||||
if section == 0 {
|
||||
return TorrentInfoRowData.numberOfRows
|
||||
} else {
|
||||
return 1
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +84,19 @@ extension TorrentViewController: UITableViewDataSource {
|
||||
if indexPath.section == 0 {
|
||||
return cellForTorrentInfoSection(at: indexPath, tableView: tableView)
|
||||
} else {
|
||||
let startCell = UITableViewCell(style: .default, reuseIdentifier: nil)
|
||||
startCell.textLabel?.text = "Start"
|
||||
startCell.textLabel?.textAlignment = .center
|
||||
startCell.textLabel?.textColor = .blue
|
||||
return startCell
|
||||
if indexPath.row == 0 {
|
||||
let startCell = UITableViewCell(style: .default, reuseIdentifier: nil)
|
||||
startCell.textLabel?.text = "Force re-check"
|
||||
startCell.textLabel?.textAlignment = .center
|
||||
startCell.textLabel?.textColor = .blue
|
||||
return startCell
|
||||
} else {
|
||||
let reCheckCell = UITableViewCell(style: .default, reuseIdentifier: nil)
|
||||
startCell.textLabel?.text = "Start"
|
||||
startCell.textLabel?.textAlignment = .center
|
||||
startCell.textLabel?.textColor = .blue
|
||||
return startCell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +136,14 @@ extension TorrentViewController: UITableViewDelegate {
|
||||
guard let cell = tableView.cellForRow(at: indexPath) else { return }
|
||||
cell.setSelected(false, animated: false)
|
||||
|
||||
if indexPath.section == 1 {
|
||||
guard indexPath.section == 1 else { return }
|
||||
if indexPath.row == 0 {
|
||||
torrentClient.forceReCheck()
|
||||
} else {
|
||||
torrentClient.start()
|
||||
tableView.reloadData()
|
||||
}
|
||||
tableView.reloadData()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user