diff --git a/Cauli/Data structures/Record.swift b/Cauli/Data structures/Record.swift index 9b1d306..3cf2e7c 100644 --- a/Cauli/Data structures/Record.swift +++ b/Cauli/Data structures/Record.swift @@ -57,20 +57,3 @@ extension Record { self.request = request } } - -extension Record { -// internal mutating func append(receivedData: Data) throws { -// guard case let .result(result)? = result else { -// throw NSError.CauliInternal.appendingDataWithoutResponse(receivedData, record: self) -// } -// var currentData = result.data ?? Data() -// currentData.append(receivedData) -// self.result = .result(Response(result.urlResponse, data: currentData)) -// } -} - -extension Record { -// internal func swapped(to path: URL) -> SwappedRecord { -// return SwappedRecord(self, folder: path) -// } -} diff --git a/Cauli/FilecachedInputOutputStream.swift b/Cauli/FilecachedInputOutputStream.swift new file mode 100644 index 0000000..f8d5f3e --- /dev/null +++ b/Cauli/FilecachedInputOutputStream.swift @@ -0,0 +1,81 @@ +// +// Copyright (c) 2018 cauli.works +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// The FilecachedInputOutputStream is an InputStream that can be written to. +/// Every byte written to the stream is first written to a temp file before +/// it can be read from. +internal class FilecachedInputOutputStream: InputStream { + + let writableOutputStream: OutputStream? + + private let cacheFile: URL + private let readableInputStream: InputStream + + deinit { + DispatchQueue.global(qos: .background).async { [cacheFile] in + do { + try FileManager.default.removeItem(at: cacheFile) + } catch (let error as NSError) + where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError { + // All good. File doesn't exist. + } catch { + print("Failed to delete temporary file with path: \(cacheFile)") + } + } + } + + init(fileCache: URL) { + writableOutputStream = OutputStream(url: fileCache, append: true) + writableOutputStream?.open() + readableInputStream = InputStream(url: fileCache)! + cacheFile = fileCache + super.init(url: fileCache)! + } + + convenience init() { + let tempfolder = URL(fileURLWithPath: NSTemporaryDirectory()) + let tempfile = tempfolder.appendingPathComponent(UUID().uuidString) + self.init(fileCache: tempfile) + } + + override var hasBytesAvailable: Bool { + return writableOutputStream?.streamStatus != .closed || readableInputStream.hasBytesAvailable + } + + override func open() { + readableInputStream.open() + } + + override func close() { + readableInputStream.close() + } + + override func read(_ buffer: UnsafeMutablePointer, maxLength len: Int) -> Int { + return readableInputStream.read(buffer, maxLength: len) + } + + override func getBuffer(_ buffer: UnsafeMutablePointer?>, length len: UnsafeMutablePointer) -> Bool { + return readableInputStream.getBuffer(buffer, length: len) + } +} diff --git a/CauliTests/FilecachedInputOutputStreamSpec.swift b/CauliTests/FilecachedInputOutputStreamSpec.swift new file mode 100644 index 0000000..ff3ed9f --- /dev/null +++ b/CauliTests/FilecachedInputOutputStreamSpec.swift @@ -0,0 +1,74 @@ +// +// Copyright (c) 2018 cauli.works +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@testable import Cauliframework +import Foundation +import Quick +import Nimble + +class FilecachedInputOutputStreamSpec: QuickSpec { + override func spec() { + describe("hasBytesAvailable") { + it("should be true if the outputstream isn't closed even if no data was written before") { + let stream = FilecachedInputOutputStream() + expect(stream.hasBytesAvailable) == true + } + it("should be false if the outputstream is closed") { + let stream = FilecachedInputOutputStream() + stream.writableOutputStream?.close() + expect(stream.hasBytesAvailable) == false + } + it("should be false if the outputstream is closed, even if unread data was written to") { + let stream = FilecachedInputOutputStream() + let data = "spec_data".data(using: .utf8)! + _ = data.withUnsafeBytes { stream.writableOutputStream?.write($0, maxLength: data.count) } + stream.writableOutputStream?.close() + expect(stream.hasBytesAvailable) == false + } + } + describe("read") { + it("shouldn't read any data if nothing was written to it") { + let stream = FilecachedInputOutputStream() + stream.open() + let buffer = UnsafeMutablePointer.allocate(capacity: 1024) + let readBytes = stream.read(buffer, maxLength: 1024) + expect(readBytes) == 0 + } + it("shouldn't read the same data that was written to the output stream") { + let stream = FilecachedInputOutputStream() + let writtenData = "spec_data".data(using: .utf8)! + _ = writtenData.withUnsafeBytes { stream.writableOutputStream?.write($0, maxLength: writtenData.count) } + stream.open() + let buffer = UnsafeMutablePointer.allocate(capacity: 1024) + let readBytes = stream.read(buffer, maxLength: 1024) + let readData = Data(bytes: buffer, count: readBytes) + expect(readBytes) == writtenData.count + expect(readData) == writtenData + } + } + } + + private func tempfile() -> URL { + let tempfolder = URL(fileURLWithPath: NSTemporaryDirectory()) + return tempfolder.appendingPathComponent(UUID().uuidString) + } +} diff --git a/Cauliframework.xcodeproj/project.pbxproj b/Cauliframework.xcodeproj/project.pbxproj index 84ae454..af03d64 100644 --- a/Cauliframework.xcodeproj/project.pbxproj +++ b/Cauliframework.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -41,6 +41,7 @@ 1FB1FF58219DB75E0021B4F4 /* UIWindow+Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB1FF55219DB75E0021B4F4 /* UIWindow+Shake.swift */; }; 1FB1FF5A219DB7DD0021B4F4 /* ViewControllerShakePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB1FF59219DB7DD0021B4F4 /* ViewControllerShakePresenter.swift */; }; 1FB3296F2181CBCD001CA03D /* RecordSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB3296E2181CBCD001CA03D /* RecordSelector.swift */; }; + 1FB67F4623322589004618EE /* FilecachedInputOutputStreamSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB67F4423322583004618EE /* FilecachedInputOutputStreamSpec.swift */; }; 1FBE790521A1A271004C81A7 /* InspectorFloret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBE790421A1A271004C81A7 /* InspectorFloret.swift */; }; 1FBE790D21A1A572004C81A7 /* TagLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBE790C21A1A572004C81A7 /* TagLabel.swift */; }; 1FD0F8812150F0A000BD383B /* Collection+ReduceAsnyc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD0F8802150F0A000BD383B /* Collection+ReduceAsnyc.swift */; }; @@ -48,6 +49,7 @@ 1FE3F73620F64A2B00233C7C /* Cauli.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FE3F72820F64A2B00233C7C /* Cauli.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1FE3F74020F64AA000233C7C /* Cauli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE3F73F20F64AA000233C7C /* Cauli.swift */; }; 1FE3F74220F64C6700233C7C /* CauliURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE3F74120F64C6700233C7C /* CauliURLProtocol.swift */; }; + 1FEF6D1F22EE197A0008C9AA /* FilecachedInputOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FEF6D1E22EE197A0008C9AA /* FilecachedInputOutputStream.swift */; }; 1FF25CF92275EDD800008E51 /* NSError+NetworkErrorShortString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF25CF82275EDD800008E51 /* NSError+NetworkErrorShortString.swift */; }; 1FFFE7D721AC7C080040B805 /* MemoryStorageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFFE7D221AC7C080040B805 /* MemoryStorageSpec.swift */; }; 5005E53D222BE0C800BD5DF7 /* DisplayingFloret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5005E53C222BE0C700BD5DF7 /* DisplayingFloret.swift */; }; @@ -123,6 +125,7 @@ 1FB1FF55219DB75E0021B4F4 /* UIWindow+Shake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIWindow+Shake.swift"; sourceTree = ""; }; 1FB1FF59219DB7DD0021B4F4 /* ViewControllerShakePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerShakePresenter.swift; sourceTree = ""; }; 1FB3296E2181CBCD001CA03D /* RecordSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordSelector.swift; sourceTree = ""; }; + 1FB67F4423322583004618EE /* FilecachedInputOutputStreamSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilecachedInputOutputStreamSpec.swift; sourceTree = ""; }; 1FBE790421A1A271004C81A7 /* InspectorFloret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorFloret.swift; sourceTree = ""; }; 1FBE790C21A1A572004C81A7 /* TagLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagLabel.swift; sourceTree = ""; }; 1FD0F8802150F0A000BD383B /* Collection+ReduceAsnyc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+ReduceAsnyc.swift"; sourceTree = ""; }; @@ -133,6 +136,7 @@ 1FE3F73520F64A2B00233C7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1FE3F73F20F64AA000233C7C /* Cauli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cauli.swift; sourceTree = ""; }; 1FE3F74120F64C6700233C7C /* CauliURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CauliURLProtocol.swift; sourceTree = ""; }; + 1FEF6D1E22EE197A0008C9AA /* FilecachedInputOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilecachedInputOutputStream.swift; sourceTree = ""; }; 1FF25CF82275EDD800008E51 /* NSError+NetworkErrorShortString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+NetworkErrorShortString.swift"; sourceTree = ""; }; 1FFFE7D121AC7C080040B805 /* Record+Fake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Record+Fake.swift"; sourceTree = ""; }; 1FFFE7D221AC7C080040B805 /* MemoryStorageSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryStorageSpec.swift; sourceTree = ""; }; @@ -324,6 +328,7 @@ 1FE3F73F20F64AA000233C7C /* Cauli.swift */, 50576BB9210E173C0085DF5E /* Floret.swift */, 1F18B1A2210EF9F500CEDD42 /* Storage.swift */, + 1FEF6D1E22EE197A0008C9AA /* FilecachedInputOutputStream.swift */, 5005E53E222BE0D400BD5DF7 /* InterceptingFloret.swift */, 5005E53C222BE0C700BD5DF7 /* DisplayingFloret.swift */, 50E068E921C67E2300A9DFA8 /* RecordModifier.swift */, @@ -352,6 +357,7 @@ 1F8C2A1421D794BD009D0382 /* CauliSpec.swift */, 50ABECFE21C834A5009667E1 /* FindReplaceFloretSpec.swift */, 5044B7E921F28555006908B2 /* NoCacheFloretSpec.swift */, + 1FB67F4423322583004618EE /* FilecachedInputOutputStreamSpec.swift */, ); path = CauliTests; sourceTree = ""; @@ -704,6 +710,7 @@ 1F6BB4562163609100D56F4F /* Configuration.swift in Sources */, 1F7BF23020F790F50023D219 /* URLSessionConfiguration+Swizzling.swift in Sources */, 1F7BF22E20F77B6C0023D219 /* Record.swift in Sources */, + 1FEF6D1F22EE197A0008C9AA /* FilecachedInputOutputStream.swift in Sources */, 1FA4A50D21B7C40100F1CA6B /* InspectorRecordTableViewCell.swift in Sources */, 5042C4D221EBE1B500652AC6 /* CauliViewController.swift in Sources */, 1F644F4A211B620B004C4271 /* WeakReference.swift in Sources */, @@ -727,6 +734,7 @@ 1F7001C4216B8250007A9355 /* CauliURLProtocolDelegateStub.swift in Sources */, 505B4BF121AC5C91008A9836 /* URLResponse+Fake.swift in Sources */, 505B4BEE21AC5BF1008A9836 /* URLResponseRepresentableSpec.swift in Sources */, + 1FB67F4623322589004618EE /* FilecachedInputOutputStreamSpec.swift in Sources */, 5CC65DBB22DA39B10019F237 /* Record+Fake.swift in Sources */, 1FFFE7D721AC7C080040B805 /* MemoryStorageSpec.swift in Sources */, ); diff --git a/Podfile b/Podfile index 1009930..accc759 100644 --- a/Podfile +++ b/Podfile @@ -1,3 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' + # Uncomment the next line to define a global platform for your project # platform :ios, '9.0'