Ivan Valkou пре 4 година
родитељ
комит
a426b63a05

+ 0 - 36
Dependencies/CGMBLEKit/.gitignore

@@ -1,36 +0,0 @@
-# OS X
-.DS_Store
-
-# Xcode
-build/
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
-xcuserdata
-*.xccheckout
-profile
-*.moved-aside
-DerivedData
-*.hmap
-*.ipa
-
-# Bundler
-.bundle
-
-Carthage
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
-#
-# Note: if you ignore the Pods directory, make sure to uncomment
-# `pod install` in .travis.yml
-#
-
-Pods/
-Carthage/
-.gitmodules

+ 0 - 72
Dependencies/CGMBLEKit/CODE_OF_CONDUCT.md

@@ -1,72 +0,0 @@
-# Code of Conduct
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, gender identity and expression, level of experience,
-nationality, personal appearance, race, religion, or sexual identity and
-orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or
-advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
-  address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the maintaner [via email](mailto:loudnate@gmail.com). All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at [http://contributor-covenant.org/version/1/4][version]
-
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/

+ 56 - 34
FreeAPS.xcodeproj/project.pbxproj

@@ -86,7 +86,6 @@
 		3818AA6F274C26A500843DB3 /* RileyLinkKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA57274C26A300843DB3 /* RileyLinkKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3818AA71274C278200843DB3 /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; };
 		3818AA72274C278200843DB3 /* LoopTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
-		38192E01261B826A0094D973 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 38192E00261B826A0094D973 /* Alamofire */; };
 		38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E03261B82FA0094D973 /* ReachabilityManager.swift */; };
 		38192E07261BA9960094D973 /* FetchTreatmentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */; };
 		38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */; };
@@ -96,7 +95,6 @@
 		3833B46D26012030003021B3 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 3833B46C26012030003021B3 /* Algorithms */; };
 		383420D625FFE38C002D46C1 /* LoopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D525FFE38C002D46C1 /* LoopView.swift */; };
 		383420D925FFEB3F002D46C1 /* Popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D825FFEB3F002D46C1 /* Popup.swift */; };
-		383948D325CD4D6D00E91849 /* Disk in Frameworks */ = {isa = PBXBuildFile; productRef = 383948D225CD4D6D00E91849 /* Disk */; };
 		383948D625CD4D8900E91849 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D525CD4D8900E91849 /* FileStorage.swift */; };
 		383948DA25CD64D500E91849 /* Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D925CD64D500E91849 /* Glucose.swift */; };
 		384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803325C385E60086DB71 /* JavaScriptWorker.swift */; };
@@ -164,6 +162,18 @@
 		38DAB280260CBB7F00F74C1A /* PumpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DAB27F260CBB7F00F74C1A /* PumpView.swift */; };
 		38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DAB289260D349500F74C1A /* FetchGlucoseManager.swift */; };
 		38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4451D274DB04600EC9A94 /* AppDelegate.swift */; };
+		38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44521274E3DDC00EC9A94 /* NetworkReachabilityManager.swift */; };
+		38E44528274E401C00EC9A94 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44527274E401C00EC9A94 /* Protected.swift */; };
+		38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4452A274E411600EC9A94 /* Disk+InternalHelpers.swift */; };
+		38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4452B274E411600EC9A94 /* Disk+Data.swift */; };
+		38E44536274E411700EC9A94 /* Disk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4452C274E411600EC9A94 /* Disk.swift */; };
+		38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4452D274E411600EC9A94 /* Disk+Helpers.swift */; };
+		38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4452E274E411600EC9A94 /* Disk+[Data].swift */; };
+		38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E4452F274E411600EC9A94 /* Disk+UIImage.swift */; };
+		38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44530274E411700EC9A94 /* Disk+[UIImage].swift */; };
+		38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44531274E411700EC9A94 /* Disk+VolumeInformation.swift */; };
+		38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44532274E411700EC9A94 /* Disk+Codable.swift */; };
+		38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44533274E411700EC9A94 /* Disk+Errors.swift */; };
 		38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E989DC25F5021400C0CED0 /* PumpStatus.swift */; };
 		38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1B25F52C9300C0CED0 /* Signpost.swift */; };
 		38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1C25F52C9300C0CED0 /* Logger.swift */; };
@@ -469,6 +479,18 @@
 		38DAB27F260CBB7F00F74C1A /* PumpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpView.swift; sourceTree = "<group>"; };
 		38DAB289260D349500F74C1A /* FetchGlucoseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchGlucoseManager.swift; sourceTree = "<group>"; };
 		38E4451D274DB04600EC9A94 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		38E44521274E3DDC00EC9A94 /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkReachabilityManager.swift; sourceTree = "<group>"; };
+		38E44527274E401C00EC9A94 /* Protected.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Protected.swift; sourceTree = "<group>"; };
+		38E4452A274E411600EC9A94 /* Disk+InternalHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+InternalHelpers.swift"; sourceTree = "<group>"; };
+		38E4452B274E411600EC9A94 /* Disk+Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+Data.swift"; sourceTree = "<group>"; };
+		38E4452C274E411600EC9A94 /* Disk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Disk.swift; sourceTree = "<group>"; };
+		38E4452D274E411600EC9A94 /* Disk+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+Helpers.swift"; sourceTree = "<group>"; };
+		38E4452E274E411600EC9A94 /* Disk+[Data].swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+[Data].swift"; sourceTree = "<group>"; };
+		38E4452F274E411600EC9A94 /* Disk+UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+UIImage.swift"; sourceTree = "<group>"; };
+		38E44530274E411700EC9A94 /* Disk+[UIImage].swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+[UIImage].swift"; sourceTree = "<group>"; };
+		38E44531274E411700EC9A94 /* Disk+VolumeInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+VolumeInformation.swift"; sourceTree = "<group>"; };
+		38E44532274E411700EC9A94 /* Disk+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+Codable.swift"; sourceTree = "<group>"; };
+		38E44533274E411700EC9A94 /* Disk+Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+Errors.swift"; sourceTree = "<group>"; };
 		38E989DC25F5021400C0CED0 /* PumpStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatus.swift; sourceTree = "<group>"; };
 		38E98A1B25F52C9300C0CED0 /* Signpost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Signpost.swift; sourceTree = "<group>"; };
 		38E98A1C25F52C9300C0CED0 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
@@ -576,9 +598,7 @@
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
 				3818AA5C274C26A300843DB3 /* MockKit.framework in Frameworks */,
 				3818AA6A274C26A500843DB3 /* RileyLinkBLEKit.framework in Frameworks */,
-				38192E01261B826A0094D973 /* Alamofire in Frameworks */,
 				3818AA64274C26A400843DB3 /* MinimedKitUI.framework in Frameworks */,
-				383948D325CD4D6D00E91849 /* Disk in Frameworks */,
 				3818AA66274C26A400843DB3 /* OmniKit.framework in Frameworks */,
 				3818AA6C274C26A500843DB3 /* RileyLinkKit.framework in Frameworks */,
 				3818AA68274C26A400843DB3 /* OmniKitUI.framework in Frameworks */,
@@ -813,6 +833,7 @@
 		3811DE9425C9D88200A708ED /* Network */ = {
 			isa = PBXGroup;
 			children = (
+				38E44521274E3DDC00EC9A94 /* NetworkReachabilityManager.swift */,
 				38192E03261B82FA0094D973 /* ReachabilityManager.swift */,
 				3811DE9625C9D88300A708ED /* HTTPResponseStatus.swift */,
 				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
@@ -828,6 +849,7 @@
 				383948D525CD4D8900E91849 /* FileStorage.swift */,
 				3811DE9C25C9D88300A708ED /* KeyValueStorage.swift */,
 				3811DE9925C9D88300A708ED /* Cache */,
+				38E44529274E40F100EC9A94 /* Disk */,
 				3811DE9D25C9D88300A708ED /* Keychain */,
 			);
 			path = Storage;
@@ -885,6 +907,7 @@
 		3811DEE325CA063400A708ED /* PropertyWrappers */ = {
 			isa = PBXGroup;
 			children = (
+				38E44527274E401C00EC9A94 /* Protected.swift */,
 				3811DEE425CA063400A708ED /* Injected.swift */,
 				3811DEE625CA063400A708ED /* SyncAccess.swift */,
 				3811DEE725CA063400A708ED /* PersistedProperty.swift */,
@@ -1145,6 +1168,23 @@
 			path = SwiftNotificationCenter;
 			sourceTree = "<group>";
 		};
+		38E44529274E40F100EC9A94 /* Disk */ = {
+			isa = PBXGroup;
+			children = (
+				38E4452C274E411600EC9A94 /* Disk.swift */,
+				38E4452E274E411600EC9A94 /* Disk+[Data].swift */,
+				38E44530274E411700EC9A94 /* Disk+[UIImage].swift */,
+				38E44532274E411700EC9A94 /* Disk+Codable.swift */,
+				38E4452B274E411600EC9A94 /* Disk+Data.swift */,
+				38E44533274E411700EC9A94 /* Disk+Errors.swift */,
+				38E4452D274E411600EC9A94 /* Disk+Helpers.swift */,
+				38E4452A274E411600EC9A94 /* Disk+InternalHelpers.swift */,
+				38E4452F274E411600EC9A94 /* Disk+UIImage.swift */,
+				38E44531274E411700EC9A94 /* Disk+VolumeInformation.swift */,
+			);
+			path = Disk;
+			sourceTree = "<group>";
+		};
 		38E98A1A25F52C9300C0CED0 /* Logger */ = {
 			isa = PBXGroup;
 			children = (
@@ -1518,10 +1558,8 @@
 			name = FreeAPS;
 			packageProductDependencies = (
 				3811DE0F25C9D37700A708ED /* Swinject */,
-				383948D225CD4D6D00E91849 /* Disk */,
 				38B17B6525DD90E0005CAE3D /* SwiftDate */,
 				3833B46C26012030003021B3 /* Algorithms */,
-				38192E00261B826A0094D973 /* Alamofire */,
 				3818AA46274C255A00843DB3 /* LibreTransmitter */,
 			);
 			productName = FreeAPS;
@@ -1594,10 +1632,8 @@
 			mainGroup = 388E594F25AD948C0019842D;
 			packageReferences = (
 				3811DE0E25C9D37700A708ED /* XCRemoteSwiftPackageReference "Swinject" */,
-				383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */,
 				38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */,
 				3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
-				38192DFF261B826A0094D973 /* XCRemoteSwiftPackageReference "Alamofire" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -1659,6 +1695,7 @@
 			files = (
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
+				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
@@ -1669,6 +1706,7 @@
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
+				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
@@ -1697,17 +1735,20 @@
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
+				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
+				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
+				38E44528274E401C00EC9A94 /* Protected.swift in Sources */,
 				3811DEB625C9D88300A708ED /* UnlockManager.swift in Sources */,
 				E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */,
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
@@ -1767,6 +1808,7 @@
 				3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */,
 				38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */,
 				38AAF8712600C1B0004AF583 /* MainChartView.swift in Sources */,
+				38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
@@ -1781,6 +1823,7 @@
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				448B6FCB252BD4796E2960C0 /* PumpSettingsEditorDataFlow.swift in Sources */,
+				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */,
 				6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */,
 				A0B8EC8CC5CD1DD237D1BCD2 /* PumpSettingsEditorRootView.swift in Sources */,
@@ -1809,7 +1852,9 @@
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */,
+				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
+				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
@@ -1821,6 +1866,7 @@
 				DD399FB31EACB9343C944C4C /* PreferencesEditorStateModel.swift in Sources */,
 				44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */,
 				E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */,
+				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
 				A6F097A14CAAE0CE0D11BE1B /* AddCarbsProvider.swift in Sources */,
 				33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */,
 				28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */,
@@ -1841,7 +1887,9 @@
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
+				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
+				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */,
 				891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */,
@@ -2198,14 +2246,6 @@
 				minimumVersion = 2.7.1;
 			};
 		};
-		38192DFF261B826A0094D973 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/Alamofire/Alamofire";
-			requirement = {
-				kind = upToNextMajorVersion;
-				minimumVersion = 5.4.2;
-			};
-		};
 		3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/apple/swift-algorithms";
@@ -2214,14 +2254,6 @@
 				minimumVersion = 0.0.3;
 			};
 		};
-		383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/saoudrizwan/Disk";
-			requirement = {
-				kind = upToNextMajorVersion;
-				minimumVersion = 0.6.4;
-			};
-		};
 		38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/malcommac/SwiftDate";
@@ -2242,21 +2274,11 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = LibreTransmitter;
 		};
-		38192E00261B826A0094D973 /* Alamofire */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 38192DFF261B826A0094D973 /* XCRemoteSwiftPackageReference "Alamofire" */;
-			productName = Alamofire;
-		};
 		3833B46C26012030003021B3 /* Algorithms */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
 			productName = Algorithms;
 		};
-		383948D225CD4D6D00E91849 /* Disk */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */;
-			productName = Disk;
-		};
 		38B17B6525DD90E0005CAE3D /* SwiftDate */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;

+ 0 - 18
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -2,24 +2,6 @@
   "object": {
     "pins": [
       {
-        "package": "Alamofire",
-        "repositoryURL": "https://github.com/Alamofire/Alamofire",
-        "state": {
-          "branch": null,
-          "revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc",
-          "version": "5.4.4"
-        }
-      },
-      {
-        "package": "Disk",
-        "repositoryURL": "https://github.com/saoudrizwan/Disk",
-        "state": {
-          "branch": null,
-          "revision": "b0cb4fdf23e51849cc2460bdc6de795c3bcca99d",
-          "version": "0.6.4"
-        }
-      },
-      {
         "package": "swift-algorithms",
         "repositoryURL": "https://github.com/apple/swift-algorithms",
         "state": {

+ 0 - 1
FreeAPS/Sources/Assemblies/NetworkAssembly.swift

@@ -1,4 +1,3 @@
-import Alamofire
 import Foundation
 import Swinject
 

+ 139 - 0
FreeAPS/Sources/Helpers/PropertyWrappers/Protected.swift

@@ -0,0 +1,139 @@
+import Foundation
+
+private protocol Lock {
+    func lock()
+    func unlock()
+}
+
+extension Lock {
+    /// Executes a closure returning a value while acquiring the lock.
+    ///
+    /// - Parameter closure: The closure to run.
+    ///
+    /// - Returns:           The value the closure generated.
+    func around<T>(_ closure: () -> T) -> T {
+        lock()
+        defer { unlock() }
+        return closure()
+    }
+
+    /// Execute a closure while acquiring the lock.
+    ///
+    /// - Parameter closure: The closure to run.
+    func around(_ closure: () -> Void) {
+        lock()
+        defer { unlock() }
+        closure()
+    }
+}
+
+/// An `os_unfair_lock` wrapper.
+final class UnfairLock: Lock {
+    private let unfairLock: os_unfair_lock_t
+
+    init() {
+        unfairLock = .allocate(capacity: 1)
+        unfairLock.initialize(to: os_unfair_lock())
+    }
+
+    deinit {
+        unfairLock.deinitialize(count: 1)
+        unfairLock.deallocate()
+    }
+
+    fileprivate func lock() {
+        os_unfair_lock_lock(unfairLock)
+    }
+
+    fileprivate func unlock() {
+        os_unfair_lock_unlock(unfairLock)
+    }
+}
+
+/// A thread-safe wrapper around a value.
+@propertyWrapper
+@dynamicMemberLookup
+final class Protected<T> {
+    private let lock = UnfairLock()
+
+    private var value: T
+
+    init(_ value: T) {
+        self.value = value
+    }
+
+    /// The contained value. Unsafe for anything more than direct read or write.
+    var wrappedValue: T {
+        get { lock.around { value } }
+        set { lock.around { value = newValue } }
+    }
+
+    var projectedValue: Protected<T> { self }
+
+    init(wrappedValue: T) {
+        value = wrappedValue
+    }
+
+    /// Synchronously read or transform the contained value.
+    ///
+    /// - Parameter closure: The closure to execute.
+    ///
+    /// - Returns:           The return value of the closure passed.
+    func read<U>(_ closure: (T) -> U) -> U {
+        lock.around { closure(self.value) }
+    }
+
+    /// Synchronously modify the protected value.
+    ///
+    /// - Parameter closure: The closure to execute.
+    ///
+    /// - Returns:           The modified value.
+    @discardableResult func write<U>(_ closure: (inout T) -> U) -> U {
+        lock.around { closure(&self.value) }
+    }
+
+    subscript<Property>(dynamicMember keyPath: WritableKeyPath<T, Property>) -> Property {
+        get { lock.around { value[keyPath: keyPath] } }
+        set { lock.around { value[keyPath: keyPath] = newValue } }
+    }
+}
+
+extension Protected where T: RangeReplaceableCollection {
+    /// Adds a new element to the end of this protected collection.
+    ///
+    /// - Parameter newElement: The `Element` to append.
+    func append(_ newElement: T.Element) {
+        write { (ward: inout T) in
+            ward.append(newElement)
+        }
+    }
+
+    /// Adds the elements of a sequence to the end of this protected collection.
+    ///
+    /// - Parameter newElements: The `Sequence` to append.
+    func append<S: Sequence>(contentsOf newElements: S) where S.Element == T.Element {
+        write { (ward: inout T) in
+            ward.append(contentsOf: newElements)
+        }
+    }
+
+    /// Add the elements of a collection to the end of the protected collection.
+    ///
+    /// - Parameter newElements: The `Collection` to append.
+    func append<C: Collection>(contentsOf newElements: C) where C.Element == T.Element {
+        write { (ward: inout T) in
+            ward.append(contentsOf: newElements)
+        }
+    }
+}
+
+extension Protected where T == Data? {
+    /// Adds the contents of a `Data` value to the end of the protected `Data`.
+    ///
+    /// - Parameter data: The `Data` to be appended.
+    func append(_ data: Data) {
+        write { (ward: inout T) in
+            ward?.append(data)
+        }
+    }
+}

+ 246 - 0
FreeAPS/Sources/Services/Network/NetworkReachabilityManager.swift

@@ -0,0 +1,246 @@
+#if !(os(watchOS) || os(Linux) || os(Windows))
+
+    import Foundation
+    import SystemConfiguration
+
+    /// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and
+    /// WiFi network interfaces.
+    ///
+    /// Reachability can be used to determine background information about why a network operation failed, or to retry
+    /// network requests when a connection is established. It should not be used to prevent a user from initiating a network
+    /// request, as it's possible that an initial request may be required to establish reachability.
+    open class NetworkReachabilityManager {
+        /// Defines the various states of network reachability.
+        public enum NetworkReachabilityStatus {
+            /// It is unknown whether the network is reachable.
+            case unknown
+            /// The network is not reachable.
+            case notReachable
+            /// The network is reachable on the associated `ConnectionType`.
+            case reachable(ConnectionType)
+
+            init(_ flags: SCNetworkReachabilityFlags) {
+                guard flags.isActuallyReachable else { self = .notReachable
+                    return }
+
+                var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
+
+                if flags.isCellular { networkStatus = .reachable(.cellular) }
+
+                self = networkStatus
+            }
+
+            /// Defines the various connection types detected by reachability flags.
+            public enum ConnectionType {
+                /// The connection type is either over Ethernet or WiFi.
+                case ethernetOrWiFi
+                /// The connection type is a cellular connection.
+                case cellular
+            }
+        }
+
+        /// A closure executed when the network reachability status changes. The closure takes a single argument: the
+        /// network reachability status.
+        public typealias Listener = (NetworkReachabilityStatus) -> Void
+
+        /// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`.
+        public static let `default` = NetworkReachabilityManager()
+
+        // MARK: - Properties
+
+        /// Whether the network is currently reachable.
+        open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi }
+
+        /// Whether the network is currently reachable over the cellular interface.
+        ///
+        /// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended.
+        ///         Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued.
+        ///
+        open var isReachableOnCellular: Bool { status == .reachable(.cellular) }
+
+        /// Whether the network is currently reachable over Ethernet or WiFi interface.
+        open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) }
+
+        /// `DispatchQueue` on which reachability will update.
+        public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue")
+
+        /// Flags of the current reachability type, if any.
+        open var flags: SCNetworkReachabilityFlags? {
+            var flags = SCNetworkReachabilityFlags()
+
+            return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil
+        }
+
+        /// The current network reachability status.
+        open var status: NetworkReachabilityStatus {
+            flags.map(NetworkReachabilityStatus.init) ?? .unknown
+        }
+
+        /// Mutable state storage.
+        struct MutableState {
+            /// A closure executed when the network reachability status changes.
+            var listener: Listener?
+            /// `DispatchQueue` on which listeners will be called.
+            var listenerQueue: DispatchQueue?
+            /// Previously calculated status.
+            var previousStatus: NetworkReachabilityStatus?
+        }
+
+        /// `SCNetworkReachability` instance providing notifications.
+        private let reachability: SCNetworkReachability
+
+        /// Protected storage for mutable state.
+        @Protected private var mutableState = MutableState()
+
+        // MARK: - Initialization
+
+        /// Creates an instance with the specified host.
+        ///
+        /// - Note: The `host` value must *not* contain a scheme, just the hostname.
+        ///
+        /// - Parameters:
+        ///   - host:          Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`).
+        public convenience init?(host: String) {
+            guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
+
+            self.init(reachability: reachability)
+        }
+
+        /// Creates an instance that monitors the address 0.0.0.0.
+        ///
+        /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
+        /// status of the device, both IPv4 and IPv6.
+        public convenience init?() {
+            var zero = sockaddr()
+            zero.sa_len = UInt8(MemoryLayout<sockaddr>.size)
+            zero.sa_family = sa_family_t(AF_INET)
+
+            guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil }
+
+            self.init(reachability: reachability)
+        }
+
+        private init(reachability: SCNetworkReachability) {
+            self.reachability = reachability
+        }
+
+        deinit {
+            stopListening()
+        }
+
+        // MARK: - Listening
+
+        /// Starts listening for changes in network reachability status.
+        ///
+        /// - Note: Stops and removes any existing listener.
+        ///
+        /// - Parameters:
+        ///   - queue:    `DispatchQueue` on which to call the `listener` closure. `.main` by default.
+        ///   - listener: `Listener` closure called when reachability changes.
+        ///
+        /// - Returns: `true` if listening was started successfully, `false` otherwise.
+        @discardableResult open func startListening(
+            onQueue queue: DispatchQueue = .main,
+            onUpdatePerforming listener: @escaping Listener
+        ) -> Bool {
+            stopListening()
+
+            $mutableState.write { state in
+                state.listenerQueue = queue
+                state.listener = listener
+            }
+
+            var context = SCNetworkReachabilityContext(
+                version: 0,
+                info: Unmanaged.passUnretained(self).toOpaque(),
+                retain: nil,
+                release: nil,
+                copyDescription: nil
+            )
+            let callback: SCNetworkReachabilityCallBack = { _, flags, info in
+                guard let info = info else { return }
+
+                let instance = Unmanaged<NetworkReachabilityManager>.fromOpaque(info).takeUnretainedValue()
+                instance.notifyListener(flags)
+            }
+
+            let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue)
+            let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context)
+
+            // Manually call listener to give initial state, since the framework may not.
+            if let currentFlags = flags {
+                reachabilityQueue.async {
+                    self.notifyListener(currentFlags)
+                }
+            }
+
+            return callbackAdded && queueAdded
+        }
+
+        /// Stops listening for changes in network reachability status.
+        open func stopListening() {
+            SCNetworkReachabilitySetCallback(reachability, nil, nil)
+            SCNetworkReachabilitySetDispatchQueue(reachability, nil)
+            $mutableState.write { state in
+                state.listener = nil
+                state.listenerQueue = nil
+                state.previousStatus = nil
+            }
+        }
+
+        // MARK: - Internal - Listener Notification
+
+        /// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed.
+        ///
+        /// - Note: Should only be called from the `reachabilityQueue`.
+        ///
+        /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status.
+        func notifyListener(_ flags: SCNetworkReachabilityFlags) {
+            let newStatus = NetworkReachabilityStatus(flags)
+
+            $mutableState.write { state in
+                guard state.previousStatus != newStatus else { return }
+
+                state.previousStatus = newStatus
+
+                let listener = state.listener
+                state.listenerQueue?.async { listener?(newStatus) }
+            }
+        }
+    }
+
+    // MARK: -
+
+    extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {}
+
+    extension SCNetworkReachabilityFlags {
+        var isReachable: Bool { contains(.reachable) }
+        var isConnectionRequired: Bool { contains(.connectionRequired) }
+        var canConnectAutomatically: Bool { contains(.connectionOnDemand) || contains(.connectionOnTraffic) }
+        var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) }
+        var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) }
+        var isCellular: Bool {
+            #if os(iOS) || os(tvOS)
+                return contains(.isWWAN)
+            #else
+                return false
+            #endif
+        }
+
+        /// Human readable `String` for all states, to help with debugging.
+        var readableDescription: String {
+            let W = isCellular ? "W" : "-"
+            let R = isReachable ? "R" : "-"
+            let c = isConnectionRequired ? "c" : "-"
+            let t = contains(.transientConnection) ? "t" : "-"
+            let i = contains(.interventionRequired) ? "i" : "-"
+            let C = contains(.connectionOnTraffic) ? "C" : "-"
+            let D = contains(.connectionOnDemand) ? "D" : "-"
+            let l = contains(.isLocalAddress) ? "l" : "-"
+            let d = contains(.isDirect) ? "d" : "-"
+            let a = contains(.connectionAutomatic) ? "a" : "-"
+
+            return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)"
+        }
+    }
+#endif

+ 0 - 1
FreeAPS/Sources/Services/Network/ReachabilityManager.swift

@@ -1,4 +1,3 @@
-import Alamofire
 import Foundation
 
 typealias ReachabilityStatus = NetworkReachabilityManager.NetworkReachabilityStatus

+ 170 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+Codable.swift

@@ -0,0 +1,170 @@
+import Foundation
+
+public extension Disk {
+    /// Save encodable struct to disk as JSON data
+    ///
+    /// - Parameters:
+    ///   - value: the Encodable struct to store
+    ///   - directory: user directory to store the file in
+    ///   - path: file location to store the data (i.e. "Folder/file.json")
+    ///   - encoder: custom JSONEncoder to encode value
+    /// - Throws: Error if there were any issues encoding the struct or writing it to disk
+    static func save<T: Encodable>(
+        _ value: T,
+        to directory: Directory,
+        as path: String,
+        encoder: JSONEncoder = JSONEncoder()
+    ) throws {
+        if path.hasSuffix("/") {
+            throw createInvalidFileNameForStructsError()
+        }
+        do {
+            let url = try createURL(for: path, in: directory)
+            let data = try encoder.encode(value)
+            try createSubfoldersBeforeCreatingFile(at: url)
+            try data.write(to: url, options: .atomic)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Append Codable struct JSON data to a file's data
+    ///
+    /// - Parameters:
+    ///   - value: the struct to store to disk
+    ///   - path: file location to store the data (i.e. "Folder/file.json")
+    ///   - directory: user directory to store the file in
+    ///   - decoder: custom JSONDecoder to decode existing values
+    ///   - encoder: custom JSONEncoder to encode new value
+    /// - Throws: Error if there were any issues with encoding/decoding or writing the encoded struct to disk
+    static func append<T: Codable>(
+        _ value: T,
+        to path: String,
+        in directory: Directory,
+        decoder: JSONDecoder = JSONDecoder(),
+        encoder: JSONEncoder = JSONEncoder()
+    ) throws {
+        if path.hasSuffix("/") {
+            throw createInvalidFileNameForStructsError()
+        }
+        do {
+            if let url = try? getExistingFileURL(for: path, in: directory) {
+                let oldData = try Data(contentsOf: url)
+                if !(!oldData.isEmpty) {
+                    try save([value], to: directory, as: path, encoder: encoder)
+                } else {
+                    let new: [T]
+                    if let old = try? decoder.decode(T.self, from: oldData) {
+                        new = [old, value]
+                    } else if var old = try? decoder.decode([T].self, from: oldData) {
+                        old.append(value)
+                        new = old
+                    } else {
+                        throw createDeserializationErrorForAppendingStructToInvalidType(url: url, type: value)
+                    }
+                    let newData = try encoder.encode(new)
+                    try newData.write(to: url, options: .atomic)
+                }
+            } else {
+                try save([value], to: directory, as: path, encoder: encoder)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Append Codable struct array JSON data to a file's data
+    ///
+    /// - Parameters:
+    ///   - value: the Codable struct array to store
+    ///   - path: file location to store the data (i.e. "Folder/file.json")
+    ///   - directory: user directory to store the file in
+    ///   - decoder: custom JSONDecoder to decode existing values
+    ///   - encoder: custom JSONEncoder to encode new value
+    /// - Throws: Error if there were any issues writing the encoded struct array to disk
+    static func append<T: Codable>(
+        _ value: [T],
+        to path: String,
+        in directory: Directory,
+        decoder: JSONDecoder = JSONDecoder(),
+        encoder: JSONEncoder = JSONEncoder()
+    ) throws {
+        if path.hasSuffix("/") {
+            throw createInvalidFileNameForStructsError()
+        }
+        do {
+            if let url = try? getExistingFileURL(for: path, in: directory) {
+                let oldData = try Data(contentsOf: url)
+                if !(!oldData.isEmpty) {
+                    try save(value, to: directory, as: path, encoder: encoder)
+                } else {
+                    let new: [T]
+                    if let old = try? decoder.decode(T.self, from: oldData) {
+                        new = [old] + value
+                    } else if var old = try? decoder.decode([T].self, from: oldData) {
+                        old.append(contentsOf: value)
+                        new = old
+                    } else {
+                        throw createDeserializationErrorForAppendingStructToInvalidType(url: url, type: value)
+                    }
+                    let newData = try encoder.encode(new)
+                    try newData.write(to: url, options: .atomic)
+                }
+            } else {
+                try save(value, to: directory, as: path, encoder: encoder)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Retrieve and decode a struct from a file on disk
+    ///
+    /// - Parameters:
+    ///   - path: path of the file holding desired data
+    ///   - directory: user directory to retrieve the file from
+    ///   - type: struct type (i.e. Message.self or [Message].self)
+    ///   - decoder: custom JSONDecoder to decode existing values
+    /// - Returns: decoded structs of data
+    /// - Throws: Error if there were any issues retrieving the data or decoding it to the specified type
+    static func retrieve<T: Decodable>(
+        _ path: String,
+        from directory: Directory,
+        as type: T.Type,
+        decoder: JSONDecoder = JSONDecoder()
+    ) throws -> T {
+        if path.hasSuffix("/") {
+            throw createInvalidFileNameForStructsError()
+        }
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            let data = try Data(contentsOf: url)
+            let value = try decoder.decode(type, from: data)
+            return value
+        } catch {
+            throw error
+        }
+    }
+}
+
+private extension Disk {
+    /// Helper method to create deserialization error for append(:path:directory:) functions
+    static func createDeserializationErrorForAppendingStructToInvalidType<T>(url: URL, type _: T) -> Error {
+        Disk.createError(
+            .deserialization,
+            description: "Could not deserialize the existing data at \(url.path) to a valid type to append to.",
+            failureReason: "JSONDecoder could not decode type \(T.self) from the data existing at the file location.",
+            recoverySuggestion: "Ensure that you only append data structure(s) with the same type as the data existing at the file location."
+        )
+    }
+
+    /// Helper method to create error for when trying to saving Codable structs as multiple files to a folder
+    static func createInvalidFileNameForStructsError() -> Error {
+        Disk.createError(
+            .invalidFileName,
+            description: "Cannot save/retrieve the Codable struct without a valid file name. Unlike how arrays of UIImages or Data are stored, Codable structs are not saved as multiple files in a folder, but rather as one JSON file. If you already successfully saved Codable struct(s) to your folder name, try retrieving it as a file named 'Folder' instead of as a folder 'Folder/'",
+            failureReason: "Disk does not save structs or arrays of structs as multiple files to a folder like it does UIImages or Data.",
+            recoverySuggestion: "Save your struct or array of structs as one file that encapsulates all the data (i.e. \"multiple-messages.json\")"
+        )
+    }
+}

+ 38 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+Data.swift

@@ -0,0 +1,38 @@
+import Foundation
+
+public extension Disk {
+    /// Save Data to disk
+    ///
+    /// - Parameters:
+    ///   - value: Data to store to disk
+    ///   - directory: user directory to store the file in
+    ///   - path: file location to store the data (i.e. "Folder/file.mp4")
+    /// - Throws: Error if there were any issues writing the given data to disk
+    static func save(_ value: Data, to directory: Directory, as path: String) throws {
+        do {
+            let url = try createURL(for: path, in: directory)
+            try createSubfoldersBeforeCreatingFile(at: url)
+            try value.write(to: url, options: .atomic)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Retrieve data from disk
+    ///
+    /// - Parameters:
+    ///   - path: path where data file is stored
+    ///   - directory: user directory to retrieve the file from
+    ///   - type: here for Swifty generics magic, use Data.self
+    /// - Returns: Data retrieved from disk
+    /// - Throws: Error if there were any issues retrieving the specified file's data
+    static func retrieve(_ path: String, from directory: Directory, as _: Data.Type) throws -> Data {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            let data = try Data(contentsOf: url)
+            return data
+        } catch {
+            throw error
+        }
+    }
+}

+ 30 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+Errors.swift

@@ -0,0 +1,30 @@
+import Foundation
+
+public extension Disk {
+    enum ErrorCode: Int {
+        case noFileFound = 0
+        case serialization = 1
+        case deserialization = 2
+        case invalidFileName = 3
+        case couldNotAccessTemporaryDirectory = 4
+        case couldNotAccessUserDomainMask = 5
+        case couldNotAccessSharedContainer = 6
+    }
+
+    static let errorDomain = "DiskErrorDomain"
+
+    /// Create custom error that FileManager can't account for
+    internal static func createError(
+        _ errorCode: ErrorCode,
+        description: String?,
+        failureReason: String?,
+        recoverySuggestion: String?
+    ) -> Error {
+        let errorInfo: [String: Any] = [
+            NSLocalizedDescriptionKey: description ?? "",
+            NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion ?? "",
+            NSLocalizedFailureReasonErrorKey: failureReason ?? ""
+        ]
+        return NSError(domain: errorDomain, code: errorCode.rawValue, userInfo: errorInfo) as Error
+    }
+}

+ 243 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+Helpers.swift

@@ -0,0 +1,243 @@
+import Foundation
+
+public extension Disk {
+    /// Get URL for existing file
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory (set nil for entire directory)
+    ///   - directory: directory the file is saved in
+    /// - Returns: URL pointing to file
+    /// - Throws: Error if no file could be found
+    @available(
+        *,
+        deprecated,
+        message: "Use Disk.url(for:in:) instead, it does not throw an error if the file does not exist."
+    ) static func getURL(for path: String?, in directory: Directory) throws -> URL {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            return url
+        } catch {
+            throw error
+        }
+    }
+
+    /// Construct URL for a potentially existing or non-existent file (Note: replaces `getURL(for:in:)` which would throw an error if file does not exist)
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory (set nil for entire directory)
+    ///   - directory: directory for the specified path
+    /// - Returns: URL for either an existing or non-existing file
+    /// - Throws: Error if URL creation failed
+    static func url(for path: String?, in directory: Directory) throws -> URL {
+        do {
+            let url = try createURL(for: path, in: directory)
+            return url
+        } catch {
+            throw error
+        }
+    }
+
+    /// Clear directory by removing all files
+    ///
+    /// - Parameter directory: directory to clear
+    /// - Throws: Error if FileManager cannot access a directory
+    static func clear(_ directory: Directory) throws {
+        do {
+            let url = try createURL(for: nil, in: directory)
+            let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
+            for fileUrl in contents {
+                try? FileManager.default.removeItem(at: fileUrl)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Remove file from the file system
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory where file is located
+    /// - Throws: Error if file could not be removed
+    static func remove(_ path: String, from directory: Directory) throws {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            try FileManager.default.removeItem(at: url)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Remove file from the file system
+    ///
+    /// - Parameters:
+    ///   - url: URL of file in filesystem
+    /// - Throws: Error if file could not be removed
+    static func remove(_ url: URL) throws {
+        do {
+            try FileManager.default.removeItem(at: url)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Checks if a file exists
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory where file is located
+    /// - Returns: Bool indicating whether file exists
+    static func exists(_ path: String, in directory: Directory) -> Bool {
+        if let _ = try? getExistingFileURL(for: path, in: directory) {
+            return true
+        }
+        return false
+    }
+
+    /// Checks if a file exists
+    ///
+    /// - Parameters:
+    ///   - url: URL of file in filesystem
+    /// - Returns: Bool indicating whether file exists
+    static func exists(_ url: URL) -> Bool {
+        if FileManager.default.fileExists(atPath: url.path) {
+            return true
+        }
+        return false
+    }
+
+    /// Sets the 'do not backup' attribute of the file or folder on disk to true. This ensures that the file holding the object data does not get deleted when the user's device has low storage, but prevents this file from being stored in any backups made of the device on iTunes or iCloud.
+    /// This is only useful for excluding cache and other application support files which are not needed in a backup. Some operations commonly made to user documents will cause the 'do not backup' property to be reset to false and so this should not be used on user documents.
+    /// Warning: You must ensure that you will purge and handle any files created with this attribute appropriately, as these files will persist on the user's disk even in low storage situtations. If you don't handle these files appropriately, then you aren't following Apple's file system guidlines and can face App Store rejection.
+    /// Ideally, you should let iOS handle deletion of files in low storage situations, and you yourself handle missing files appropriately (i.e. retrieving an image from the web again if it does not exist on disk anymore.)
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory where file is located
+    /// - Throws: Error if file could not set its 'isExcludedFromBackup' property
+    static func doNotBackup(_ path: String, in directory: Directory) throws {
+        do {
+            try setIsExcludedFromBackup(to: true, for: path, in: directory)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Sets the 'do not backup' attribute of the file or folder on disk to true. This ensures that the file holding the object data does not get deleted when the user's device has low storage, but prevents this file from being stored in any backups made of the device on iTunes or iCloud.
+    /// This is only useful for excluding cache and other application support files which are not needed in a backup. Some operations commonly made to user documents will cause the 'do not backup' property to be reset to false and so this should not be used on user documents.
+    /// Warning: You must ensure that you will purge and handle any files created with this attribute appropriately, as these files will persist on the user's disk even in low storage situtations. If you don't handle these files appropriately, then you aren't following Apple's file system guidlines and can face App Store rejection.
+    /// Ideally, you should let iOS handle deletion of files in low storage situations, and you yourself handle missing files appropriately (i.e. retrieving an image from the web again if it does not exist on disk anymore.)
+    ///
+    /// - Parameters:
+    ///   - url: URL of file in filesystem
+    /// - Throws: Error if file could not set its 'isExcludedFromBackup' property
+    static func doNotBackup(_ url: URL) throws {
+        do {
+            try setIsExcludedFromBackup(to: true, for: url)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Sets the 'do not backup' attribute of the file or folder on disk to false. This is the default behaviour so you don't have to use this function unless you already called doNotBackup(name:directory:) on a specific file.
+    /// This default backing up behaviour allows anything in the .documents and .caches directories to be stored in backups made of the user's device (on iCloud or iTunes)
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory where file is located
+    /// - Throws: Error if file could not set its 'isExcludedFromBackup' property
+    static func backup(_ path: String, in directory: Directory) throws {
+        do {
+            try setIsExcludedFromBackup(to: false, for: path, in: directory)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Sets the 'do not backup' attribute of the file or folder on disk to false. This is the default behaviour so you don't have to use this function unless you already called doNotBackup(name:directory:) on a specific file.
+    /// This default backing up behaviour allows anything in the .documents and .caches directories to be stored in backups made of the user's device (on iCloud or iTunes)
+    ///
+    /// - Parameters:
+    ///   - url: URL of file in filesystem
+    /// - Throws: Error if file could not set its 'isExcludedFromBackup' property
+    static func backup(_ url: URL) throws {
+        do {
+            try setIsExcludedFromBackup(to: false, for: url)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Move file to a new directory
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory the file is currently in
+    ///   - newDirectory: new directory to store file in
+    /// - Throws: Error if file could not be moved
+    static func move(_ path: String, in directory: Directory, to newDirectory: Directory) throws {
+        do {
+            let currentUrl = try getExistingFileURL(for: path, in: directory)
+            let justDirectoryPath = try createURL(for: nil, in: directory).absoluteString
+            let filePath = currentUrl.absoluteString.replacingOccurrences(of: justDirectoryPath, with: "")
+            let newUrl = try createURL(for: filePath, in: newDirectory)
+            try createSubfoldersBeforeCreatingFile(at: newUrl)
+            try FileManager.default.moveItem(at: currentUrl, to: newUrl)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Move file to a new directory
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory the file is currently in
+    ///   - newDirectory: new directory to store file in
+    /// - Throws: Error if file could not be moved
+    static func move(_ originalURL: URL, to newURL: URL) throws {
+        do {
+            try createSubfoldersBeforeCreatingFile(at: newURL)
+            try FileManager.default.moveItem(at: originalURL, to: newURL)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Rename a file
+    ///
+    /// - Parameters:
+    ///   - path: path of file relative to directory
+    ///   - directory: directory the file is in
+    ///   - newName: new name to give to file
+    /// - Throws: Error if object could not be renamed
+    static func rename(_ path: String, in directory: Directory, to newPath: String) throws {
+        do {
+            let currentUrl = try getExistingFileURL(for: path, in: directory)
+            let justDirectoryPath = try createURL(for: nil, in: directory).absoluteString
+            var currentFilePath = currentUrl.absoluteString.replacingOccurrences(of: justDirectoryPath, with: "")
+            if isFolder(currentUrl), currentFilePath.suffix(1) != "/" {
+                currentFilePath = currentFilePath + "/"
+            }
+            let currentValidFilePath = try getValidFilePath(from: path)
+            let newValidFilePath = try getValidFilePath(from: newPath)
+            let newFilePath = currentFilePath.replacingOccurrences(of: currentValidFilePath, with: newValidFilePath)
+            let newUrl = try createURL(for: newFilePath, in: directory)
+            try createSubfoldersBeforeCreatingFile(at: newUrl)
+            try FileManager.default.moveItem(at: currentUrl, to: newUrl)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Check if file at a URL is a folder
+    static func isFolder(_ url: URL) -> Bool {
+        var isDirectory: ObjCBool = false
+        if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) {
+            if isDirectory.boolValue {
+                return true
+            }
+        }
+        return false
+    }
+}

+ 181 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+InternalHelpers.swift

@@ -0,0 +1,181 @@
+import Foundation
+
+extension Disk {
+    /// Create and returns a URL constructed from specified directory/path
+    static func createURL(for path: String?, in directory: Directory) throws -> URL {
+        let filePrefix = "file://"
+        var validPath: String?
+        if let path = path {
+            do {
+                validPath = try getValidFilePath(from: path)
+            } catch {
+                throw error
+            }
+        }
+        var searchPathDirectory: FileManager.SearchPathDirectory
+        switch directory {
+        case .documents:
+            searchPathDirectory = .documentDirectory
+        case .caches:
+            searchPathDirectory = .cachesDirectory
+        case .applicationSupport:
+            searchPathDirectory = .applicationSupportDirectory
+        case .temporary:
+            if var url = URL(string: NSTemporaryDirectory()) {
+                if let validPath = validPath {
+                    url = url.appendingPathComponent(validPath, isDirectory: false)
+                }
+                if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix {
+                    let fixedUrlString = filePrefix + url.absoluteString
+                    url = URL(string: fixedUrlString)!
+                }
+                return url
+            } else {
+                throw createError(
+                    .couldNotAccessTemporaryDirectory,
+                    description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")",
+                    failureReason: "Could not get access to the application's temporary directory.",
+                    recoverySuggestion: "Use a different directory."
+                )
+            }
+        case let .sharedContainer(appGroupName):
+            if var url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) {
+                if let validPath = validPath {
+                    url = url.appendingPathComponent(validPath, isDirectory: false)
+                }
+                if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix {
+                    let fixedUrl = filePrefix + url.absoluteString
+                    url = URL(string: fixedUrl)!
+                }
+                return url
+            } else {
+                throw createError(
+                    .couldNotAccessSharedContainer,
+                    description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")",
+                    failureReason: "Could not get access to shared container with app group named \(appGroupName).",
+                    recoverySuggestion: "Check that the app-group name in the entitlement matches the string provided."
+                )
+            }
+        }
+        if var url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first {
+            if let validPath = validPath {
+                url = url.appendingPathComponent(validPath, isDirectory: false)
+            }
+            if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix {
+                let fixedUrlString = filePrefix + url.absoluteString
+                url = URL(string: fixedUrlString)!
+            }
+            return url
+        } else {
+            throw createError(
+                .couldNotAccessUserDomainMask,
+                description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")",
+                failureReason: "Could not get access to the file system's user domain mask.",
+                recoverySuggestion: "Use a different directory."
+            )
+        }
+    }
+
+    /// Find an existing file's URL or throw an error if it doesn't exist
+    static func getExistingFileURL(for path: String?, in directory: Directory) throws -> URL {
+        do {
+            let url = try createURL(for: path, in: directory)
+            if FileManager.default.fileExists(atPath: url.path) {
+                return url
+            }
+            throw createError(
+                .noFileFound,
+                description: "Could not find an existing file or folder at \(url.path).",
+                failureReason: "There is no existing file or folder at \(url.path)",
+                recoverySuggestion: "Check if a file or folder exists before trying to commit an operation on it."
+            )
+        } catch {
+            throw error
+        }
+    }
+
+    /// Convert a user generated name to a valid file name
+    static func getValidFilePath(from originalString: String) throws -> String {
+        var invalidCharacters = CharacterSet(charactersIn: ":")
+        invalidCharacters.formUnion(.newlines)
+        invalidCharacters.formUnion(.illegalCharacters)
+        invalidCharacters.formUnion(.controlCharacters)
+        let pathWithoutIllegalCharacters = originalString
+            .components(separatedBy: invalidCharacters)
+            .joined(separator: "")
+        let validFileName = removeSlashesAtBeginning(of: pathWithoutIllegalCharacters)
+        guard !validFileName.isEmpty, validFileName != "." else {
+            throw createError(
+                .invalidFileName,
+                description: "\(originalString) is an invalid file name.",
+                failureReason: "Cannot write/read a file with the name \(originalString) on disk.",
+                recoverySuggestion: "Use another file name with alphanumeric characters."
+            )
+        }
+        return validFileName
+    }
+
+    /// Helper method for getValidFilePath(from:) to remove all "/" at the beginning of a String
+    static func removeSlashesAtBeginning(of string: String) -> String {
+        var string = string
+        if string.prefix(1) == "/" {
+            string.remove(at: string.startIndex)
+        }
+        if string.prefix(1) == "/" {
+            string = removeSlashesAtBeginning(of: string)
+        }
+        return string
+    }
+
+    /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system
+    static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for path: String?, in directory: Directory) throws {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            var resourceUrl = url
+            var resourceValues = URLResourceValues()
+            resourceValues.isExcludedFromBackup = isExcludedFromBackup
+            try resourceUrl.setResourceValues(resourceValues)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system
+    static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for url: URL) throws {
+        do {
+            var resourceUrl = url
+            var resourceValues = URLResourceValues()
+            resourceValues.isExcludedFromBackup = isExcludedFromBackup
+            try resourceUrl.setResourceValues(resourceValues)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Create necessary sub folders before creating a file
+    static func createSubfoldersBeforeCreatingFile(at url: URL) throws {
+        do {
+            let subfolderUrl = url.deletingLastPathComponent()
+            var subfolderExists = false
+            var isDirectory: ObjCBool = false
+            if FileManager.default.fileExists(atPath: subfolderUrl.path, isDirectory: &isDirectory) {
+                if isDirectory.boolValue {
+                    subfolderExists = true
+                }
+            }
+            if !subfolderExists {
+                try FileManager.default.createDirectory(at: subfolderUrl, withIntermediateDirectories: true, attributes: nil)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Get Int from a file name
+    static func fileNameInt(_ url: URL) -> Int? {
+        let fileExtension = url.pathExtension
+        let filePath = url.lastPathComponent
+        let fileName = filePath.replacingOccurrences(of: fileExtension, with: "")
+        return Int(String(fileName.filter { "0123456789".contains($0) }))
+    }
+}

+ 109 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+UIImage.swift

@@ -0,0 +1,109 @@
+import Foundation
+import UIKit
+
+public extension Disk {
+    /// Save image to disk
+    ///
+    /// - Parameters:
+    ///   - value: image to store to disk
+    ///   - directory: user directory to store the image file in
+    ///   - path: file location to store the data (i.e. "Folder/file.png")
+    /// - Throws: Error if there were any issues writing the image to disk
+    static func save(_ value: UIImage, to directory: Directory, as path: String) throws {
+        do {
+            var imageData: Data
+            if path.suffix(4).lowercased() == ".png" {
+                let pngData: Data?
+                #if swift(>=4.2)
+                    pngData = value.pngData()
+                #else
+                    pngData = UIImagePNGRepresentation(value)
+                #endif
+                if let data = pngData {
+                    imageData = data
+                } else {
+                    throw createError(
+                        .serialization,
+                        description: "Could not serialize UIImage to PNG.",
+                        failureReason: "Data conversion failed.",
+                        recoverySuggestion: "Try saving this image as a .jpg or without an extension at all."
+                    )
+                }
+            } else if path.suffix(4).lowercased() == ".jpg" || path.suffix(5).lowercased() == ".jpeg" {
+                let jpegData: Data?
+                #if swift(>=4.2)
+                    jpegData = value.jpegData(compressionQuality: 1)
+                #else
+                    jpegData = UIImageJPEGRepresentation(value, 1)
+                #endif
+                if let data = jpegData {
+                    imageData = data
+                } else {
+                    throw createError(
+                        .serialization,
+                        description: "Could not serialize UIImage to JPEG.",
+                        failureReason: "Data conversion failed.",
+                        recoverySuggestion: "Try saving this image as a .png or without an extension at all."
+                    )
+                }
+            } else {
+                var data: Data?
+                #if swift(>=4.2)
+                    if let pngData = value.pngData() {
+                        data = pngData
+                    } else if let jpegData = value.jpegData(compressionQuality: 1) {
+                        data = jpegData
+                    }
+                #else
+                    if let pngData = UIImagePNGRepresentation(value) {
+                        data = pngData
+                    } else if let jpegData = UIImageJPEGRepresentation(value, 1) {
+                        data = jpegData
+                    }
+                #endif
+                if let data = data {
+                    imageData = data
+                } else {
+                    throw createError(
+                        .serialization,
+                        description: "Could not serialize UIImage to Data.",
+                        failureReason: "UIImage could not serialize to PNG or JPEG data.",
+                        recoverySuggestion: "Make sure image is not corrupt or try saving without an extension at all."
+                    )
+                }
+            }
+            let url = try createURL(for: path, in: directory)
+            try createSubfoldersBeforeCreatingFile(at: url)
+            try imageData.write(to: url, options: .atomic)
+        } catch {
+            throw error
+        }
+    }
+
+    /// Retrieve image from disk
+    ///
+    /// - Parameters:
+    ///   - path: path where image is stored
+    ///   - directory: user directory to retrieve the image file from
+    ///   - type: here for Swifty generics magic, use UIImage.self
+    /// - Returns: UIImage from disk
+    /// - Throws: Error if there were any issues retrieving the specified image
+    static func retrieve(_ path: String, from directory: Directory, as _: UIImage.Type) throws -> UIImage {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            let data = try Data(contentsOf: url)
+            if let image = UIImage(data: data) {
+                return image
+            } else {
+                throw createError(
+                    .deserialization,
+                    description: "Could not decode UIImage from \(url.path).",
+                    failureReason: "A UIImage could not be created out of the data in \(url.path).",
+                    recoverySuggestion: "Try deserializing \(url.path) manually after retrieving it as Data."
+                )
+            }
+        } catch {
+            throw error
+        }
+    }
+}

+ 53 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+VolumeInformation.swift

@@ -0,0 +1,53 @@
+import Foundation
+
+/// Checking Volume Storage Capacity
+/// Confirm that you have enough local storage space for a large amount of data.
+///
+/// Source: https://developer.apple.com/documentation/foundation/nsurlresourcekey/checking_volume_storage_capacity?changes=latest_major&language=objc
+@available(iOS 11.0, *)
+public extension Disk {
+    /// Helper method to query against a resource value key
+    private static func getVolumeResourceValues(for key: URLResourceKey) -> URLResourceValues? {
+        let fileUrl = URL(fileURLWithPath: "/")
+        let results = try? fileUrl.resourceValues(forKeys: [key])
+        return results
+    }
+
+    /// Volume’s total capacity in bytes.
+    static var totalCapacity: Int? {
+        let resourceValues = getVolumeResourceValues(for: .volumeTotalCapacityKey)
+        return resourceValues?.volumeTotalCapacity
+    }
+
+    /// Volume’s available capacity in bytes.
+    static var availableCapacity: Int? {
+        let resourceValues = getVolumeResourceValues(for: .volumeAvailableCapacityKey)
+        return resourceValues?.volumeAvailableCapacity
+    }
+
+    /// Volume’s available capacity in bytes for storing important resources.
+    ///
+    /// Indicates the amount of space that can be made available  for things the user has explicitly requested in the app's UI (i.e. downloading a video or new level for a game.)
+    /// If you need more space than what's available - let user know the request cannot be fulfilled.
+    static var availableCapacityForImportantUsage: Int? {
+        let resourceValues = getVolumeResourceValues(for: .volumeAvailableCapacityForImportantUsageKey)
+        if let result = resourceValues?.volumeAvailableCapacityForImportantUsage {
+            return Int(exactly: result)
+        } else {
+            return nil
+        }
+    }
+
+    /// Volume’s available capacity in bytes for storing nonessential resources.
+    ///
+    /// Indicates the amount of space available for things that the user is likely to want but hasn't explicitly requested (i.e. next episode in video series they're watching, or recently updated documents in a server that they might be likely to open.)
+    /// For these types of files you might store them initially in the caches directory until they are actually used, at which point you can move them in app support or documents directory.
+    static var availableCapacityForOpportunisticUsage: Int? {
+        let resourceValues = getVolumeResourceValues(for: .volumeAvailableCapacityForOpportunisticUsageKey)
+        if let result = resourceValues?.volumeAvailableCapacityForOpportunisticUsage {
+            return Int(exactly: result)
+        } else {
+            return nil
+        }
+    }
+}

+ 115 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+[Data].swift

@@ -0,0 +1,115 @@
+import Foundation
+
+public extension Disk {
+    /// Save an array of Data objects to disk
+    ///
+    /// - Parameters:
+    ///   - value: array of Data to store to disk
+    ///   - directory: user directory to store the files in
+    ///   - path: folder location to store the data files (i.e. "Folder/")
+    /// - Throws: Error if there were any issues creating a folder and writing the given [Data] to files in it
+    static func save(_ value: [Data], to directory: Directory, as path: String) throws {
+        do {
+            let folderUrl = try createURL(for: path, in: directory)
+            try createSubfoldersBeforeCreatingFile(at: folderUrl)
+            try FileManager.default.createDirectory(at: folderUrl, withIntermediateDirectories: false, attributes: nil)
+            for i in 0 ..< value.count {
+                let data = value[i]
+                let dataName = "\(i)"
+                let dataUrl = folderUrl.appendingPathComponent(dataName, isDirectory: false)
+                try data.write(to: dataUrl, options: .atomic)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Append a file with Data to a folder
+    ///
+    /// - Parameters:
+    ///   - value: Data to store to disk
+    ///   - directory: user directory to store the file in
+    ///   - path: folder location to store the data files (i.e. "Folder/")
+    /// - Throws: Error if there were any issues writing the given data to disk
+    static func append(_ value: Data, to path: String, in directory: Directory) throws {
+        do {
+            if let folderUrl = try? getExistingFileURL(for: path, in: directory) {
+                let fileUrls = try FileManager.default.contentsOfDirectory(
+                    at: folderUrl,
+                    includingPropertiesForKeys: nil,
+                    options: []
+                )
+                var largestFileNameInt = -1
+                for i in 0 ..< fileUrls.count {
+                    let fileUrl = fileUrls[i]
+                    if let fileNameInt = fileNameInt(fileUrl) {
+                        if fileNameInt > largestFileNameInt {
+                            largestFileNameInt = fileNameInt
+                        }
+                    }
+                }
+                let newFileNameInt = largestFileNameInt + 1
+                let data = value
+                let dataName = "\(newFileNameInt)"
+                let dataUrl = folderUrl.appendingPathComponent(dataName, isDirectory: false)
+                try data.write(to: dataUrl, options: .atomic)
+            } else {
+                let array = [value]
+                try save(array, to: directory, as: path)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Append an array of data objects as files to a folder
+    ///
+    /// - Parameters:
+    ///   - value: array of Data to store to disk
+    ///   - directory: user directory to create folder with data objects
+    ///   - path: folder location to store the data files (i.e. "Folder/")
+    /// - Throws: Error if there were any issues writing the given Data
+    static func append(_ value: [Data], to path: String, in directory: Directory) throws {
+        do {
+            if let _ = try? getExistingFileURL(for: path, in: directory) {
+                for data in value {
+                    try append(data, to: path, in: directory)
+                }
+            } else {
+                try save(value, to: directory, as: path)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Retrieve an array of Data objects from disk
+    ///
+    /// - Parameters:
+    ///   - path: path of folder that's holding the Data objects' files
+    ///   - directory: user directory where folder was created for holding Data objects
+    ///   - type: here for Swifty generics magic, use [Data].self
+    /// - Returns: [Data] from disk
+    /// - Throws: Error if there were any issues retrieving the specified folder of files
+    static func retrieve(_ path: String, from directory: Directory, as _: [Data].Type) throws -> [Data] {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            let fileUrls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
+            let sortedFileUrls = fileUrls.sorted(by: { (url1, url2) -> Bool in
+                if let fileNameInt1 = fileNameInt(url1), let fileNameInt2 = fileNameInt(url2) {
+                    return fileNameInt1 <= fileNameInt2
+                }
+                return true
+            })
+            var dataObjects = [Data]()
+            for i in 0 ..< sortedFileUrls.count {
+                let fileUrl = sortedFileUrls[i]
+                let data = try Data(contentsOf: fileUrl)
+                dataObjects.append(data)
+            }
+            return dataObjects
+        } catch {
+            throw error
+        }
+    }
+}

+ 177 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk+[UIImage].swift

@@ -0,0 +1,177 @@
+import Foundation
+import UIKit
+
+public extension Disk {
+    /// Save an array of images to disk
+    ///
+    /// - Parameters:
+    ///   - value: array of images to store
+    ///   - directory: user directory to store the images in
+    ///   - path: folder location to store the images (i.e. "Folder/")
+    /// - Throws: Error if there were any issues creating a folder and writing the given images to it
+    static func save(_ value: [UIImage], to directory: Directory, as path: String) throws {
+        do {
+            let folderUrl = try createURL(for: path, in: directory)
+            try createSubfoldersBeforeCreatingFile(at: folderUrl)
+            try FileManager.default.createDirectory(at: folderUrl, withIntermediateDirectories: false, attributes: nil)
+            for i in 0 ..< value.count {
+                let image = value[i]
+                var imageData: Data
+                var imageName = "\(i)"
+                var pngData: Data?
+                var jpegData: Data?
+                #if swift(>=4.2)
+                    if let data = image.pngData() {
+                        pngData = data
+                    } else if let data = image.jpegData(compressionQuality: 1) {
+                        jpegData = data
+                    }
+                #else
+                    if let data = UIImagePNGRepresentation(image) {
+                        pngData = data
+                    } else if let data = UIImageJPEGRepresentation(image, 1) {
+                        jpegData = data
+                    }
+                #endif
+                if let data = pngData {
+                    imageData = data
+                    imageName = imageName + ".png"
+                } else if let data = jpegData {
+                    imageData = data
+                    imageName = imageName + ".jpg"
+                } else {
+                    throw createError(
+                        .serialization,
+                        description: "Could not serialize UIImage \(i) in the array to Data.",
+                        failureReason: "UIImage \(i) could not serialize to PNG or JPEG data.",
+                        recoverySuggestion: "Make sure there are no corrupt images in the array."
+                    )
+                }
+                let imageUrl = folderUrl.appendingPathComponent(imageName, isDirectory: false)
+                try imageData.write(to: imageUrl, options: .atomic)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Append an image to a folder
+    ///
+    /// - Parameters:
+    ///   - value: image to store to disk
+    ///   - path: folder location to store the image (i.e. "Folder/")
+    ///   - directory: user directory to store the image file in
+    /// - Throws: Error if there were any issues writing the image to disk
+    static func append(_ value: UIImage, to path: String, in directory: Directory) throws {
+        do {
+            if let folderUrl = try? getExistingFileURL(for: path, in: directory) {
+                let fileUrls = try FileManager.default.contentsOfDirectory(
+                    at: folderUrl,
+                    includingPropertiesForKeys: nil,
+                    options: []
+                )
+                var largestFileNameInt = -1
+                for i in 0 ..< fileUrls.count {
+                    let fileUrl = fileUrls[i]
+                    if let fileNameInt = fileNameInt(fileUrl) {
+                        if fileNameInt > largestFileNameInt {
+                            largestFileNameInt = fileNameInt
+                        }
+                    }
+                }
+                let newFileNameInt = largestFileNameInt + 1
+                var imageData: Data
+                var imageName = "\(newFileNameInt)"
+                var pngData: Data?
+                var jpegData: Data?
+                #if swift(>=4.2)
+                    if let data = value.pngData() {
+                        pngData = data
+                    } else if let data = value.jpegData(compressionQuality: 1) {
+                        jpegData = data
+                    }
+                #else
+                    if let data = UIImagePNGRepresentation(value) {
+                        pngData = data
+                    } else if let data = UIImageJPEGRepresentation(value, 1) {
+                        jpegData = data
+                    }
+                #endif
+                if let data = pngData {
+                    imageData = data
+                    imageName = imageName + ".png"
+                } else if let data = jpegData {
+                    imageData = data
+                    imageName = imageName + ".jpg"
+                } else {
+                    throw createError(
+                        .serialization,
+                        description: "Could not serialize UIImage to Data.",
+                        failureReason: "UIImage could not serialize to PNG or JPEG data.",
+                        recoverySuggestion: "Make sure image is not corrupt."
+                    )
+                }
+                let imageUrl = folderUrl.appendingPathComponent(imageName, isDirectory: false)
+                try imageData.write(to: imageUrl, options: .atomic)
+            } else {
+                let array = [value]
+                try save(array, to: directory, as: path)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Append an array of images to a folder
+    ///
+    /// - Parameters:
+    ///   - value: images to store to disk
+    ///   - path: folder location to store the images (i.e. "Folder/")
+    ///   - directory: user directory to store the images in
+    /// - Throws: Error if there were any issues writing the images to disk
+    static func append(_ value: [UIImage], to path: String, in directory: Directory) throws {
+        do {
+            if let _ = try? getExistingFileURL(for: path, in: directory) {
+                for image in value {
+                    try append(image, to: path, in: directory)
+                }
+            } else {
+                try save(value, to: directory, as: path)
+            }
+        } catch {
+            throw error
+        }
+    }
+
+    /// Retrieve an array of images from a folder on disk
+    ///
+    /// - Parameters:
+    ///   - path: path of folder holding desired images
+    ///   - directory: user directory where images' folder was created
+    ///   - type: here for Swifty generics magic, use [UIImage].self
+    /// - Returns: [UIImage] from disk
+    /// - Throws: Error if there were any issues retrieving the specified folder of images
+    static func retrieve(_ path: String, from directory: Directory, as _: [UIImage].Type) throws -> [UIImage] {
+        do {
+            let url = try getExistingFileURL(for: path, in: directory)
+            let fileUrls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
+            let sortedFileUrls = fileUrls.sorted(by: { (url1, url2) -> Bool in
+                if let fileNameInt1 = fileNameInt(url1), let fileNameInt2 = fileNameInt(url2) {
+                    return fileNameInt1 <= fileNameInt2
+                }
+                return true
+            })
+            var images = [UIImage]()
+            for i in 0 ..< sortedFileUrls.count {
+                let fileUrl = sortedFileUrls[i]
+                let data = try Data(contentsOf: fileUrl)
+                if let image = UIImage(data: data) {
+                    images.append(image)
+                }
+            }
+            return images
+        } catch {
+            throw error
+        }
+    }
+}

+ 62 - 0
FreeAPS/Sources/Services/Storage/Disk/Disk.swift

@@ -0,0 +1,62 @@
+import Foundation
+
+/**
+ 💾 Disk
+ Easily work with the file system without worrying about any of its intricacies!
+
+ - Save Codable structs, UIImage, [UIImage], Data, [Data] to Apple recommended locations on the user's disk, without having to worry about serialization.
+ - Retrieve an object from disk as the type you specify, without having to worry about deserialization.
+ - Remove specific objects from disk, clear entire directories if you need to, check if an object exists on disk, and much more!
+ - Follow Apple's strict guidelines concerning persistence and using the file system easily.
+ */
+public class Disk {
+    private init() {}
+
+    public enum Directory: Equatable {
+        /// Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory.
+        /// Files in this directory are automatically backed up by iCloud. To disable this feature for a specific file, use the .doNotBackup(:in:) method.
+        case documents
+
+        /// Data that can be downloaded again or regenerated should be stored in the <Application_Home>/Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications.
+        /// Use this directory to write any application-specific support files that you want to persist between launches of the application or during application updates. Your application is generally responsible for adding and removing these files. It should also be able to re-create these files as needed because iTunes removes them during a full restoration of the device. In iOS 2.2 and later, the contents of this directory are not backed up by iTunes.
+        /// Note that the system may delete the Caches/ directory to free up disk space, so your app must be able to re-create or download these files as needed.
+        case caches
+
+        /// Put app-created support files in the <Application_Home>/Library/Application support directory. In general, this directory includes files that the app uses to run but that should remain hidden from the user. This directory can also include data files, configuration files, templates and modified versions of resources loaded from the app bundle.
+        /// Files in this directory are automatically backed up by iCloud. To disable this feature for a specific file, use the .doNotBackup(:in:) method.
+        case applicationSupport
+
+        /// Data that is used only temporarily should be stored in the <Application_Home>/tmp directory. Although these files are not backed up to iCloud, remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device.
+        /// The system will periodically purge these files when your app is not running; therefore, you cannot rely on these files persisting after your app terminates.
+        case temporary
+
+        /// Sandboxed apps that need to share files with other apps from the same developer on a given device can use a shared container along with the com.apple.security.application-groups entitlement.
+        /// The shared container or "app group" identifier string is used to locate the corresponding group's shared directory.
+        /// For more details, visit https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati
+        case sharedContainer(appGroupName: String)
+
+        public var pathDescription: String {
+            switch self {
+            case .documents: return "<Application_Home>/Documents"
+            case .caches: return "<Application_Home>/Library/Caches"
+            case .applicationSupport: return "<Application_Home>/Library/Application"
+            case .temporary: return "<Application_Home>/tmp"
+            case let .sharedContainer(appGroupName): return "\(appGroupName)"
+            }
+        }
+
+        public static func == (lhs: Directory, rhs: Directory) -> Bool {
+            switch (lhs, rhs) {
+            case (.applicationSupport, .applicationSupport),
+                 (.caches, .caches),
+                 (.documents, .documents),
+                 (.temporary, .temporary):
+                return true
+            case let (.sharedContainer(appGroupName: name1), .sharedContainer(appGroupName: name2)):
+                return name1 == name2
+            default:
+                return false
+            }
+        }
+    }
+}

+ 0 - 1
FreeAPS/Sources/Services/Storage/FileStorage.swift

@@ -1,4 +1,3 @@
-import Disk
 import Foundation
 
 protocol FileStorage {