diff --git a/DataCapturing/Sources/DataCapturing/Model/FinishedMeasurement.swift b/DataCapturing/Sources/DataCapturing/Model/FinishedMeasurement.swift index 3cb740db..24f43573 100644 --- a/DataCapturing/Sources/DataCapturing/Model/FinishedMeasurement.swift +++ b/DataCapturing/Sources/DataCapturing/Model/FinishedMeasurement.swift @@ -28,8 +28,6 @@ import OSLog A `Measurement` can be synchronized to a Cyface server via an instance of `Synchronizer`. - Author: Klemens Muthmann - - Version: 2.0.0 - - since: 11.0.0 */ public class FinishedMeasurement: Hashable, Equatable { /// A device wide unique identifier for this measurement. Usually set by incrementing a counter. @@ -75,9 +73,19 @@ public class FinishedMeasurement: Hashable, Equatable { - throws: `InconstantData.locationOrderViolation` if the timestamps of the locations in this measurement are not strongly monotonically increasing. */ public convenience init(managedObject: MeasurementMO) throws { - let accelerationFile = SensorValueFile(fileType: .accelerationValueType, qualifier: String(managedObject.unsignedIdentifier)) - let directionFile = SensorValueFile(fileType: .directionValueType, qualifier: String(managedObject.unsignedIdentifier)) - let rotationFile = SensorValueFile(fileType: .rotationValueType, qualifier: String(managedObject.unsignedIdentifier)) + let sensorValueFileFactory = DefaultSensorValueFileFactory() + let accelerationFile = try sensorValueFileFactory.create( + fileType: .accelerationValueType, + qualifier: String(managedObject.unsignedIdentifier) + ) + let directionFile = try sensorValueFileFactory.create( + fileType: .directionValueType, + qualifier: String(managedObject.unsignedIdentifier) + ) + let rotationFile = try sensorValueFileFactory.create( + fileType: .rotationValueType, + qualifier: String(managedObject.unsignedIdentifier) + ) self.init( identifier: managedObject.unsignedIdentifier, diff --git a/DataCapturing/Sources/DataCapturing/Persistence/CapturedDataStorage.swift b/DataCapturing/Sources/DataCapturing/Persistence/CapturedDataStorage.swift index de11846c..cf5b93b3 100644 --- a/DataCapturing/Sources/DataCapturing/Persistence/CapturedDataStorage.swift +++ b/DataCapturing/Sources/DataCapturing/Persistence/CapturedDataStorage.swift @@ -27,8 +27,6 @@ import CoreData Implementations of this protocol are capable of storing captured data to some kind of permanent storage. - author: Klemens Muthmann - - version: 1.0.0 - - Since: 12.0.0 */ public protocol CapturedDataStorage { /// Subscribe to a running measurement and store the data produced by that measurement. @@ -45,8 +43,6 @@ public protocol CapturedDataStorage { An implementation of `CapturedDataStorage` for storing the data to a CoreData database. - author: Klemens Muthmann - - version: 1.0.1 - - Since: 12.0.0 */ public class CapturedCoreDataStorage where SVFF.Serializable == [SensorValue] { // MARK: - Properties @@ -93,47 +89,84 @@ public class CapturedCoreDataStorage where SVFF.Se return identifier } } +} - private func load(measurement identifier: UInt64, from context: NSManagedObjectContext) throws -> MeasurementMO { - guard let measurementRequest = context.persistentStoreCoordinator?.managedObjectModel.fetchRequestFromTemplate( - withName: "measurementByIdentifier", - substitutionVariables: ["identifier": identifier] - ) else { - os_log( - "Unable to load measurement fetch request.", - log: OSLog.persistence, - type: .debug - ) - throw PersistenceError.measurementNotLoadable(identifier) - } - guard let measurementMO = try measurementRequest.execute().first as? MeasurementMO else { - os_log( - "Unable to load measurement to store to", - log: OSLog.persistence, - type: .debug - ) - throw PersistenceError.measurementNotLoadable(identifier) - } +// MARK: - Implementation of CapturedDataStorage Protocol +extension CapturedCoreDataStorage: CapturedDataStorage { - return measurementMO + /// Recievie updates from the provided ``Measurement`` and store the data to a ``DataStoreStack``. + public func subscribe( + to measurement: Measurement, + _ initialMode: String, + _ receiveCompletion: @escaping ((_ databaseIdentifier: UInt64) async -> Void) + ) throws -> UInt64 { + let measurementIdentifier = try createMeasurement(initialMode) + let messageHandler = try MessageHandler(fileFactory: sensorValueFileFactory, measurementIdentifier: measurementIdentifier, dataStoreStack: dataStoreStack) + + let cachedFlow = measurement.measurementMessages.collect(.byTime(cachingQueue, 1.0)) + cachedFlow + .sink(receiveCompletion: { status in + switch status { + case .finished: + os_log( + "Completing storage flow.", + log: OSLog.persistence, + type: .debug + ) + Task { + await receiveCompletion(measurementIdentifier) + } + case .failure(let error): + os_log("Unable to complete measurement %@", log: OSLog.persistence, type: .error, error.localizedDescription) + } + } + ) { messages in + do { + try messageHandler.handle(messages: messages) + } catch { + os_log("Unable to store data! Error %{PUBLIC}@",log: OSLog.persistence ,type: .error, error.localizedDescription) + } + }.store(in: &cancellables) + + return measurementIdentifier } - private func handle(messages: [Message], measurement identifier: UInt64) throws { - try self.dataStoreStack.wrapInContext { context in - let measurementMo = try self.load(measurement: identifier, from: context) + public func unsubscribe() { + cancellables.removeAll(keepingCapacity: true) + } +} - let accelerationsFile = self.sensorValueFileFactory.create( - fileType: SensorValueFileType.accelerationValueType, - qualifier: String(measurementMo.unsignedIdentifier) - ) - let rotationsFile = self.sensorValueFileFactory.create( - fileType: SensorValueFileType.rotationValueType, - qualifier: String(measurementMo.unsignedIdentifier) - ) - let directionsFile = self.sensorValueFileFactory.create( - fileType: SensorValueFileType.directionValueType, - qualifier: String(measurementMo.unsignedIdentifier) - ) +struct MessageHandler where SVFF.Serializable == [SensorValue] { + // MARK: - Properties + public let measurementIdentifier: UInt64 + public let accelerationsFile: SVFF.FileType + public let rotationsFile: SVFF.FileType + public let directionsFile: SVFF.FileType + let dataStoreStack: DataStoreStack + + // MARK: - Initializers + init(fileFactory: SVFF, measurementIdentifier: UInt64, dataStoreStack: DataStoreStack) throws { + self.dataStoreStack = dataStoreStack + self.measurementIdentifier = measurementIdentifier + self.accelerationsFile = try fileFactory.create( + fileType: SensorValueFileType.accelerationValueType, + qualifier: String(measurementIdentifier) + ) + self.rotationsFile = try fileFactory.create( + fileType: SensorValueFileType.rotationValueType, + qualifier: String(measurementIdentifier) + ) + self.directionsFile = try fileFactory.create( + fileType: SensorValueFileType.directionValueType, + qualifier: String(measurementIdentifier) + ) + } + + // MARK: - Methods + + func handle(messages: [Message]) throws { + try self.dataStoreStack.wrapInContext { context in + let measurementMo = try self.load(measurement: measurementIdentifier, from: context) try messages.forEach { message in switch message { @@ -167,6 +200,36 @@ public class CapturedCoreDataStorage where SVFF.Se } } + // MARK: - Private Methods + /// Load a measurement from the database. This should only be executed within a valid CoreData context. + private func load(measurement identifier: UInt64, from context: NSManagedObjectContext) throws -> MeasurementMO { + guard let measurementRequest = context.persistentStoreCoordinator?.managedObjectModel.fetchRequestFromTemplate( + withName: "measurementByIdentifier", + substitutionVariables: ["identifier": identifier] + ) else { + os_log( + "Unable to load measurement fetch request.", + log: OSLog.persistence, + type: .debug + ) + throw PersistenceError.measurementNotLoadable(identifier) + } + guard let measurementMO = try measurementRequest.execute().first as? MeasurementMO else { + os_log( + "Unable to load measurement to store to", + log: OSLog.persistence, + type: .debug + ) + throw PersistenceError.measurementNotLoadable(identifier) + } + + return measurementMO + } + + /// Store a ``GeoLocation`` to the database. + /// + /// - Parameter location: The location to store. + /// - Parameter measurementMo: The measurement to store the location to. private func store(location: GeoLocation, to measurementMo: MeasurementMO, _ context: NSManagedObjectContext) { os_log("Storing location to database.", log: OSLog.persistence, type: .debug) if let lastTrack = measurementMo.typedTracks().last { @@ -174,21 +237,35 @@ public class CapturedCoreDataStorage where SVFF.Se } } + /// Store an ``Altitude`` to the database. + /// + /// - Parameter altitude: The altitude to store. + /// - Parameter measurementMo: The measurement to store the altitude to. private func store(altitude: Altitude, to measurementMo: MeasurementMO, _ context: NSManagedObjectContext) { if let lastTrack = measurementMo.typedTracks().last { lastTrack.addToAltitudes(AltitudeMO(altitude: altitude, context: context)) } } + /// Store a sensor value (e.g. direction, rotation, acceleration) to a file on the local disk. + /// + /// - Parameter value: The value to store to the file. + /// - Parameter to: The file to store the value to. private func store(_ value: SensorValue, to file: SVF) throws where SVF.Serializable == SVFF.Serializable { do { _ = try file.write(serializable: [value]) } catch { - debugPrint("Unable to write data to file \(file.fileName)!") + debugPrint("Unable to write data to file \(file.qualifiedPath)!") throw error } } + /// Callback, called when the measurement has been stopped and all values have been stored. + /// + /// - Parameter measurement: The measurement that was finished. + /// - Parameter context: The database context used to communicate with the database. + /// - Parameter time: The time for the final stop event. + /// - Attention: Only call this within a valid CoreData context (same thread as the one that opened the provided context). private func onStop(measurement measurementMo: MeasurementMO, _ context: NSManagedObjectContext, _ time: Date) throws { os_log("Storing stopped event to database.", log: OSLog.persistence, type: .debug) measurementMo.addToEvents(EventMO(event: Event(time: time, type: .lifecycleStop), context: context)) @@ -196,49 +273,4 @@ public class CapturedCoreDataStorage where SVFF.Se try context.save() os_log("Stored finished measurement.", log: OSLog.persistence, type: .debug) } - -} - -// MARK: - Implementation of CapturedDataStorage Protocol -extension CapturedCoreDataStorage: CapturedDataStorage { - - /// Recievie updates from the provided ``Measurement`` and store the data to a ``DataStoreStack``. - public func subscribe( - to measurement: Measurement, - _ initialMode: String, - _ receiveCompletion: @escaping ((_ databaseIdentifier: UInt64) async -> Void) - ) throws -> UInt64 { - let measurementIdentifier = try createMeasurement(initialMode) - - let cachedFlow = measurement.measurementMessages.collect(.byTime(cachingQueue, 1.0)) - cachedFlow - .sink(receiveCompletion: { status in - switch status { - case .finished: - os_log( - "Completing storage flow.", - log: OSLog.persistence, - type: .debug - ) - Task { - await receiveCompletion(measurementIdentifier) - } - case .failure(let error): - os_log("Unable to complete measurement %@", log: OSLog.persistence, type: .error, error.localizedDescription) - } - } - ) { [weak self] (messages: [Message]) in - do { - try self?.handle(messages: messages, measurement: measurementIdentifier) - } catch { - os_log("Unable to store data! Error %{PUBLIC}@",log: OSLog.persistence ,type: .error, error.localizedDescription) - } - }.store(in: &cancellables) - - return measurementIdentifier - } - - public func unsubscribe() { - cancellables.removeAll(keepingCapacity: true) - } } diff --git a/DataCapturing/Sources/DataCapturing/Persistence/FileSupport.swift b/DataCapturing/Sources/DataCapturing/Persistence/FileSupport.swift index d6e31c03..9215d1ac 100644 --- a/DataCapturing/Sources/DataCapturing/Persistence/FileSupport.swift +++ b/DataCapturing/Sources/DataCapturing/Persistence/FileSupport.swift @@ -24,8 +24,6 @@ import os.log The protocol for writing accelerations to a file. - Author: Klemens Muthmann - - Version: 4.0.0 - - Since: 2.0.0 */ public protocol FileSupport { @@ -34,12 +32,13 @@ public protocol FileSupport { associatedtype SpecificSerializer: BinarySerializer /// The generic type of the data to store to a file. associatedtype Serializable - /// The name of the file to store. - var fileName: String { get } - /// The file extension of the file to store. - var fileExtension: String { get } + /// Transforms the provided data into a binary representation. var serializer: SpecificSerializer { get } - var qualifier: String { get } + /// The path to the file storing the data. + var qualifiedPath: URL { get } + + // MARK: - Initializers + init(qualifiedPath: URL) // MARK: - Methods /** @@ -61,6 +60,22 @@ public protocol FileSupport { extension FileSupport { + // MARK: - Initializers + init(rootPath: URL, fileType: SensorValueFileType, qualifier: String) throws { + let fileManager = FileManager.default + let measurementDirectoryPath = rootPath.appendingPathComponent(qualifier) + try fileManager.createDirectory(at: measurementDirectoryPath, withIntermediateDirectories: true) + + let qualifiedPath = measurementDirectoryPath + .appendingPathComponent(fileType.fileName) + .appendingPathExtension(fileType.fileExtension) + if !fileManager.fileExists(atPath: qualifiedPath.path) { + fileManager.createFile(atPath: qualifiedPath.path, contents: nil) + } + self.init(qualifiedPath: qualifiedPath) + } + + // MARK: - Methods /** Write a file containing a serialized measurement in Cyface Binary Format, to the local file system data storage. @@ -74,7 +89,7 @@ extension FileSupport { */ public func write(serializable: Serializable) throws -> URL where SpecificSerializer.Serializable == Serializable { let data = try serializer.serializeCompressed(serializable: serializable) - let filePath = try path(qualifier: qualifier) + let filePath = qualifiedPath let fileHandle = try FileHandle(forWritingTo: filePath) defer { fileHandle.closeFile() } @@ -83,34 +98,6 @@ extension FileSupport { return filePath } - /** - Creates the path to a file containing data in the Cyface binary format. - - - Parameter qualifier: - - Returns: The path to the file as an URL. - - Throws: Some internal file system error on failure of creating the file at the required path. - */ - public func path(qualifier: String) throws -> URL { - let root = "Application Support" - let measurementDirectory = "measurements" - let fileManager = FileManager.default - let libraryDirectory = FileManager.SearchPathDirectory.libraryDirectory - let libraryDirectoryUrl = try fileManager.url(for: libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - - let measurementDirectoryPath = libraryDirectoryUrl - .appendingPathComponent(root) - .appendingPathComponent(measurementDirectory) - .appendingPathComponent(qualifier) - try fileManager.createDirectory(at: measurementDirectoryPath, withIntermediateDirectories: true) - - let filePath = measurementDirectoryPath.appendingPathComponent(fileName).appendingPathExtension(fileExtension) - if !fileManager.fileExists(atPath: filePath.path) { - fileManager.createFile(atPath: filePath.path, contents: nil) - } - - return filePath - } - /** Removes the data file for the provided measurement. If this was the last or only data file it also deletes the folder containing the files for the measurement. @@ -118,7 +105,7 @@ extension FileSupport { - Throws: Some internal file system error on failure of creating the file at the required path. */ public func delete() throws { - let filePath = try path(qualifier: qualifier) + let filePath = qualifiedPath let parent = filePath.deletingLastPathComponent() let fileManager = FileManager.default @@ -140,8 +127,6 @@ extension FileSupport { Struct implementing the `FileSupport` protocol to store sensor values to a file in Cyface binary format. - Author: Klemens Muthmann - - Version: 5.0.0 - - Since: 2.0.0 - Note: This class was called `AccelerationsFile` prior to SDK version 6.0.0. */ public struct SensorValueFile: FileSupport { @@ -150,28 +135,18 @@ public struct SensorValueFile: FileSupport { /// A serializer to transform between sensor values and the Cyface Binary Format. public let serializer = SensorValueSerializer() - /// The file name for the file containing the sensor values for one measurement. - public let fileName = "accel" - - /// File extension used for files containing accelerations. - public let fileExtension: String - let fileType: SensorValueFileType - public let qualifier: String - + public let qualifiedPath: URL // MARK: - Initializers /// Public initializer for external systems to access sensor value data. - init(fileType: SensorValueFileType, qualifier: String) { - self.fileType = fileType - self.qualifier = qualifier - self.fileExtension = fileType.fileExtension + public init(qualifiedPath: URL) { + self.qualifiedPath = qualifiedPath } // MARK: - Methods /** Writes the provided sensor values to the provided measurement. - - Parameters: - serializable: The array of sensor values to write. @@ -182,17 +157,16 @@ public struct SensorValueFile: FileSupport { */ public func write(serializable: [SensorValue]) throws -> URL { let sensorValueData = try serializer.serialize(serializable: serializable) - let sensorValueFilePath = try path(qualifier: qualifier) - let fileHandle = try FileHandle(forWritingTo: sensorValueFilePath) + let fileHandle = try FileHandle(forWritingTo: qualifiedPath) defer { fileHandle.closeFile()} - guard FileManager.default.isWritableFile(atPath: sensorValueFilePath.path) else { + guard FileManager.default.isWritableFile(atPath: qualifiedPath.path) else { fatalError("Unable to write sensor data since file is not writable!") } fileHandle.seekToEndOfFile() fileHandle.write(sensorValueData) - return sensorValueFilePath + return qualifiedPath } /** @@ -203,7 +177,7 @@ public struct SensorValueFile: FileSupport { */ func load() throws -> [SensorValue] { do { - let fileHandle = try FileHandle(forReadingFrom: path(qualifier: qualifier)) + let fileHandle = try FileHandle(forReadingFrom: qualifiedPath) defer {fileHandle.closeFile()} let data = fileHandle.readDataToEndOfFile() return try serializer.deserialize(data: data) @@ -222,7 +196,7 @@ public struct SensorValueFile: FileSupport { */ func data() throws -> Data { do { - let fileHandle = try FileHandle(forReadingFrom: path(qualifier: qualifier)) + let fileHandle = try FileHandle(forReadingFrom: qualifiedPath) defer {fileHandle.closeFile()} return fileHandle.readDataToEndOfFile() } catch let error { @@ -239,8 +213,6 @@ case notReadable ``` - Author: Klemens Muthmann - - Version: 1.0.0 - - Since: 2.0.0 */ public enum FileSupportError: Error { /** @@ -251,29 +223,54 @@ public enum FileSupportError: Error { case notReadable(cause: Error) } +extension FileSupportError: LocalizedError { + /// The internationalized error description providing further details about a thrown error. + public var errorDescription: String? { + switch self { + case .notReadable(cause: let error): + let errorMessage = NSLocalizedString( + "de.cyface.error.FileSupportError.notReadable", + comment: """ +Tell the user that a file they wanted to open, was not readable. The causing error is provided as a String as the first parameter. +""" + ) + return String.localizedStringWithFormat(errorMessage, error.localizedDescription) + } + } +} + /** One type of a sensor value file, such as a file for accelerations, rotations or directions. This class may not be instantiated directly. The only valid instances are provided as static properties. - Author: Klemens Muthmann - - Version: 3.0.0 - - Since: 6.0.0 */ public class SensorValueFileType { + + // MARK: - Properties /// A file type for acceleration files. public static let accelerationValueType = SensorValueFileType( - fileExtension: "cyfa") + fileName: "accel", + fileExtension: "cyfa" + ) /// A file type for rotation files. public static let rotationValueType = SensorValueFileType( - fileExtension: "cyfr") + fileName: "rot", + fileExtension: "cyfr" + ) /// A file type for direction files. public static let directionValueType = SensorValueFileType( - fileExtension: "cyfd") + fileName: "dir", + fileExtension: "cyfd" + ) /// The file extension of the represented file type. public let fileExtension: String + /// The name of the file for this type of sensor data. + public let fileName: String + // MARK: - Initializers /** Creates a new completely initiailized `SensorValueFileType`. This should never be called, since the only valid instances are pregenerated. @@ -281,7 +278,8 @@ public class SensorValueFileType { - Parameters: - fileExtension: The file extension of the represented file type. */ - private init(fileExtension: String) { + private init(fileName: String, fileExtension: String) { + self.fileName = fileName self.fileExtension = fileExtension } } diff --git a/DataCapturing/Sources/DataCapturing/Persistence/SensorValueFileFactory.swift b/DataCapturing/Sources/DataCapturing/Persistence/SensorValueFileFactory.swift index 444a2677..57f16ff6 100644 --- a/DataCapturing/Sources/DataCapturing/Persistence/SensorValueFileFactory.swift +++ b/DataCapturing/Sources/DataCapturing/Persistence/SensorValueFileFactory.swift @@ -16,6 +16,7 @@ * You should have received a copy of the GNU General Public License * along with the Cyface SDK for iOS. If not, see . */ +import Foundation /** A factory to externalize the creation of ``SensorValueFile`` instances. @@ -25,37 +26,64 @@ This can be used if different formats are required or the actual file is mocked for testing. - Author: Klemens Muthmann - - Version: 1.0.0 - - Since: 12.0.0 */ public protocol SensorValueFileFactory { + /// The type of object to serialize in the files created from this factory. associatedtype Serializable + /// The serializer for the provided `Serializable`. associatedtype SpecificSerializer + /// The type of objects this factory creates. associatedtype FileType: FileSupport where FileType.Serializable == Serializable, FileType.SpecificSerializer == SpecificSerializer /// Create the actual file for a certain type - func create(fileType: SensorValueFileType, qualifier: String) -> FileType + func create(fileType: SensorValueFileType, qualifier: String) throws -> FileType +} + +// MARK: - Implementation +extension SensorValueFileFactory { + /// The root path used to store data via this app. This is in global scope, so it gets initialized at application start, since finding the location is a computation heavy operation. + func rootPath() throws -> URL { + let root = "Application Support" + let measurementDirectory = "measurements" + let fileManager = FileManager.default + let libraryDirectory = FileManager.SearchPathDirectory.libraryDirectory + let libraryDirectoryUrl = try fileManager.url(for: libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + + let measurementUrl = libraryDirectoryUrl + .appendingPathComponent(root) + .appendingPathComponent(measurementDirectory) + try fileManager.createDirectory(at: measurementUrl, withIntermediateDirectories: true) + return measurementUrl + } } /** Create the default ``SensorValueFile`` required by a recent Cyface Data Collector processing the Protobuf format and using the Google Media Upload Protocol. - Author: Klemens Muthmann - - Version: 1.0.0 - - Since: 12.0.0 */ public struct DefaultSensorValueFileFactory: SensorValueFileFactory { + /// This factory is used to create files storing arrays of ``SensorValue``. public typealias Serializable = [SensorValue] + /// This factory creates files that serialize data using a ``SensorValueSerializer``. public typealias SpecificSerializer = SensorValueSerializer + /// This factory creates ``SensorValueFile``. public typealias FileType = SensorValueFile + // MARK: - Initializers + /// Create a new instance of this struct. public init() { // Nothing to do here. } - public func create(fileType: SensorValueFileType, qualifier: String) -> SensorValueFile { - return SensorValueFile( + /// Create a new ``SensorValueFile``. + /// + /// - Parameter qualifier: Used to make the file unique and distinguishable from other files storing the same type of data. Usually this is the measurement identifier. + /// - Parameter fileType: The type of ``SensorValue`` to store. + public func create(fileType: SensorValueFileType, qualifier: String) throws -> SensorValueFile { + return try SensorValueFile( + rootPath: rootPath(), fileType: SensorValueFileType.accelerationValueType, qualifier: qualifier ) diff --git a/DataCapturing/Sources/DataCapturing/Synchronization/Upload/ServerConnectionError.swift b/DataCapturing/Sources/DataCapturing/Synchronization/Upload/ServerConnectionError.swift index a4b7791c..68f920e0 100644 --- a/DataCapturing/Sources/DataCapturing/Synchronization/Upload/ServerConnectionError.swift +++ b/DataCapturing/Sources/DataCapturing/Synchronization/Upload/ServerConnectionError.swift @@ -23,8 +23,6 @@ import Foundation A structure encapsulating errors used by server connections. - Author: Klemens Muthmann - - Version: 6.0.0 - - Since: 1.0.0 */ public enum ServerConnectionError: Error { /// If authentication was carried out but was not successful. The username of the failed authentication attempt is provided as a parameter.