├── .gitattributes ├── .github └── workflows │ └── Tests.yml ├── .gitignore ├── .swiftlint.yml ├── FileStreamer ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard └── Info.plist ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── FileStreamer │ ├── FSReader.swift │ └── FSWriter.swift ├── Tests └── FileStreamerTests │ ├── FileStreamerTests.swift │ └── Info.plist └── temp.bundle ├── demo.mp3 └── test.txt /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eonist/FileStreamer/39b1790f3f2bbd520c17de39d7d2d92c5eb79b2c/.gitattributes -------------------------------------------------------------------------------- /.github/workflows/Tests.yml: -------------------------------------------------------------------------------- 1 | # Updated 2024-07-21 2 | name: Tests 3 | 4 | on: [push] 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: macOS-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Build 14 | run: swift build -v 15 | - name: Run tests 16 | run: swift test -v 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - anyobject_protocol 3 | - array_init 4 | #- attributes 5 | - block_based_kvo 6 | - class_delegate_protocol 7 | - closing_brace 8 | - closure_end_indentation 9 | - closure_parameter_position 10 | - closure_spacing 11 | - collection_alignment 12 | - colon 13 | - comma 14 | - compiler_protocol_init 15 | # - conditional_returns_on_newline 16 | - contains_over_first_not_nil 17 | - control_statement 18 | - deployment_target 19 | - discarded_notification_center_observer 20 | - discouraged_direct_init 21 | - discouraged_object_literal 22 | - discouraged_optional_boolean 23 | # - discouraged_optional_collection 24 | - duplicate_imports 25 | - dynamic_inline 26 | - empty_count 27 | - empty_enum_arguments 28 | - empty_parameters 29 | - empty_parentheses_with_trailing_closure 30 | - empty_string 31 | - empty_xctest_method 32 | - explicit_init 33 | - fallthrough 34 | - fatal_error_message 35 | - first_where 36 | - for_where 37 | - generic_type_name 38 | - identical_operands 39 | - identifier_name 40 | - implicit_getter 41 | - implicit_return 42 | - inert_defer 43 | - is_disjoint 44 | - joined_default_parameter 45 | - last_where 46 | - leading_whitespace 47 | - legacy_cggeometry_functions 48 | - legacy_constant 49 | - legacy_constructor 50 | - legacy_hashing 51 | - legacy_nsgeometry_functions 52 | - legacy_random 53 | - literal_expression_end_indentation 54 | - lower_acl_than_parent 55 | - mark 56 | - modifier_order 57 | - multiline_arguments 58 | - multiline_function_chains 59 | - multiline_literal_brackets 60 | - multiline_parameters 61 | - multiline_parameters_brackets 62 | - multiple_closures_with_trailing_closure 63 | - nimble_operator 64 | - no_extension_access_modifier 65 | - no_fallthrough_only 66 | - notification_center_detachment 67 | - number_separator 68 | - object_literal 69 | - opening_brace 70 | - operator_usage_whitespace 71 | - operator_whitespace 72 | - overridden_super_call 73 | - pattern_matching_keywords 74 | - private_action 75 | # - private_outlet 76 | - private_unit_test 77 | - prohibited_super_call 78 | - protocol_property_accessors_order 79 | - redundant_discardable_let 80 | - redundant_nil_coalescing 81 | - redundant_objc_attribute 82 | - redundant_optional_initialization 83 | - redundant_set_access_control 84 | - redundant_string_enum_value 85 | - redundant_type_annotation 86 | - redundant_void_return 87 | - required_enum_case 88 | - return_arrow_whitespace 89 | - shorthand_operator 90 | - sorted_first_last 91 | # - statement_position 92 | - static_operator 93 | # - strong_iboutlet 94 | - superfluous_disable_command 95 | - switch_case_alignment 96 | # - switch_case_on_newline 97 | - syntactic_sugar 98 | - todo 99 | - toggle_bool 100 | - trailing_closure 101 | - trailing_comma 102 | - trailing_newline 103 | - trailing_semicolon 104 | - trailing_whitespace 105 | - type_name 106 | # - unavailable_function 107 | - unneeded_break_in_switch 108 | - unneeded_parentheses_in_closure_argument 109 | #- untyped_error_in_catch 110 | - unused_closure_parameter 111 | - unused_control_flow_label 112 | - unused_enumerated 113 | - unused_optional_binding 114 | - unused_setter_value 115 | - valid_ibinspectable 116 | - vertical_parameter_alignment 117 | - vertical_parameter_alignment_on_call 118 | - vertical_whitespace_closing_braces 119 | - vertical_whitespace_opening_braces 120 | - void_return 121 | - weak_computed_property 122 | - weak_delegate 123 | - xct_specific_matcher 124 | - xctfail_message 125 | - yoda_condition 126 | analyzer_rules: 127 | - unused_import 128 | - unused_private_declaration 129 | force_cast: warning 130 | force_unwrapping: warning 131 | number_separator: 132 | minimum_length: 5 133 | object_literal: 134 | image_literal: false 135 | discouraged_object_literal: 136 | color_literal: false 137 | identifier_name: 138 | max_length: 139 | warning: 100 140 | error: 100 141 | min_length: 142 | warning: 1 143 | error: 1 144 | validates_start_with_lowercase: false 145 | allowed_symbols: 146 | - '_' 147 | excluded: 148 | - 'x' 149 | - 'y' 150 | - 'a' 151 | - 'b' 152 | - 'x1' 153 | - 'x2' 154 | - 'y1' 155 | - 'y2' 156 | macOS_deployment_target: '10.12' 157 | -------------------------------------------------------------------------------- /FileStreamer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | @UIApplicationMain 3 | class AppDelegate: UIResponder, UIApplicationDelegate { 4 | lazy var window: UIWindow? = { 5 | let win = UIWindow(frame: UIScreen.main.bounds) 6 | let viewController = ViewController() 7 | win.rootViewController = viewController 8 | win.makeKeyAndVisible() // Important since we have no Main storyboard anymore 9 | return win 10 | }() 11 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 12 | _ = window 13 | return true 14 | } 15 | } 16 | class ViewController: UIViewController { 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | view = View() 20 | view.backgroundColor = .orange 21 | } 22 | override var prefersStatusBarHidden: Bool { false } 23 | } 24 | class View: UIView { 25 | override init(frame: CGRect) { 26 | super.init(frame: frame) 27 | } 28 | /** 29 | * Boilerplate 30 | */ 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FileStreamer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /FileStreamer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /FileStreamer/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FileStreamer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ResourceHelper", 6 | "repositoryURL": "https://github.com/eonist/ResourceHelper.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "4359edc3fe4f9fa2a78c95261ff1327f3e433780", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "FileStreamer", 6 | products: [ 7 | .library( 8 | name: "FileStreamer", 9 | targets: ["FileStreamer"]) 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/eonist/ResourceHelper.git", .branch("master")) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "FileStreamer", 17 | dependencies: []), 18 | .testTarget( 19 | name: "FileStreamerTests", 20 | dependencies: ["FileStreamer", "ResourceHelper"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Lang](https://img.shields.io/badge/Language-Swift-orange.svg) 2 | ![mit](https://img.shields.io/badge/License-MIT-brightgreen.svg) 3 | ![Tests](https://github.com/light-stream/FileStreamer/workflows/Tests/badge.svg) 4 | 5 | # FileStreamer 6 | > Continuous data to disk 7 | 8 | ### Installation: 9 | - SPM `"https://github.com/light-stream/FileStreamer.git"` branch: `"master"` 10 | 11 | ### Writer example: 12 | ```swift 13 | let filePath: String = NSString(string: "~/Desktop/del.txt").expandingTildeInPath 14 | guard let data: Data = ("black dog" as NSString).data(using: String.Encoding.utf8.rawValue) else {Swift.print("unable to create data");return} 15 | FSWriter.write(url: URL(filePath), data: data, index: 0) 16 | ``` 17 | 18 | ### Reader example: 19 | ```swift 20 | let filePath: String = NSString(string: "~/Desktop/del.txt").expandingTildeInPath 21 | let data: Data = FSReader.read(filePath: filePath, startIndex: 50, endIndex: 100) 22 | Swift.print("\(String(data: data, encoding: .utf8))") // blalbslalballabalbla... 23 | ``` 24 | 25 | ### Size example: 26 | ```swift 27 | let fileSize = FSReader.fileSize(filePath: filePath) 28 | ``` 29 | -------------------------------------------------------------------------------- /Sources/FileStreamer/FSReader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | /** 3 | * Reads data from a file (Continiously) 4 | */ 5 | public class FSReader { 6 | /** 7 | * Reads data from filepath 8 | * ## Examples: 9 | * let filePath: String = NSString(string: "~/Desktop/del.txt").expandingTildeInPath 10 | * let data: Data = FileStreamReader.read(filePath: filePath, startIndex: 50, endIndex: 100) 11 | * Swift.print("\(String(data: data, encoding: .utf8))") // blalbslalballabalbla... 12 | */ 13 | public static func read(url: URL, startIndex: UInt64, endIndex: UInt64) throws -> Data { 14 | do { 15 | let file: FileHandle = try .init(forReadingFrom: url) 16 | file.seek(toFileOffset: startIndex) 17 | let length: Int = .init(endIndex - startIndex) 18 | let databuffer = file.readData(ofLength: length) 19 | file.closeFile() 20 | return databuffer 21 | } 22 | catch { throw ReaderError.initError(error: error, forPath: url.path) } 23 | } 24 | } 25 | /** 26 | * Helper 27 | */ 28 | extension FSReader { 29 | /** 30 | * Support for filePath 31 | */ 32 | public static func read(filePath: String, startIndex: UInt64, endIndex: UInt64) throws -> Data { 33 | let url: URL = .init(fileURLWithPath: filePath) 34 | return try read(url: url, startIndex: startIndex, endIndex: endIndex) 35 | } 36 | /** 37 | * Read string 38 | */ 39 | static func read(filePath: String, start: UInt64, end: UInt64) throws -> String { 40 | let data: Data = try read(filePath: filePath, startIndex: start, endIndex: end) 41 | guard let string = String(data: data, encoding: .utf8) else { throw ReaderError.unableToGetStringFromData(dataLength: data.count) } 42 | return string 43 | } 44 | /** 45 | * Returns filesize for a filePath 46 | * ## Examples: 47 | * let fileSize = FileStreamReader.fileSize(filePath: filePath) 48 | * - Note: same as doing `data.count` 49 | */ 50 | public static func fileSize(filePath: String) throws -> UInt64 { 51 | let fileUrl = URL(fileURLWithPath: filePath) 52 | let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: (fileUrl.path)) 53 | return attributes[FileAttributeKey.size] as? UInt64 ?? (attributes as NSDictionary).fileSize() 54 | } 55 | } 56 | /** 57 | * Error 58 | */ 59 | extension FSReader { 60 | enum ReaderError: Error { 61 | case initError(error: Error, forPath: String) 62 | case unableToGetStringFromData(dataLength: Int) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/FileStreamer/FSWriter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | /** 3 | * Writes data to a file (Continiously) 4 | */ 5 | public final class FSWriter { 6 | /** 7 | * Reads data from filepath 8 | * - Important: ⚠️️ This method writes over the data that is already there (It does not insert) 9 | * - Note: https://stackoverflow.com/questions/37981375/nsfilehandle-updateatpath-how-can-i-update-file-instead-of-overwriting 10 | * ## Examples: 11 | * let filePath: String = NSString(string: "~/Desktop/del.txt").expandingTildeInPath 12 | * guard let data: Data = ("black dog" as NSString).data(using: String.Encoding.utf8.rawValue) else {Swift.print("unable to create data");return} 13 | * FileStreamWriter.write(url: URL(filePath), data: data, index: 0) 14 | */ 15 | public static func write(url: URL, data: Data, index: UInt64) throws { 16 | let fileExists: Bool = FileManager().fileExists(atPath: url.path) 17 | if fileExists == false { 18 | do { try data.write(to: url, options: .atomic) } // Make the file, since it didn't exist 19 | catch { throw WriterError.unableToWrite(error: error, forFilePath: url.path) } 20 | } 21 | do { 22 | let file: FileHandle = try .init(forUpdating: url) 23 | file.seek(toFileOffset: index) 24 | file.write(data) 25 | file.closeFile() 26 | } catch { 27 | throw WriterError.unableToInitFileHandleForWrite(error: error, forFilePath: url.path) 28 | } 29 | } 30 | /** 31 | * Empties a file 32 | */ 33 | public static func clear(filePath: String) throws { 34 | let url: URL = .init(fileURLWithPath: filePath) 35 | do { 36 | let file: FileHandle = try .init(forUpdating: url) 37 | file.truncateFile(atOffset: 0) 38 | file.closeFile() 39 | } catch { 40 | throw WriterError.unableToInitFileHandleForClear(error: error, forFilePath: filePath) 41 | } 42 | } 43 | } 44 | /** 45 | * Convenience 46 | */ 47 | extension FSWriter { 48 | /** 49 | * Support for filePath 50 | */ 51 | public static func write(filePath: String, data: Data, index: UInt64) throws { 52 | let url: URL = .init(fileURLWithPath: filePath) 53 | try write(url: url, data: data, index: index) 54 | } 55 | } 56 | /** 57 | * Error 58 | */ 59 | extension FSWriter { 60 | enum WriterError: Error { 61 | case unableToWrite(error: Error, forFilePath: String) 62 | case unableToInitFileHandleForWrite(error: Error, forFilePath: String) 63 | case unableToInitFileHandleForClear(error: Error, forFilePath: String) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/FileStreamerTests/FileStreamerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ResourceHelper 3 | import FileStreamer 4 | 5 | class FileStreamerTests: XCTestCase { 6 | func testExample() { 7 | XCTAssertTrue(readFile()) 8 | } 9 | } 10 | extension FileStreamerTests { 11 | /** 12 | * Test Filesize and 13 | */ 14 | func readFile() -> Bool { 15 | let fromFilePath: String = ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/demo.mp3").path 16 | guard let fileSize: UInt64 = try? FSReader.fileSize(filePath: fromFilePath) else { Swift.print("err - filesize"); return false } 17 | Swift.print("fileSize: \(fileSize)") 18 | guard let data: Data = try? FSReader.read(filePath: fromFilePath, startIndex: 0, endIndex: fileSize) else { Swift.print("unable to read data"); return false } 19 | guard !data.isEmpty else { Swift.print("err, missing file"); return false } 20 | Swift.print("data.count: \(data.count)") 21 | Swift.print("data is readable ✅") 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/FileStreamerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /temp.bundle/demo.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:235ef3f11f9d0d7e79c64c380f939b6b1c6c1ef7c34fceaa31075114c7280c49 3 | size 38912 4 | -------------------------------------------------------------------------------- /temp.bundle/test.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8b2b81f867e15ae3c2e6e20d7ba634e59c77c0a8c4c1720d4b91a6fbce95e07e 3 | size 2124 4 | --------------------------------------------------------------------------------