Skip to content

Commit 671d352

Browse files
Merge pull request #1 from gal-yedidovich/improvement/async-bytes
Improvement/async bytes
2 parents b114d36 + 6f2849a commit 671d352

4 files changed

Lines changed: 83 additions & 37 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// AsyncChunkedSequence.swift
3+
//
4+
//
5+
// Created by Gal Yedidovich on 05/12/2021.
6+
//
7+
8+
9+
@available(macOS 12.0, iOS 15.0, *)
10+
extension AsyncSequence {
11+
func chunked(countOf chunkSize: Int) -> AsyncChunkedSequence<Self> {
12+
AsyncChunkedSequence(sequence: self, chunkSize: chunkSize)
13+
}
14+
}
15+
16+
@available(macOS 12.0, iOS 15.0, *)
17+
struct AsyncChunkedSequence<AsyncSeq : AsyncSequence>: AsyncSequence {
18+
typealias Element = [AsyncSeq.Element]
19+
20+
let sequence: AsyncSeq
21+
let chunkSize: Int
22+
23+
func makeAsyncIterator() -> AsyncIterator {
24+
AsyncIterator(innerIterator: sequence.makeAsyncIterator(), chunkSize: chunkSize)
25+
}
26+
27+
struct AsyncIterator: AsyncIteratorProtocol {
28+
var innerIterator: AsyncSeq.AsyncIterator
29+
let chunkSize: Int
30+
31+
mutating func next() async throws -> Element? {
32+
var chunk: Element = []
33+
34+
while chunk.count < chunkSize, let value = try await innerIterator.next() {
35+
chunk.append(value)
36+
}
37+
38+
return chunk.isEmpty ? nil : chunk
39+
}
40+
}
41+
}

Sources/SimpleEncryptor/CryptoService/ChaChaPolyService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99
import CryptoKit
1010

1111
struct ChaChaPolyService: CryptoService {
12-
private static let BUFFER_SIZE = 1024 * 32
12+
static let BUFFER_SIZE = 1024 * 32
1313

1414
func encrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
1515
try ChaChaPoly.seal(data, using: key).combined

Sources/SimpleEncryptor/CryptoService/FileProcessing.swift

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,28 @@ import CryptoKit
1010

1111
typealias Operation = (Data) throws -> Data
1212
typealias FinalOperation = () throws -> Data
13-
typealias StreamsBlock = (InputStream, OutputStream) async throws -> ()
13+
typealias SafeStreamBlock = (OutputStream) async throws -> ()
1414

1515
@available(macOS 12.0, iOS 15.0, *)
1616
func process(file src: URL, to dest: URL, using key: SymmetricKey, bufferSize: Int = 32 * 1000,
1717
operation: Operation, finalOperation: FinalOperation? = nil,
1818
onProgress: OnProgress?) async throws {
19-
try await stream(from: src, to: dest) { input, output in
20-
21-
guard let fileSize = src.fileSize else {
22-
throw ProccessingError.fileNotFound
23-
}
24-
19+
guard let fileSize = src.fileSize else {
20+
throw ProccessingError.fileNotFound
21+
}
22+
23+
try await safeStream(to: dest) { output in
2524
var offset: Int = 0
2625
var count = 0
2726

28-
try await input.readAll(bufferSize: bufferSize) { buffer, bytesRead in
29-
offset += bytesRead
27+
let batches = src.resourceBytes.chunked(countOf: bufferSize)
28+
for try await batch in batches {
29+
offset += batch.count
3030
onProgress?(Int((offset * 100) / fileSize))
3131

32-
let data = Data(bytes: buffer, count: bytesRead)
33-
output.write(data: try operation(data))
32+
let processedData = try operation(Data(batch))
33+
output.write(data: processedData)
34+
3435
count = (count + 1) % 10
3536
if count == 0 {
3637
await Task.yield()
@@ -44,24 +45,21 @@ func process(file src: URL, to dest: URL, using key: SymmetricKey, bufferSize: I
4445
}
4546

4647
@available(macOS 12.0, iOS 15.0, *)
47-
private func stream(from src: URL, to dest: URL, operation: StreamsBlock) async throws {
48+
private func safeStream(to dest: URL, operation: SafeStreamBlock) async throws {
4849
let fm = FileManager.default
4950

5051
let tempDir = fm.temporaryDirectory
5152
try fm.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)
5253
let tempFile = tempDir.appendingPathComponent(UUID().uuidString)
5354

54-
guard let input = InputStream(url: src) else {
55-
throw ProccessingError.failedToCreateInputStream
56-
}
5755
guard let output = OutputStream(url: tempFile, append: false) else {
5856
throw ProccessingError.failedToCreateOutputStream
5957
}
6058

6159
output.open()
6260
defer { output.close() }
6361

64-
try await operation(input, output)
62+
try await operation(output)
6563

6664
if fm.fileExists(atPath: dest.path) {
6765
try fm.removeItem(at: dest)
@@ -78,24 +76,6 @@ extension URL {
7876
}
7977
}
8078

81-
extension InputStream {
82-
typealias Buffer = [UInt8]
83-
84-
@available(macOS 12.0, iOS 15.0, *)
85-
func readAll(bufferSize: Int, block: (Buffer, Int) async throws -> Void) async rethrows {
86-
open()
87-
defer { close() }
88-
89-
var buffer = [UInt8](repeating: 0, count: bufferSize)
90-
while hasBytesAvailable {
91-
let bytesRead = read(&buffer, maxLength: buffer.count)
92-
guard bytesRead > 0 else { break }
93-
94-
try await block(buffer, bytesRead)
95-
}
96-
}
97-
}
98-
9979
extension OutputStream {
10080
func write(data: Data) {
10181
let buffer = [UInt8](data)
@@ -105,13 +85,11 @@ extension OutputStream {
10585

10686
enum ProccessingError: LocalizedError {
10787
case fileNotFound
108-
case failedToCreateInputStream
10988
case failedToCreateOutputStream
11089

11190
var errorDescription: String? {
11291
switch self {
11392
case .fileNotFound: return "Source file not found"
114-
case .failedToCreateInputStream: return "Failed to create input stream from source file"
11593
case .failedToCreateOutputStream: return "Failed to create output stream to destination file"
11694
}
11795
}

Tests/Tests/SimpleEncryptorTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,33 @@ class SimpleEncryptorTests: XCTestCase {
5757
}
5858
}
5959

60+
@available(macOS 12.0, iOS 15.0, *)
61+
func testShouldCallProgressTenTimes() async throws {
62+
//Given
63+
let BUFFER_SIZE: Int = ChaChaPolyService.BUFFER_SIZE
64+
let EXPECTED_NUMBER_OF_STEPS = 10
65+
66+
let encryptor = SimpleEncryptor(type: .chachaPoly, keyService: MockKeyService())
67+
let data = Data(randomString(length: BUFFER_SIZE * EXPECTED_NUMBER_OF_STEPS).utf8)
68+
var count = 0
69+
70+
let baseURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
71+
let url = baseURL.appendingPathComponent("data.txt")
72+
let encUrl = baseURL.appendingPathComponent("enc_data.txt")
73+
try data.write(to: url)
74+
75+
//When
76+
try await encryptor.encrypt(file: url, to: encUrl) { progress in
77+
count += 1
78+
}
79+
80+
//Then
81+
XCTAssertEqual(count, EXPECTED_NUMBER_OF_STEPS)
82+
83+
try FileManager.default.removeItem(at: url)
84+
try FileManager.default.removeItem(at: encUrl)
85+
}
86+
6087
private func testDataEncryption(withType type: CryptoServiceType) throws {
6188
//Given
6289
let encryptor = SimpleEncryptor(type: type, keyService: MockKeyService())

0 commit comments

Comments
 (0)