Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Configurable chunk provider #138

Merged
merged 2 commits into from
Oct 24, 2024
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
56 changes: 37 additions & 19 deletions Sources/InfomaniakCore/Chunking/RangeProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,36 @@ public protocol RangeProvidable {
var fileSize: UInt64 { get throws }
}

public struct RangeProvider: RangeProvidable {
/// Encapsulating API parameters used to compute ranges
public enum APIConstants {
static let chunkMinSize: UInt64 = 1 * 1024 * 1024
static let chunkMaxSizeClient: UInt64 = 50 * 1024 * 1024
static let chunkMaxSizeServer: UInt64 = 1 * 1024 * 1024 * 1024
static let optimalChunkCount: UInt64 = 200
static let maxTotalChunks: UInt64 = 10000
static let minTotalChunks: UInt64 = 1

/// the limit supported by the app
static let fileMaxSizeClient = APIConstants.maxTotalChunks * APIConstants.chunkMaxSizeClient

/// the limit supported by the server
static let fileMaxSizeServer = APIConstants.maxTotalChunks * APIConstants.chunkMaxSizeServer
@frozen public struct RangeProvider: RangeProvidable {
@frozen public struct Config {
public let chunkMinSize: UInt64
public let chunkMaxSizeClient: UInt64
public let chunkMaxSizeServer: UInt64
public let optimalChunkCount: UInt64
public let maxTotalChunks: UInt64
public let minTotalChunks: UInt64

public let fileMaxSizeClient: UInt64
public let fileMaxSizeServer: UInt64

public init(
chunkMinSize: UInt64,
chunkMaxSizeClient: UInt64,
chunkMaxSizeServer: UInt64,
optimalChunkCount: UInt64,
maxTotalChunks: UInt64,
minTotalChunks: UInt64
) {
self.chunkMinSize = chunkMinSize
self.chunkMaxSizeClient = chunkMaxSizeClient
self.chunkMaxSizeServer = chunkMaxSizeServer
self.optimalChunkCount = optimalChunkCount
self.maxTotalChunks = maxTotalChunks
self.minTotalChunks = minTotalChunks

fileMaxSizeClient = maxTotalChunks * chunkMaxSizeClient
fileMaxSizeServer = maxTotalChunks * chunkMaxSizeServer
}
}

enum ErrorDomain: Error {
Expand All @@ -73,8 +88,11 @@ public struct RangeProvider: RangeProvidable {
/// The internal methods split into another type, make testing easier
var guts: RangeProviderGutsable

public init(fileURL: URL) {
guts = RangeProviderGuts(fileURL: fileURL)
let config: Config

public init(fileURL: URL, config: Config) {
guts = RangeProviderGuts(fileURL: fileURL, config: config)
self.config = config
}

public var fileSize: UInt64 {
Expand All @@ -89,8 +107,8 @@ public struct RangeProvider: RangeProvidable {
let size = try fileSize

// Check for files too large to be processed by mobile app or the server
guard size < APIConstants.fileMaxSizeClient,
size < APIConstants.fileMaxSizeServer else {
guard size < config.fileMaxSizeClient,
size < config.fileMaxSizeServer else {
// TODO: notify Sentry
throw ErrorDomain.FileTooLarge
}
Expand Down
16 changes: 9 additions & 7 deletions Sources/InfomaniakCore/Chunking/RangeProviderGuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ public protocol RangeProviderGutsable {
}

/// Subdivided **RangeProvider**, so it is easier to test
public struct RangeProviderGuts: RangeProviderGutsable {
@frozen public struct RangeProviderGuts: RangeProviderGutsable {
/// The URL of the local file to scan
public let fileURL: URL

public let config: RangeProvider.Config

public func buildRanges(fileSize: UInt64, totalChunksCount: UInt64, chunkSize: UInt64) throws -> [DataRange] {
// malformed requests
guard totalChunksCount > 0 else {
Expand Down Expand Up @@ -109,23 +111,23 @@ public struct RangeProviderGuts: RangeProviderGutsable {
public func preferredChunkSize(for fileSize: UInt64) -> UInt64 {
// In extension to reduce memory footprint, we reduce drastically chunk size
guard !Bundle.main.isExtension else {
let capChunkSize = min(fileSize, RangeProvider.APIConstants.chunkMinSize)
let capChunkSize = min(fileSize, config.chunkMinSize)
return capChunkSize
}

let potentialChunkSize = fileSize / RangeProvider.APIConstants.optimalChunkCount
let potentialChunkSize = fileSize / config.optimalChunkCount

let chunkSize: UInt64
switch potentialChunkSize {
case 0 ..< RangeProvider.APIConstants.chunkMinSize:
chunkSize = RangeProvider.APIConstants.chunkMinSize
case 0 ..< config.chunkMinSize:
chunkSize = config.chunkMinSize

case RangeProvider.APIConstants.chunkMinSize ... RangeProvider.APIConstants.chunkMaxSizeClient:
case config.chunkMinSize ... config.chunkMaxSizeClient:
chunkSize = potentialChunkSize

/// Strictly higher than `APIConstants.chunkMaxSize`
default:
chunkSize = RangeProvider.APIConstants.chunkMaxSizeClient
chunkSize = config.chunkMaxSizeClient
}

/// Set a lower bound to chunk size
Expand Down
2 changes: 1 addition & 1 deletion Tests/InfomaniakCoreTests/Chunking/ITChunkProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ final class ITChunkProvider: XCTestCase {

do {
let expectedData = try Data(contentsOf: pathURL)
let rangeProvider = RangeProvider(fileURL: pathURL)
let rangeProvider = RangeProvider(fileURL: pathURL, config: TestRangeProviderConfig.default)
let ranges = try rangeProvider.allRanges
guard let chunkProvider = ChunkProvider(fileURL: pathURL, ranges: ranges) else {
XCTFail("Unexpected")
Expand Down
2 changes: 1 addition & 1 deletion Tests/InfomaniakCoreTests/Chunking/ITRangeProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class ITRangeProvider: XCTestCase {
return
}

let rangeProvider = RangeProvider(fileURL: pathURL)
let rangeProvider = RangeProvider(fileURL: pathURL, config: TestRangeProviderConfig.default)

// WHEN
do {
Expand Down
18 changes: 9 additions & 9 deletions Tests/InfomaniakCoreTests/Chunking/ITRangeProviderGuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class ITRangeProviderGuts: XCTestCase {
let expectedFileBytes = UInt64(4_865_229)
let bundle = Bundle.module
let pathURL = bundle.url(forResource: file, withExtension: "jpg")!
let guts = RangeProviderGuts(fileURL: pathURL)
let guts = RangeProviderGuts(fileURL: pathURL, config: TestRangeProviderConfig.default)

// WHEN
do {
Expand All @@ -46,7 +46,7 @@ final class ITRangeProviderGuts: XCTestCase {
func testReadFileByteSize_FileDoesNotExists() {
// GIVEN
let notThereFileURL = URL(string: "file:///Arcalod_2117.jpg")!
let rangeProvider = RangeProviderGuts(fileURL: notThereFileURL)
let rangeProvider = RangeProviderGuts(fileURL: notThereFileURL, config: TestRangeProviderConfig.default)

// WHEN
do {
Expand All @@ -68,7 +68,7 @@ final class ITRangeProviderGuts: XCTestCase {
// GIVEN
let bundle = Bundle.module
let pathURL = bundle.url(forResource: file, withExtension: "jpg")!
let guts = RangeProviderGuts(fileURL: pathURL)
let guts = RangeProviderGuts(fileURL: pathURL, config: TestRangeProviderConfig.default)
let fileChunks = UInt64(4)
let expectedChunks = 5
let chunksSize = UInt64(1 * 1024 * 1024)
Expand Down Expand Up @@ -98,7 +98,7 @@ final class ITRangeProviderGuts: XCTestCase {

func testBuildRanges_fromEmptyFile() {
// GIVEN
let guts = RangeProviderGuts(fileURL: URL(string: "http://infomaniak.ch")!)
let guts = RangeProviderGuts(fileURL: URL(string: "http://infomaniak.ch")!, config: TestRangeProviderConfig.default)
let emptyFileSize = UInt64(0)
let fileChunks = UInt64(1)
let chunksSize = UInt64(1)
Expand All @@ -122,7 +122,7 @@ final class ITRangeProviderGuts: XCTestCase {
// GIVEN
let bundle = Bundle.module
let pathURL = bundle.url(forResource: file, withExtension: "jpg")!
let guts = RangeProviderGuts(fileURL: pathURL)
let guts = RangeProviderGuts(fileURL: pathURL, config: TestRangeProviderConfig.default)

// WHEN
do {
Expand All @@ -134,16 +134,16 @@ final class ITRangeProviderGuts: XCTestCase {
XCTAssertTrue(preferredChunkSize <= size)

// this should not be strictly imposed but I can quickly check behaviour here
// XCTAssertTrue(preferredChunkSize >= RangeProvider.APIConstants.chunkMinSize)
// XCTAssertTrue(preferredChunkSize <= RangeProvider.APIConstants.chunkMaxSize)
// XCTAssertTrue(preferredChunkSize >= TestRangeProviderConfig.default.chunkMinSize)
// XCTAssertTrue(preferredChunkSize <= TestRangeProviderConfig.default.chunkMaxSize)
} catch {
XCTFail("Unexpected \(error)")
}
}

func testPreferredChunkSize_0() {
// GIVEN
let guts = RangeProviderGuts(fileURL: URL(string: "http://infomaniak.ch")!)
let guts = RangeProviderGuts(fileURL: URL(string: "http://infomaniak.ch")!, config: TestRangeProviderConfig.default)

// WHEN
let preferredChunkSize = guts.preferredChunkSize(for: 0)
Expand All @@ -154,7 +154,7 @@ final class ITRangeProviderGuts: XCTestCase {

func testPreferredChunkSize_notLargerThanFileSize() {
// GIVEN
let guts = RangeProviderGuts(fileURL: URL(string: "http://infomaniak.ch")!)
let guts = RangeProviderGuts(fileURL: URL(string: "http://infomaniak.ch")!, config: TestRangeProviderConfig.default)
let superSmallFileSize: UInt64 = 10

// WHEN
Expand Down
30 changes: 30 additions & 0 deletions Tests/InfomaniakCoreTests/Chunking/TestRangeProviderConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Infomaniak kDrive - iOS App
Copyright (C) 2023 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

@testable import InfomaniakCore

enum TestRangeProviderConfig {
static let `default` = RangeProvider.Config(
chunkMinSize: 1 * 1024 * 1024,
chunkMaxSizeClient: 50 * 1024 * 1024,
chunkMaxSizeServer: 1 * 1024 * 1024 * 1024,
optimalChunkCount: 200,
maxTotalChunks: 10000,
minTotalChunks: 1
)
}
10 changes: 5 additions & 5 deletions Tests/InfomaniakCoreTests/Chunking/UTRangeProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class UTRangeProvider: XCTestCase {
func testAllRanges_zeroes() throws {
// GIVEN
let stubURL = URL(string: "file:///Arcalod_2117.jpg")!
var rangeProvider = RangeProvider(fileURL: stubURL)
var rangeProvider = RangeProvider(fileURL: stubURL, config: TestRangeProviderConfig.default)
let mckGuts = MCKRangeProviderGutsable( /* all zeroes by default */ )

rangeProvider.guts = mckGuts
Expand All @@ -42,9 +42,9 @@ final class UTRangeProvider: XCTestCase {
func testAllRanges_FileTooLarge() throws {
// GIVEN
let stubURL = URL(string: "file:///Arcalod_2117.jpg")!
var rangeProvider = RangeProvider(fileURL: stubURL)
var rangeProvider = RangeProvider(fileURL: stubURL, config: TestRangeProviderConfig.default)
let mckGuts = MCKRangeProviderGutsable()
mckGuts.readFileByteSizeReturnValue = RangeProvider.APIConstants.fileMaxSizeClient + 1
mckGuts.readFileByteSizeReturnValue = TestRangeProviderConfig.default.fileMaxSizeClient + 1
rangeProvider.guts = mckGuts

// WHEN
Expand All @@ -67,9 +67,9 @@ final class UTRangeProvider: XCTestCase {
func testAllRanges_Success() throws {
// GIVEN
let stubURL = URL(string: "file:///Arcalod_2117.jpg")!
var rangeProvider = RangeProvider(fileURL: stubURL)
var rangeProvider = RangeProvider(fileURL: stubURL, config: TestRangeProviderConfig.default)
let mckGuts = MCKRangeProviderGutsable()
mckGuts.readFileByteSizeReturnValue = RangeProvider.APIConstants.chunkMinSize + 1
mckGuts.readFileByteSizeReturnValue = TestRangeProviderConfig.default.chunkMinSize + 1
mckGuts.preferredChunkSizeReturnValue = 1 * 1024 * 1024

rangeProvider.guts = mckGuts
Expand Down
Loading
Loading