Skip to content

Commit 62485c2

Browse files
committed
Remove stored file size property from ChunkedFile (#50)
Store fileURL inside ChunkedFile when an active handle is available Simplify state checking inside ChunkedFile Prevent potential crash due to overflow Remove SIZE_UNKNOWN Keep a reference to FileManager for eventual dependency injection when initializing ChunkedFile Add extension method to work around Swift compiler buffoonery
1 parent 3ed2f28 commit 62485c2

File tree

4 files changed

+71
-44
lines changed

4 files changed

+71
-44
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// FileManager+FileOperations.swift
3+
//
4+
5+
import Foundation
6+
7+
extension FileManager {
8+
9+
// Work around Swift compiler not bridging Dictionary
10+
// and NSDictionary properly when calling attributesOfItem
11+
func fileSizeOfItem(
12+
atPath path: String
13+
) throws -> UInt64 {
14+
(try attributesOfItem(atPath: path) as NSDictionary)
15+
.fileSize()
16+
}
17+
}

Sources/MuxUploadSDK/InternalUtilities/ChunkedFile.swift

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ import Foundation
1313
/// Buffers are allocated for each new chunk so they can safely escape to other threads
1414
/// Call ``close`` when you're done with this object
1515
class ChunkedFile {
16-
17-
static let SIZE_UNKNOWN: UInt64 = 0
18-
/// The size of the file. Call ``open`` to populate this with a real value, otherwise it will be ``SIZE_UNKNOWN``
19-
var fileSize: UInt64 {
20-
return _fileSize
16+
17+
private struct State {
18+
var fileHandle: FileHandle
19+
var fileURL: URL
20+
var filePosition: UInt64 = 0
2121
}
22-
22+
2323
private let chunkSize: Int
24-
25-
private var fileHandle: FileHandle?
26-
private var filePos: UInt64 = 0
27-
private var _fileSize: UInt64 = SIZE_UNKNOWN
24+
25+
var fileManager = FileManager.default
26+
27+
private var state: State?
28+
29+
private var fileHandle: FileHandle? {
30+
state?.fileHandle
31+
}
32+
private var fileURL: URL? {
33+
state?.fileURL
34+
}
35+
private var filePos: UInt64 {
36+
state?.filePosition ?? 0
37+
}
2838

2939
/// Reads the next chunk from the file, advancing the file for the next read
3040
/// This method does synchronous I/O, so call it in the background
@@ -39,20 +49,19 @@ class ChunkedFile {
3949
return Result.failure(ChunkedFileError.fileHandle(error))
4050
}
4151
}
42-
52+
4353
/// Opens the internal file ahead of time. Calling this is optional, but it's available
4454
/// Calling this multiple times (on the same thread) will have no effect unless you also ``close`` it
4555
/// Throws if the file couldn't be opened
4656
func openFile(fileURL: URL) throws {
47-
if fileHandle == nil {
57+
if state == nil {
4858
do {
49-
guard let fileSize = try FileManager.default.attributesOfItem(atPath: fileURL.path)[FileAttributeKey.size] as? UInt64 else {
50-
throw ChunkedFileError.invalidState("Cannot retrieve file size")
51-
}
52-
self._fileSize = fileSize
53-
54-
let handle = try FileHandle(forReadingFrom: fileURL)
55-
fileHandle = handle
59+
let fileSize = try fileManager.fileSizeOfItem(atPath: fileURL.path)
60+
let fileHandle = try FileHandle(forReadingFrom: fileURL)
61+
state = State(
62+
fileHandle: fileHandle,
63+
fileURL: fileURL
64+
)
5665
MuxUploadSDK.logger?.info("Opened file with len \(String(describing: fileSize)) at path \(fileURL.path)")
5766
} catch {
5867
throw ChunkedFileError.fileHandle(error)
@@ -68,23 +77,26 @@ class ChunkedFile {
6877
} catch {
6978
MuxUploadSDK.logger?.warning("Swallowed error closing file: \(error.localizedDescription)")
7079
}
71-
fileHandle = nil
72-
filePos = 0
73-
_fileSize = ChunkedFile.SIZE_UNKNOWN
80+
state = nil
7481
}
7582

7683
func seekTo(byte: UInt64) throws {
7784
// Worst case: we start from the begining and there's a few very quick chunk successes
7885
try fileHandle?.seek(toOffset: byte)
79-
filePos = byte
86+
state?.filePosition = byte
8087
}
8188

8289
private func doReadNextChunk() throws -> FileChunk {
8390
MuxUploadSDK.logger?.info("--doReadNextChunk")
84-
guard let fileHandle = fileHandle else {
91+
guard let fileHandle = fileHandle, let fileURL = fileURL else {
8592
throw ChunkedFileError.invalidState("doReadNextChunk called without file handle. Did you call open()?")
8693
}
8794
let data = try fileHandle.read(upToCount: chunkSize)
95+
96+
let fileSize = try fileManager.fileSizeOfItem(
97+
atPath: fileURL.path
98+
)
99+
88100
guard let data = data else {
89101
// Called while already at the end of the file. We read zero bytes, "ending" at the end of the file
90102
return FileChunk(startByte: fileSize, endByte: fileSize, totalFileSize: fileSize, chunkData: Data(capacity: 0))
@@ -100,7 +112,7 @@ class ChunkedFile {
100112
chunkData: data
101113
)
102114

103-
self.filePos = newFilePos
115+
state?.filePosition = newFilePos
104116

105117
return chunk
106118
}

Sources/MuxUploadSDK/Upload/ChunkedFileUploader.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ class ChunkedFileUploader {
8888
let task = Task.detached { [self] in
8989
do {
9090
// It's fine if it's already open, that's handled by ignoring the call
91-
let fileSize = file.fileSize
91+
let fileSize = try FileManager.default.fileSizeOfItem(
92+
atPath: uploadInfo.videoFile.path
93+
)
9294
let result = try await makeWorker().performUpload()
9395
file.close()
9496

@@ -240,9 +242,19 @@ fileprivate actor Worker {
240242
try chunkedFile.seekTo(byte: startingReadCount)
241243

242244
let startTime = Date().timeIntervalSince1970
243-
244-
let fileSize = chunkedFile.fileSize
245-
let wideFileSize = Int64(fileSize)
245+
let fileSize = try FileManager.default.fileSizeOfItem(
246+
atPath: uploadInfo.videoFile.path
247+
)
248+
249+
let wideFileSize: Int64
250+
251+
// Prevent overflow if UInt64 exceeds Int64.max
252+
if fileSize >= Int64.max {
253+
wideFileSize = Int64.max
254+
} else {
255+
wideFileSize = Int64(fileSize)
256+
}
257+
246258
overallProgress.totalUnitCount = wideFileSize
247259
overallProgress.isCancellable = false
248260
overallProgress.completedUnitCount = Int64(startingReadCount)

Tests/MuxUploadSDKTests/Internal Utilities Tests/ChunkedFileTests.swift

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,7 @@ final class ChunkedFileTests: XCTestCase {
7777

7878
func testMisuseBeforeOpen() throws {
7979
let file = ChunkedFile(chunkSize: 10 * 1000 * 1000)
80-
81-
let fileSizeBeforeOpen = file.fileSize
82-
XCTAssertEqual(
83-
fileSizeBeforeOpen,
84-
ChunkedFile.SIZE_UNKNOWN,
85-
"File size should not be known before open"
86-
)
87-
80+
8881
let readResult = file.readNextChunk()
8982
switch readResult {
9083
case .success(_): return XCTFail("readNextChunk should fail before open")
@@ -104,13 +97,6 @@ final class ChunkedFileTests: XCTestCase {
10497
try file.openFile(fileURL: testFileURL)
10598
file.close()
10699

107-
let fileSizeAfterClose = file.fileSize
108-
XCTAssertEqual(
109-
fileSizeAfterClose,
110-
ChunkedFile.SIZE_UNKNOWN,
111-
"File size should not be known after close"
112-
)
113-
114100
let chunkResult = file.readNextChunk()
115101
switch chunkResult {
116102
case .success(_): return XCTFail("read after close should not succeed")

0 commit comments

Comments
 (0)