153 lines
5.9 KiB
Swift
153 lines
5.9 KiB
Swift
//
|
|
// TorrentFileManager.swift
|
|
// BitTorrent
|
|
//
|
|
// Created by Ben Davis on 23/07/2017.
|
|
// Copyright © 2017 Ben Davis. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
enum TorrentFileManagerError: Error {
|
|
case couldNotCreateFile
|
|
}
|
|
|
|
public class TorrentFileManager {
|
|
|
|
let metaInfo: TorrentMetaInfo
|
|
let rootDirectory: String
|
|
|
|
fileprivate let fileHandle: FileHandleProtocol
|
|
|
|
convenience init(metaInfo: TorrentMetaInfo, rootDirectory: String) {
|
|
let fileHandles = TorrentFileManager.createFileHandles(for: metaInfo, in: rootDirectory)
|
|
self.init(metaInfo: metaInfo, rootDirectory: rootDirectory, fileHandles: fileHandles)
|
|
}
|
|
|
|
init(metaInfo: TorrentMetaInfo, rootDirectory: String, fileHandles: [FileHandleProtocol]) {
|
|
self.metaInfo = metaInfo
|
|
self.rootDirectory = rootDirectory
|
|
self.fileHandle = MultiFileHandle(fileHandles: fileHandles)
|
|
}
|
|
|
|
private static func createFileHandles(for metaInfo: TorrentMetaInfo, in rootDirectory: String) -> [FileHandle] {
|
|
var result: [FileHandle] = []
|
|
for fileInfo in metaInfo.info.files {
|
|
let fullPath = rootDirectory + "/" + fileInfo.path
|
|
let fileHandle = FileHandle(forUpdatingAtPath: fullPath)!
|
|
result.append(fileHandle)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MARK: set and get piece
|
|
|
|
func setPiece(at index: Int, data: Data) {
|
|
let byteIndex = index * metaInfo.info.pieceLength
|
|
fileHandle.seek(toFileOffset: UInt64(byteIndex))
|
|
fileHandle.write(data)
|
|
}
|
|
|
|
func getPiece(at index: Int) -> Data {
|
|
let byteIndex = index * metaInfo.info.pieceLength
|
|
let length = metaInfo.info.lengthOfPiece(at: index)
|
|
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
|
|
extension TorrentFileManager {
|
|
|
|
public static func prepareRootDirectory(_ rootDirectory: String,
|
|
forTorrentMetaInfo metaInfo: TorrentMetaInfo) throws {
|
|
|
|
try createDirectoryIfNeeded(directoryPath: rootDirectory)
|
|
|
|
for file in metaInfo.info.files {
|
|
let fullPath = rootDirectory + "/" + file.path
|
|
try createSubDirectoryIfNeeded(at: fullPath)
|
|
try createEmptyFileIfNeeded(at: fullPath, length: file.length)
|
|
}
|
|
}
|
|
|
|
private static func createSubDirectoryIfNeeded(at path: String) throws {
|
|
let directory = URL(fileURLWithPath: path, isDirectory: false).deletingLastPathComponent()
|
|
try createDirectoryIfNeeded(directoryPath: directory.path)
|
|
}
|
|
|
|
private static func createDirectoryIfNeeded(directoryPath: String) throws {
|
|
if (!FileManager.default.fileExists(atPath: directoryPath)) {
|
|
try FileManager.default.createDirectory(atPath: directoryPath,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil)
|
|
}
|
|
}
|
|
|
|
private static func createEmptyFileIfNeeded(at path: String, length: Int) throws {
|
|
|
|
guard !FileManager.default.fileExists(atPath: path) else {
|
|
return
|
|
}
|
|
|
|
guard FileManager.default.createFile(atPath: path, contents: nil, attributes: nil) else {
|
|
throw TorrentFileManagerError.couldNotCreateFile
|
|
}
|
|
|
|
let fileDescriptor: CInt = open(path, O_WRONLY, 0644) // open file for writing
|
|
lseek(fileDescriptor, off_t(length - 1), SEEK_SET) // seek to the last byte ...
|
|
write(fileDescriptor, UnsafeRawPointer([0]), 1) // ... and write a 0 to it
|
|
close(fileDescriptor) // Now we have a file of the correct size we close it
|
|
}
|
|
}
|
|
|
|
// Save/Load progress
|
|
extension TorrentFileManager {
|
|
|
|
private static func sanitizedFileName(from infoHash: Data) -> String {
|
|
let base64EncodedString = String(asciiData: infoHash.base64EncodedData())!
|
|
let sanitizedString = base64EncodedString
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
return sanitizedString + ".torrentprogress"
|
|
}
|
|
|
|
static func saveProgressBitfield(_ bitfield: BitField, infoHash: Data) {
|
|
let fileName = sanitizedFileName(from: infoHash)
|
|
let documentsPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory,
|
|
.userDomainMask,
|
|
true)[0] as String
|
|
let documentsUrl = URL(fileURLWithPath: documentsPath, isDirectory: true)
|
|
let fileURL = documentsUrl.appendingPathComponent(fileName, isDirectory: false)
|
|
try? bitfield.toData().write(to: fileURL)
|
|
}
|
|
|
|
static func loadSavedProgressBitfield(infoHash: Data, size: Int) -> BitField? {
|
|
let fileName = sanitizedFileName(from: infoHash)
|
|
let documentsPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory,
|
|
.userDomainMask,
|
|
true)[0] as String
|
|
let documentsUrl = URL(fileURLWithPath: documentsPath, isDirectory: true)
|
|
let fileURL = documentsUrl.appendingPathComponent(fileName, isDirectory: false)
|
|
if let data = try? Data(contentsOf: fileURL) {
|
|
return BitField(data: data, size: size)
|
|
}
|
|
return nil
|
|
}
|
|
}
|