Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Features

- Feature: Alternative `readFile` and `readFileInChunks` methods with optional `length` and `offset` parameters.

## 1.0.1

### Fixes
Expand Down
34 changes: 30 additions & 4 deletions IONFilesystemLib/IONFILEChunkPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ public class IONFILEChunkPublisher: Publisher {
private let url: URL
private let chunkSize: Int
private let encoding: IONFILEEncoding
private let offset: Int
private let length: Int

init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding) {
init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding, _ offset: Int = 0, _ length: Int = -1) {
self.url = url
self.chunkSize = chunkSize
self.encoding = encoding
self.offset = offset
self.length = length
}

public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = IONFILEChunkSubscription(url, chunkSize, encoding, subscriber)
let subscription = IONFILEChunkSubscription(url, chunkSize, encoding, offset, length, subscriber)
subscriber.receive(subscription: subscription)
}
}
Expand All @@ -25,24 +29,46 @@ private class IONFILEChunkSubscription<S: Subscriber>: Subscription where S.Inpu
private let fileHandle: FileHandle?
private let chunkSize: Int
private let encoding: IONFILEEncoding
private let offset: Int
private let length: Int
private let subscriber: S
private var isCompleted = false

init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding, _ subscriber: S) {
init(_ url: URL, _ chunkSize: Int, _ encoding: IONFILEEncoding, _ offset: Int = 0, _ length: Int = -1, _ subscriber: S) {
self.fileHandle = try? FileHandle(forReadingFrom: url)
self.chunkSize = chunkSize
self.encoding = encoding
self.subscriber = subscriber
self.offset = offset
self.length = length
}

func request(_ demand: Subscribers.Demand) {
guard let fileHandle = fileHandle, !isCompleted else {
return subscriber.receive(completion: .failure(IONFILEChunkPublisherError.notAbleToReadFile))
}

if (offset > 0) {
do {
try fileHandle.seek(toOffset: UInt64(offset))
} catch {
complete(withValue: .failure(error))
return
}
}

var remainingToRead = Int.max
if (length > 0) {
remainingToRead = length
}
while demand > .none {
do {
if let chunk = try fileHandle.read(upToCount: chunkSize), !chunk.isEmpty {
var readCount = chunkSize
if (length > 0) {
readCount = min(readCount, remainingToRead)
}
if readCount > 0, let chunk = try fileHandle.read(upToCount: readCount), !chunk.isEmpty {
remainingToRead -= chunk.count
let chunkToEmit: IONFILEEncodingValueMapper
switch encoding {
case .byteBuffer: chunkToEmit = .byteBuffer(value: chunk)
Expand Down
2 changes: 2 additions & 0 deletions IONFilesystemLib/IONFILEManager+Protocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public protocol IONFILEDirectoryManager {
public protocol IONFILEFileManager {
func readEntireFile(atURL: URL, withEncoding: IONFILEEncoding) throws -> IONFILEEncodingValueMapper
func readFileInChunks(atURL: URL, withEncoding: IONFILEEncoding, andChunkSize: Int) throws -> IONFILEChunkPublisher
func readEntireFile(atURL: URL, withEncoding: IONFILEEncoding, andOffset: Int, andLength: Int) throws -> IONFILEEncodingValueMapper
func readFileInChunks(atURL: URL, withEncoding: IONFILEEncoding, andChunkSize: Int, andOffset: Int, andLength: Int) throws -> IONFILEChunkPublisher
func getFileURL(atPath: String, withSearchPath: IONFILESearchPath) throws -> URL
func deleteFile(atURL: URL) throws
func saveFile(atURL: URL, withEncodingAndData: IONFILEEncodingValueMapper, includeIntermediateDirectories: Bool) throws
Expand Down
62 changes: 51 additions & 11 deletions IONFilesystemLib/IONFILEManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,35 @@ extension IONFILEManager: IONFILEDirectoryManager {

extension IONFILEManager: IONFILEFileManager {
public func readEntireFile(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding) throws -> IONFILEEncodingValueMapper {
try readEntireFile(atURL: fileURL, withEncoding: encoding, andOffset: 0, andLength: -1)
}

public func readFileInChunks(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding, andChunkSize chunkSize: Int) throws -> IONFILEChunkPublisher {
try readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: chunkSize, andOffset: 0, andLength: -1)
}

public func readEntireFile(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding, andOffset offset: Int, andLength length: Int) throws -> IONFILEEncodingValueMapper {
try withSecurityScopedAccess(to: fileURL) {
let result: IONFILEEncodingValueMapper
switch encoding {
case .byteBuffer:
let fileData = try readFileAsByteBuffer(from: fileURL)
result = .byteBuffer(value: fileData)
case .string(let stringEncoding):
let fileData = try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding)
result = .string(encoding: stringEncoding, value: fileData)
if (offset > 0 || length > 0) {
result = try readPartialFile(fileURL, encoding, offset, length)
} else {
switch encoding {
case .byteBuffer:
let fileData = try readFileAsByteBuffer(from: fileURL)
result = .byteBuffer(value: fileData)
case .string(let stringEncoding):
let fileData = try readFileAsString(from: fileURL, using: stringEncoding.stringEncoding)
result = .string(encoding: stringEncoding, value: fileData)
}
}

return result
}
}

public func readFileInChunks(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding, andChunkSize chunkSize: Int) throws -> IONFILEChunkPublisher {
public func readFileInChunks(atURL fileURL: URL, withEncoding encoding: IONFILEEncoding, andChunkSize chunkSize: Int, andOffset offset: Int, andLength length: Int) throws -> IONFILEChunkPublisher {
try withSecurityScopedAccess(to: fileURL) {
.init(fileURL, chunkSize, encoding)
.init(fileURL, chunkSize, encoding, offset, length)
}
}

Expand Down Expand Up @@ -179,6 +190,35 @@ private extension IONFILEManager {

return try operation()
}

func readPartialFile(_ fileURL: URL, _ encoding: IONFILEEncoding, _ offset: Int, _ length: Int) throws -> IONFILEEncodingValueMapper {
let fileHandle = try FileHandle(forReadingFrom: fileURL)
defer {
try? fileHandle.close()
}

if (offset > 0) {
try fileHandle.seek(toOffset: UInt64(offset))
}
let data: Data?
if (length > 0) {
data = try fileHandle.read(upToCount: length)
} else {
data = try fileHandle.readToEnd()
}

let nonNilData: Data = data ?? Data()

switch encoding {
case .byteBuffer:
return .byteBuffer(value: nonNilData)
case .string(let stringEncoding):
guard let stringData = String(data: nonNilData, encoding: stringEncoding.stringEncoding) else {
throw IONFILEFileManagerError.cantDecodeData(usingEncoding: stringEncoding)
}
return .string(encoding: stringEncoding, value: stringData)
}
}

func readFileAsByteBuffer(from fileURL: URL) throws -> Data {
try Data(contentsOf: fileURL)
Expand Down
95 changes: 91 additions & 4 deletions IONFilesystemLibTests/IONFILEFileManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,63 @@ extension IONFILEFileManagerTests {
// When and Then
XCTAssertThrowsError(try fetchEntireContent(forURL: fileURL, withEncoding: .string(encoding: .utf8)))
}

func test_readEntireFile_offsetAndLengthWithExampleEncoding_returnsCorrectData() throws {
// Given
createFileManager()
let fileURL = try XCTUnwrap(fetchConfigurationFile())
let offset = 7
let length = 5
let encoding: IONFILEStringEncoding = .utf8

// When
let result = try sut.readEntireFile(atURL: fileURL, withEncoding: .string(encoding: encoding), andOffset: offset, andLength: length)

// Then
guard case .string(let resultEncoding, let resultValue) = result else {
XCTFail("Wrong result type")
return
}
XCTAssertEqual(resultEncoding, encoding)
XCTAssertEqual(resultValue, "world")
}

func test_readEntire_offsetAndLengthWithByteBufferEncoding_returnsCorrectData() throws {
// Given
createFileManager()
let fileURL = try XCTUnwrap(fetchConfigurationFile())
let offset = 7
let length = 5

// When
let result = try sut.readEntireFile(atURL: fileURL, withEncoding: .byteBuffer, andOffset: offset, andLength: length)

// Then
guard case .byteBuffer(let resultValue) = result else {
XCTFail("Wrong result type")
return
}
XCTAssertEqual(String(data: resultValue, encoding: .utf8), "world")
}

func test_readEntireFile_offsetLargerThanEOF_returnsEmptyString() throws {
// Given
createFileManager()
let fileURL = try XCTUnwrap(fetchConfigurationFile())
let offset = 1000
let length = 5
let encoding: IONFILEStringEncoding = .utf8

// When
let result = try sut.readEntireFile(atURL: fileURL, withEncoding: .string(encoding: encoding), andOffset: offset, andLength: length)

// Then
guard case .string(let resultEncoding, let resultValue) = result else {
XCTFail("Wrong result type")
return
}
XCTAssertEqual(resultValue, "")
}
}

// MARK: - 'readFileInChunks' tests
Expand Down Expand Up @@ -104,6 +161,36 @@ extension IONFILEFileManagerTests {
// When and Then
XCTAssertThrowsError(try fetchChunkedContent(forURL: fileURL, withEncoding: .string(encoding: .utf8)))
}

func test_readFileInChunks_withOffsetAndLength_returnsContentSuccessfully() throws {
// Given
createFileManager()
let offset = 7
let length = 5

// When
let fileContent = try fetchChunkedContent(
forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8), andOffset: offset, andLength: length
)

// Then
XCTAssertEqual(fileContent, "world")
}

func test_readFileInChunks_offsetLargerThanEOF_returnsEmptyString() throws {
// Given
createFileManager()
let offset = 1000
let length = 5

// When
let fileContent = try fetchChunkedContent(
forFile: (Configuration.fileName, Configuration.fileExtension), withEncoding: .string(encoding: .utf8), andOffset: offset, andLength: length
)

// Then
XCTAssertEqual(fileContent, "")
}
}

// MARK: - 'getFileURL' tests
Expand Down Expand Up @@ -870,13 +957,13 @@ private extension IONFILEFileManagerTests {
return try treat(content: content, withEncoding: encoding)
}

func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false) throws -> String {
func fetchChunkedContent(forFile file: (name: String, extension: String), withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false, andOffset offset: Int = 0, andLength length: Int = -1) throws -> String {
let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: file.name, withExtension: file.extension))
return try fetchChunkedContent(forURL: fileURL, withEncoding: encoding, forceURLError: forceURLError)
return try fetchChunkedContent(forURL: fileURL, withEncoding: encoding, forceURLError: forceURLError, andOffset: offset, andLength: length)
}

@discardableResult
func fetchChunkedContent(forURL url: URL, withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false) throws -> String {
func fetchChunkedContent(forURL url: URL, withEncoding encoding: IONFILEEncoding, forceURLError: Bool = false, andOffset offset: Int = 0, andLength length: Int = -1) throws -> String {
var fileURL = url
var contentArray = [String]()
var error: Error?
Expand All @@ -885,7 +972,7 @@ private extension IONFILEFileManagerTests {
if forceURLError {
fileURL.deleteLastPathComponent()
}
try sut.readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: 3) // 3 bytes
try sut.readFileInChunks(atURL: fileURL, withEncoding: encoding, andChunkSize: 3, andOffset: offset, andLength: length) // 3 bytes
.sink(receiveCompletion: { completion in
if case .failure(let failure) = completion {
error = failure
Expand Down
Loading