├── .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 | 
2 | 
3 | 
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 |
--------------------------------------------------------------------------------