Bläddra i källkod

[*] 跑通版本

yanxuyao 11 månader sedan
incheckning
9210bd1e14
100 ändrade filer med 12665 tillägg och 0 borttagningar
  1. 12 0
      .gitignore
  2. 36 0
      Frameworks/DesignKit/DesignKit.podspec
  3. 21 0
      Frameworks/DesignKit/LICENSE
  4. 15 0
      Frameworks/DesignKit/src/Avatar/UIImageViewExtensions.swift
  5. 59 0
      Frameworks/DesignKit/src/Color/UIColorExtensions.swift
  6. 42 0
      Frameworks/DesignKit/src/FavoriteButton/UIButtonExtensions.swift
  7. 83 0
      Frameworks/DesignKit/src/Font/UIFontExtensions.swift
  8. 17 0
      Frameworks/DesignKit/src/Spacing/Spacing.swift
  9. 177 0
      Frameworks/FileKit/.gitignore
  10. 1 0
      Frameworks/FileKit/.swift-version
  11. 19 0
      Frameworks/FileKit/.swiftlint.yml
  12. 32 0
      Frameworks/FileKit/.travis.yml
  13. 17 0
      Frameworks/FileKit/FileKit.playground/Contents.swift
  14. 4 0
      Frameworks/FileKit/FileKit.playground/contents.xcplayground
  15. 14 0
      Frameworks/FileKit/FileKit.podspec
  16. 1184 0
      Frameworks/FileKit/FileKit.xcodeproj/project.pbxproj
  17. 7 0
      Frameworks/FileKit/FileKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  18. 21 0
      Frameworks/FileKit/LICENSE.md
  19. 52 0
      Frameworks/FileKit/Package.swift
  20. 451 0
      Frameworks/FileKit/README.md
  21. 53 0
      Frameworks/FileKit/Sources/Array+File.swift
  22. 38 0
      Frameworks/FileKit/Sources/ArrayFile.swift
  23. 37 0
      Frameworks/FileKit/Sources/Bundle+FileKit.swift
  24. 82 0
      Frameworks/FileKit/Sources/Data+FileKit.swift
  25. 91 0
      Frameworks/FileKit/Sources/DataFile.swift
  26. 153 0
      Frameworks/FileKit/Sources/DataType.swift
  27. 53 0
      Frameworks/FileKit/Sources/Dictionary+File.swift
  28. 38 0
      Frameworks/FileKit/Sources/DictionaryFile.swift
  29. 56 0
      Frameworks/FileKit/Sources/DirectoryEnumerator.swift
  30. 93 0
      Frameworks/FileKit/Sources/DispatchEvent.swift
  31. 338 0
      Frameworks/FileKit/Sources/DispatchWatcher.swift
  32. 277 0
      Frameworks/FileKit/Sources/File.swift
  33. 107 0
      Frameworks/FileKit/Sources/FileKit.swift
  34. 169 0
      Frameworks/FileKit/Sources/FileKitError.swift
  35. 93 0
      Frameworks/FileKit/Sources/FilePermissions.swift
  36. 179 0
      Frameworks/FileKit/Sources/FileProtection.swift
  37. 341 0
      Frameworks/FileKit/Sources/FileSystemEvent.swift
  38. 115 0
      Frameworks/FileKit/Sources/FileSystemEventStream.swift
  39. 229 0
      Frameworks/FileKit/Sources/FileSystemWatcher.swift
  40. 98 0
      Frameworks/FileKit/Sources/FileType.swift
  41. 92 0
      Frameworks/FileKit/Sources/Image+FileKit.swift
  42. 36 0
      Frameworks/FileKit/Sources/ImageFile.swift
  43. 66 0
      Frameworks/FileKit/Sources/JSONType.swift
  44. 42 0
      Frameworks/FileKit/Sources/NSArray+FileKit.swift
  45. 49 0
      Frameworks/FileKit/Sources/NSData+FileKit.swift
  46. 63 0
      Frameworks/FileKit/Sources/NSDataFile.swift
  47. 37 0
      Frameworks/FileKit/Sources/NSDictionary+FileKit.swift
  48. 83 0
      Frameworks/FileKit/Sources/NSString+FileKit.swift
  49. 437 0
      Frameworks/FileKit/Sources/Operators.swift
  50. 1381 0
      Frameworks/FileKit/Sources/Path.swift
  51. 40 0
      Frameworks/FileKit/Sources/Process+FileKit.swift
  52. 66 0
      Frameworks/FileKit/Sources/PropertyListType.swift
  53. 48 0
      Frameworks/FileKit/Sources/RelativePathType.swift
  54. 71 0
      Frameworks/FileKit/Sources/String+FileKit.swift
  55. 346 0
      Frameworks/FileKit/Sources/TextFile.swift
  56. 26 0
      Frameworks/FileKit/Support/Info.plist
  57. 943 0
      Frameworks/FileKit/Tests/FileKitTests.swift
  58. 24 0
      Frameworks/FileKit/Tests/Info.plist
  59. 44 0
      Frameworks/LiveKitPlugin/LiveKitPlugin.podspec
  60. 14 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Common/Macro.swift
  61. 96 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Config/AppStorage.swift
  62. 22 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Adaptive+Extensions.swift
  63. 30 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Bundle+HxExt.swift
  64. 75 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Color+Extension.swift
  65. 14 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Localized+Extensions.swift
  66. 21 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/NSDataAsset+Extensions.swift
  67. 42 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/String+Extensions.swift
  68. 15 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIFont+Extension.swift
  69. 24 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIImage+HxExt.swift
  70. 8 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIImageView+Extensions.swift
  71. 67 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIView+Extensions.swift
  72. 298 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Http/MeetingHttpManager.swift
  73. 169 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Http/P2PHttpManager.swift
  74. 121 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/ListenerManager.swift
  75. 26 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/Base/IMBaseListener.swift
  76. 51 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/MeetingMessageListener.swift
  77. 150 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/MeetingRoomListener.swift
  78. 126 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/P2PMessageListener.swift
  79. 14 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitConfiguration.swift
  80. 44 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitDarwinNotificationCenter.swift
  81. 47 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitEnumerator.swift
  82. 567 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitManager.swift
  83. 59 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/HXMessageAction.swift
  84. 105 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/MessageHandler+Meeting.swift
  85. 122 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/MessageHandler+P2P.swift
  86. 52 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/MessageHandler.swift
  87. 226 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Music/RTCCallMusicManager.swift
  88. 73 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Music/RTCClientMuteChecker.swift
  89. 115 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Notification/RTCNotificationManager.swift
  90. 104 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Others/PermissionManager.swift
  91. 37 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Others/SkipManager.swift
  92. 189 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Share/BroadcastServerSocketConnection.swift
  93. 142 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Share/ShareScreenManager.swift
  94. 222 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Share/SocketConnectionFrameReader.swift
  95. 51 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/AppConfigure.swift
  96. 57 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/CreateMeetingModel.swift
  97. 25 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/ICEServer.swift
  98. 93 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/ImageMessageElem.swift
  99. 67 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/MeetingInfoModel.swift
  100. 87 0
      Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/UserAccount.swift

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+*.xcuserstate
+*.xcbkptlist
+xcschememanagement.plist
+xcshareddata/
+xcuserdata/
+.DS_Store
+#CocoaPods
+Pods
+!Podfile
+!Podfile.lock
+hoxin.xcworkspace/xcuserdata/tancheng.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+.DS_Store

+ 36 - 0
Frameworks/DesignKit/DesignKit.podspec

@@ -0,0 +1,36 @@
+# Start from https://github.com/CocoaPods/pod-template/blob/master/NAME.podspec
+#
+# Be sure to run `pod lib lint ${POD_NAME}.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+  s.name             = 'DesignKit'
+  s.version          = '1.0.0'
+  s.summary          = 'Design components'
+
+# This description is used to generate tags and improve search results.
+#   * Think: What does it do? Why did you write it? What is the focus?
+#   * Try to keep it short, snappy and to the point.
+#   * Write the description between the DESC delimiters below.
+#   * Finally, don't worry about the indent, CocoaPods strips it!
+
+  s.description      = <<-DESC
+Contains the decomponents for Design System.
+                       DESC
+
+  s.homepage         = 'https://github.com/JakeLin/moments-ios'
+  s.license          = 'MIT'
+  s.author           = 'MIT'
+  s.source           = { :path => '.' }
+
+  s.ios.deployment_target = '10.0'
+  s.swift_versions = '5.3'
+
+  s.source_files = 'src/**/*'
+  # s.resources = 'assets/**/*'
+
+end

+ 21 - 0
Frameworks/DesignKit/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Jake Lin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 15 - 0
Frameworks/DesignKit/src/Avatar/UIImageViewExtensions.swift

@@ -0,0 +1,15 @@
+//
+//  Avatar.swift
+//  DesignKit
+//
+//  Created by Jake Lin on 20/10/20.
+//
+
+import UIKit
+
+public extension UIImageView {
+    func asAvatar(cornerRadius: CGFloat = 4) {
+        clipsToBounds = true
+        layer.cornerRadius = cornerRadius
+    }
+}

+ 59 - 0
Frameworks/DesignKit/src/Color/UIColorExtensions.swift

@@ -0,0 +1,59 @@
+//
+//  UIColor+Palette.swift
+//  DesignKit
+//
+//  Created by Jake Lin on 20/10/20.
+//
+// The colors are picked up from https://backpack.github.io/guidelines/colors
+
+import UIKit
+
+public extension UIColor {
+    static let designKit = DesignKitPalette.self
+
+    enum DesignKitPalette {
+        /// Light: 0770e3  Dark: 109, 159, 235
+        public static let primary: UIColor = dynamicColor(light: UIColor(hex: 0x0770e3), dark: UIColor(hex: 0x6d9feb))
+        public static let background: UIColor = dynamicColor(light: .white, dark: .black)
+        /// Light: 241, 242, 248  Dark: 29, 27, 32
+        public static let secondaryBackground: UIColor = dynamicColor(light: UIColor(hex: 0xf1f2f8), dark: UIColor(hex: 0x1D1B20))
+        ///Light: white Dark:44, 44, 46
+        public static let tertiaryBackground: UIColor = dynamicColor(light: .white, dark: UIColor(hex: 0x2C2C2E))
+        /// Light: 239, 239, 239  Dark: 29, 29, 29
+        public static let line: UIColor = dynamicColor(light: UIColor(hex: 0xEFEFEF), dark: UIColor(hex: 0x1D1D1D))
+        /// Light: 17, 18, 54 Dark: white
+        public static let primaryText: UIColor = dynamicColor(light: UIColor(hex: 0x111236), dark: .white)
+        /// Light: 104, 105, 127 Dark: 142, 142, 147
+        public static let secondaryText: UIColor = dynamicColor(light: UIColor(hex: 0x68697f), dark: UIColor(hex: 0x8E8E93))
+        /// Light: 143, 144, 160 Dark: 142, 142, 147
+        public static let tertiaryText: UIColor = dynamicColor(light: UIColor(hex: 0x8f90a0), dark: UIColor(hex: 0x8E8E93))
+        /// Light: 178, 178, 191 Dark: 142, 142, 147
+        public static let quaternaryText: UIColor = dynamicColor(light: UIColor(hex: 0xb2b2bf), dark: UIColor(hex: 0x8E8E93))
+        /// Light: 255, 255, 255 Dark: 44, 44, 44
+        public static let messageOtherBackground: UIColor = dynamicColor(light: UIColor(hex: 0xFFFFFF), dark: UIColor(hex: 0x2C2C2C))
+        /// Light: 175,175,175 Dark: 150,150,150
+        public static let placeholderBackground: UIColor = dynamicColor(light: UIColor(hex: 0xAFAFAF), dark: UIColor(hex: 0x969696))
+        /// Light: 26, 26, 26 Dark: 255, 255
+        public static let messageOtherText: UIColor = dynamicColor(light: UIColor(hex: 0x1A1A1A), dark: UIColor(hex: 0xFFFFFF))
+        /// Light: 160, 234, 111  Dark: 160, 234, 111
+        public static let messageOwnBackground: UIColor = dynamicColor(light: UIColor(hex: 0xA0EA6F), dark: UIColor(hex: 0xA0EA6F))
+        /// Light: 26, 26, 26  Dark: 26, 26, 26
+        public static let messageOwnText: UIColor = dynamicColor(light: UIColor(hex: 0x1A1A1A), dark: UIColor(hex: 0x1A1A1A))
+        /// Light: 79,79,79  Dark: 195,195,159
+        public static let iconColor: UIColor = dynamicColor(light: UIColor(hex: 0x4F4F4F), dark: UIColor(hex: 0xC3C3C3))
+    }
+    static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
+        return UIColor { $0.userInterfaceStyle == .dark ? dark : light }
+    }
+}
+
+public extension UIColor {
+    convenience init(hex: Int) {
+        let components = (
+                R: CGFloat((hex >> 16) & 0xff) / 255,
+                G: CGFloat((hex >> 08) & 0xff) / 255,
+                B: CGFloat((hex >> 00) & 0xff) / 255
+        )
+        self.init(red: components.R, green: components.G, blue: components.B, alpha: 1)
+    }
+}

+ 42 - 0
Frameworks/DesignKit/src/FavoriteButton/UIButtonExtensions.swift

@@ -0,0 +1,42 @@
+//
+//  UIButtonExtensions.swift
+//  DesignKit
+//
+//  Created by Jake Lin on 2/11/20.
+//
+
+import Foundation
+import UIKit
+
+public extension UIButton {
+//    func asStarFavoriteButton(pointSize: CGFloat = 18, weight: UIImage.SymbolWeight = .semibold, scale: UIImage.SymbolScale = .default, fillColor: UIColor = UIColor(hex: 0xf1c40f)) {
+//        let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: pointSize, weight: weight, scale: scale)
+//        let starImage = UIImage(systemName: "star", withConfiguration: symbolConfiguration)
+//        setImage(starImage, for: .normal)
+//
+//        let starFillImage = UIImage(systemName: "star.fill", withConfiguration: symbolConfiguration)
+//        setImage(starFillImage, for: .selected)
+//
+//        tintColor = fillColor
+//        addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
+//    }
+//
+//    func asHeartFavoriteButton(pointSize: CGFloat = 18, weight: UIImage.SymbolWeight = .semibold, scale: UIImage.SymbolScale = .default, fillColor: UIColor = UIColor(hex: 0xe74c3c)) {
+//        let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: pointSize, weight: weight, scale: scale)
+//        let heartImage = UIImage(systemName: "heart", withConfiguration: symbolConfiguration)
+//        setImage(heartImage, for: .normal)
+//
+//        let heartFillImage = UIImage(systemName: "heart.fill", withConfiguration: symbolConfiguration)
+//        setImage(heartFillImage, for: .selected)
+//
+//        tintColor = fillColor
+//        addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
+//    }
+}
+
+private extension UIButton {
+    @objc
+    private func touchUpInside(sender: UIButton) {
+        isSelected = !isSelected
+    }
+}

+ 83 - 0
Frameworks/DesignKit/src/Font/UIFontExtensions.swift

@@ -0,0 +1,83 @@
+//
+//  UIFont+Typography.swift
+//  DesignKit
+//
+//  Created by Jake Lin on 19/10/20.
+//
+
+import UIKit
+import SwiftUI
+
+public extension UIFont {
+    static let designKit = DesignKitTypography()
+
+    struct DesignKitTypography {
+        public var display1: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 42, weight: .semibold), forTextStyle: .largeTitle, maximumPointSize: 49)
+        }
+
+        public var display2: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 36, weight: .semibold), forTextStyle: .largeTitle, maximumPointSize: 42)
+        }
+
+        public var title1: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 24, weight: .semibold), forTextStyle: .title1, maximumPointSize: 29)
+        }
+
+        public var title2: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 20, weight: .semibold), forTextStyle: .title2, maximumPointSize: 24)
+        }
+
+        public var title3: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 18, weight: .semibold), forTextStyle: .title3, maximumPointSize: 23)
+        }
+
+        public var title4: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 14, weight: .regular), forTextStyle: .headline, maximumPointSize: 18)
+        }
+
+        public var title5: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 12, weight: .regular), forTextStyle: .subheadline, maximumPointSize: 16)
+        }
+
+        public var bodyBold: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 16, weight: .semibold), forTextStyle: .body, maximumPointSize: 21)
+        }
+
+        public var body: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 16, weight: .regular), forTextStyle: .body, maximumPointSize: 21)
+        }
+
+        public var captionBold: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 14, weight: .semibold), forTextStyle: .caption1, maximumPointSize: 20)
+        }
+
+        public var caption: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 14, weight: .light), forTextStyle: .caption1, maximumPointSize: 20)
+        }
+
+        public var small: UIFont {
+            scaled(baseFont: .systemFont(ofSize: 12, weight: .light), forTextStyle: .footnote, maximumPointSize: 16)
+        }
+    }
+    static func font(ofSize fontSize: CGFloat, weight: UIFont.Weight = .regular) -> UIFont {
+        return .systemFont(ofSize: fontSize, weight: weight)
+    }
+}
+
+public extension Font {
+    static func font(ofSize fontSize: CGFloat, weight: UIFont.Weight = .regular) -> Font {
+        return Font(UIFont.systemFont(ofSize: fontSize, weight: weight))
+    }
+}
+
+private extension UIFont.DesignKitTypography {
+    func scaled(baseFont: UIFont, forTextStyle textStyle: UIFont.TextStyle = .body, maximumPointSize: CGFloat? = nil) -> UIFont {
+        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
+
+        if let maximumPointSize = maximumPointSize {
+            return fontMetrics.scaledFont(for: baseFont, maximumPointSize: maximumPointSize)
+        }
+        return fontMetrics.scaledFont(for: baseFont)
+    }
+}

+ 17 - 0
Frameworks/DesignKit/src/Spacing/Spacing.swift

@@ -0,0 +1,17 @@
+//
+//  Spacing.swift
+//  DesignKit
+//
+//  Created by Jake Lin on 24/10/20.
+//
+
+public struct Spacing {
+    public static let twoExtraSmall: CGFloat = 4
+    public static let extraSmall: CGFloat = 8
+    public static let small: CGFloat = 12
+    public static let medium: CGFloat = 18
+    public static let large: CGFloat = 24
+    public static let extraLarge: CGFloat = 32
+    public static let twoExtraLarge: CGFloat = 40
+    public static let threeExtraLarge: CGFloat = 48
+}

+ 177 - 0
Frameworks/FileKit/.gitignore

@@ -0,0 +1,177 @@
+
+# Created by https://www.gitignore.io/api/xcode,appcode,swift,osx
+
+### Xcode ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+
+### AppCode ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+
+# Sensitive or high-churn files:
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/dataSources.local.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### AppCode Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+
+
+### Swift ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xcuserstate
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+
+
+### OSX ###
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon

+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+

+ 1 - 0
Frameworks/FileKit/.swift-version

@@ -0,0 +1 @@
+5.0

+ 19 - 0
Frameworks/FileKit/.swiftlint.yml

@@ -0,0 +1,19 @@
+included:
+    - Sources
+
+# rule identifiers to exclude from running
+disabled_rules:
+    - cyclomatic_complexity
+    - variable_name
+    - type_name
+    - todo
+
+shorthand_operator: warning
+superfluous_disable_command: warning
+
+# some rules are only opt-in
+opt_in_rules:
+    - empty_count
+
+# configurable rules can be customized from this configuration file
+line_length: 200

+ 32 - 0
Frameworks/FileKit/.travis.yml

@@ -0,0 +1,32 @@
+language: objective-c
+osx_image: xcode10.2
+env:
+  global:
+    - LC_CTYPE=en_US.UTF-8
+    - LANG=en_US.UTF-8
+    - PROJECT=FileKit.xcodeproj
+    - IOS_FRAMEWORK_SCHEME="FileKit-iOS"
+    - MACOS_FRAMEWORK_SCHEME="FileKit-OSX"
+    - IOS_SDK=iphonesimulator12.2
+    - MACOS_SDK=macosx10.14
+  matrix:
+    - DESTINATION="OS=12.2,name=iPhone 8" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO"
+    - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="YES"
+before_install:
+  - gem install xcpretty --no-document --quiet
+script:
+  - set -o pipefail
+  - xcodebuild -version
+  - xcodebuild -showsdks
+
+  - if [ $RUN_TESTS == "YES" ]; then
+      xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
+    else
+      xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty;
+    fi
+after_success:
+  - bash <(curl -s https://codecov.io/bash)
+
+branches:
+  only:
+    - master

+ 17 - 0
Frameworks/FileKit/FileKit.playground/Contents.swift

@@ -0,0 +1,17 @@
+/*:
+# FileKit
+Use this playground to try out FileKit
+*/
+import Cocoa
+import FileKit
+import XCPlayground
+import PlaygroundSupport
+
+extension Path {
+    static let SharedPlaygroundData = Path(url: playgroundSharedDataDirectory)!
+}
+
+let shared = Path.SharedPlaygroundData
+let sample = TextFile(path: shared/"filekit_sample.txt")
+try? sample.write("Hello there!")
+try? sample.read()

+ 4 - 0
Frameworks/FileKit/FileKit.playground/contents.xcplayground

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<playground version='5.0' target-platform='osx' display-mode='rendered'>
+    <timeline fileName='timeline.xctimeline'/>
+</playground>

+ 14 - 0
Frameworks/FileKit/FileKit.podspec

@@ -0,0 +1,14 @@
+Pod::Spec.new do |s|
+    s.name                      = "FileKit"
+    s.version                   = "2022.10.17.4"
+    s.summary                   = "Simple and expressive file management in Swift."
+    s.homepage                  = "https://github.com/nvzqz/FileKit"
+    s.license                   = { :type => "MIT", :file => "LICENSE.md" }
+    s.author                    = "Nikolai Vazquez"
+    s.ios.deployment_target     = "11.0"
+    s.source                    = { :git => '', :tag => s.version }
+    s.source_files              = "Sources/*.swift"
+end
+
+
+

+ 1184 - 0
Frameworks/FileKit/FileKit.xcodeproj/project.pbxproj

@@ -0,0 +1,1184 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		17217D561CDCD13700723D11 /* RelativePathType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D551CDCD13700723D11 /* RelativePathType.swift */; };
+		17217D571CDCD1BD00723D11 /* RelativePathType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D551CDCD13700723D11 /* RelativePathType.swift */; };
+		17217D581CDCD1C300723D11 /* RelativePathType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D551CDCD13700723D11 /* RelativePathType.swift */; };
+		17217D591CDCD1C900723D11 /* RelativePathType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D551CDCD13700723D11 /* RelativePathType.swift */; };
+		17217D5E1CDCD74000723D11 /* DispatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5C1CDCD74000723D11 /* DispatchEvent.swift */; };
+		17217D5F1CDCD74000723D11 /* DispatchWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5D1CDCD74000723D11 /* DispatchWatcher.swift */; };
+		17217D601CDCD79C00723D11 /* DispatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5C1CDCD74000723D11 /* DispatchEvent.swift */; };
+		17217D611CDCD79C00723D11 /* DispatchWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5D1CDCD74000723D11 /* DispatchWatcher.swift */; };
+		17217D621CDCD90B00723D11 /* DispatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5C1CDCD74000723D11 /* DispatchEvent.swift */; };
+		17217D631CDCD90B00723D11 /* DispatchWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5D1CDCD74000723D11 /* DispatchWatcher.swift */; };
+		17217D641CDCD91200723D11 /* DispatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5C1CDCD74000723D11 /* DispatchEvent.swift */; };
+		17217D651CDCD91200723D11 /* DispatchWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17217D5D1CDCD74000723D11 /* DispatchWatcher.swift */; };
+		520E0E301C110CA900BAEA99 /* Process+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520E0E2F1C110CA900BAEA99 /* Process+FileKit.swift */; };
+		520E0E311C110CA900BAEA99 /* Process+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520E0E2F1C110CA900BAEA99 /* Process+FileKit.swift */; };
+		520E0E321C110CA900BAEA99 /* Process+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520E0E2F1C110CA900BAEA99 /* Process+FileKit.swift */; };
+		520E0E331C110CA900BAEA99 /* Process+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520E0E2F1C110CA900BAEA99 /* Process+FileKit.swift */; };
+		52255DCB1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52255DCA1C042D6E00AC1DA5 /* FilePermissions.swift */; };
+		52255DCC1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52255DCA1C042D6E00AC1DA5 /* FilePermissions.swift */; };
+		52255DCD1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52255DCA1C042D6E00AC1DA5 /* FilePermissions.swift */; };
+		52255DCE1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52255DCA1C042D6E00AC1DA5 /* FilePermissions.swift */; };
+		523C33781B9A7EFC00AB70E4 /* TextFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33771B9A7EFC00AB70E4 /* TextFile.swift */; };
+		523C33791B9A7EFC00AB70E4 /* TextFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33771B9A7EFC00AB70E4 /* TextFile.swift */; };
+		523C337B1B9B68D600AB70E4 /* DictionaryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C337A1B9B68D600AB70E4 /* DictionaryFile.swift */; };
+		523C337C1B9B68D600AB70E4 /* DictionaryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C337A1B9B68D600AB70E4 /* DictionaryFile.swift */; };
+		523C33991B9B764600AB70E4 /* DataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33981B9B764600AB70E4 /* DataType.swift */; };
+		523C339A1B9B764600AB70E4 /* DataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33981B9B764600AB70E4 /* DataType.swift */; };
+		523C33A11B9B772A00AB70E4 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A01B9B772A00AB70E4 /* File.swift */; };
+		523C33A21B9B772A00AB70E4 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A01B9B772A00AB70E4 /* File.swift */; };
+		523C33A71B9B894A00AB70E4 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A61B9B894A00AB70E4 /* Operators.swift */; };
+		523C33A81B9B894A00AB70E4 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A61B9B894A00AB70E4 /* Operators.swift */; };
+		524D31571BC79067008B93D0 /* ArrayFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D31561BC79067008B93D0 /* ArrayFile.swift */; };
+		524D31581BC79067008B93D0 /* ArrayFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D31561BC79067008B93D0 /* ArrayFile.swift */; };
+		524D31591BC79067008B93D0 /* ArrayFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D31561BC79067008B93D0 /* ArrayFile.swift */; };
+		524D315B1BC79331008B93D0 /* NSDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315A1BC79331008B93D0 /* NSDataFile.swift */; };
+		524D315C1BC79331008B93D0 /* NSDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315A1BC79331008B93D0 /* NSDataFile.swift */; };
+		524D315D1BC79331008B93D0 /* NSDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315A1BC79331008B93D0 /* NSDataFile.swift */; };
+		524D315F1BC7A02A008B93D0 /* Image+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315E1BC7A02A008B93D0 /* Image+FileKit.swift */; };
+		524D31601BC7A02A008B93D0 /* Image+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315E1BC7A02A008B93D0 /* Image+FileKit.swift */; };
+		524D31611BC7A02A008B93D0 /* Image+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315E1BC7A02A008B93D0 /* Image+FileKit.swift */; };
+		5263A8FC1B96B94D00635A93 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7C31B968E8000AA473F /* Path.swift */; };
+		5263A8FD1B96B94D00635A93 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7C31B968E8000AA473F /* Path.swift */; };
+		5263A8FE1B96B95000635A93 /* String+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7E51B96ADA100AA473F /* String+FileKit.swift */; };
+		5263A8FF1B96B95100635A93 /* String+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7E51B96ADA100AA473F /* String+FileKit.swift */; };
+		5263A9071B96BA3D00635A93 /* FileKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263A9061B96BA3D00635A93 /* FileKitTests.swift */; };
+		5263A9091B96BA3D00635A93 /* FileKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5204B84B1B96B83800AA473F /* FileKit.framework */; };
+		5276124B1BAEA43600503D0A /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A01B9B772A00AB70E4 /* File.swift */; };
+		5276124C1BAEA43600503D0A /* TextFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33771B9A7EFC00AB70E4 /* TextFile.swift */; };
+		5276124D1BAEA43600503D0A /* DictionaryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C337A1B9B68D600AB70E4 /* DictionaryFile.swift */; };
+		5276124E1BAEA43600503D0A /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7C31B968E8000AA473F /* Path.swift */; };
+		5276124F1BAEA43600503D0A /* DataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33981B9B764600AB70E4 /* DataType.swift */; };
+		527612501BAEA43600503D0A /* FileKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BF6BB01B99322000F07E13 /* FileKitError.swift */; };
+		527612511BAEA43600503D0A /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A61B9B894A00AB70E4 /* Operators.swift */; };
+		527612521BAEA45D00503D0A /* String+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7E51B96ADA100AA473F /* String+FileKit.swift */; };
+		52A016C41C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016C31C013F3D0045A9C8 /* DirectoryEnumerator.swift */; };
+		52A016C51C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016C31C013F3D0045A9C8 /* DirectoryEnumerator.swift */; };
+		52A016C61C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016C31C013F3D0045A9C8 /* DirectoryEnumerator.swift */; };
+		52A016C71C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016C31C013F3D0045A9C8 /* DirectoryEnumerator.swift */; };
+		52A016E91C01565C0045A9C8 /* FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016E81C01565C0045A9C8 /* FileKit.swift */; };
+		52A016EA1C01565C0045A9C8 /* FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016E81C01565C0045A9C8 /* FileKit.swift */; };
+		52A016EB1C01565C0045A9C8 /* FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016E81C01565C0045A9C8 /* FileKit.swift */; };
+		52A016EC1C01565C0045A9C8 /* FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016E81C01565C0045A9C8 /* FileKit.swift */; };
+		52A016F21C0184CA0045A9C8 /* FileProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F11C0184CA0045A9C8 /* FileProtection.swift */; };
+		52A016F31C0184CA0045A9C8 /* FileProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F11C0184CA0045A9C8 /* FileProtection.swift */; };
+		52A016F41C0184CA0045A9C8 /* FileProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F11C0184CA0045A9C8 /* FileProtection.swift */; };
+		52A016F61C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F51C01ACE50045A9C8 /* NSDictionary+FileKit.swift */; };
+		52A016F71C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F51C01ACE50045A9C8 /* NSDictionary+FileKit.swift */; };
+		52A016F81C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F51C01ACE50045A9C8 /* NSDictionary+FileKit.swift */; };
+		52A016F91C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016F51C01ACE50045A9C8 /* NSDictionary+FileKit.swift */; };
+		52A016FB1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FA1C01AD1E0045A9C8 /* NSArray+FileKit.swift */; };
+		52A016FC1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FA1C01AD1E0045A9C8 /* NSArray+FileKit.swift */; };
+		52A016FD1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FA1C01AD1E0045A9C8 /* NSArray+FileKit.swift */; };
+		52A016FE1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FA1C01AD1E0045A9C8 /* NSArray+FileKit.swift */; };
+		52A017001C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FF1C01AD4A0045A9C8 /* NSData+FileKit.swift */; };
+		52A017011C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FF1C01AD4A0045A9C8 /* NSData+FileKit.swift */; };
+		52A017021C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FF1C01AD4A0045A9C8 /* NSData+FileKit.swift */; };
+		52A017031C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A016FF1C01AD4A0045A9C8 /* NSData+FileKit.swift */; };
+		52A017051C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017041C01AD6F0045A9C8 /* Bundle+FileKit.swift */; };
+		52A017061C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017041C01AD6F0045A9C8 /* Bundle+FileKit.swift */; };
+		52A017071C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017041C01AD6F0045A9C8 /* Bundle+FileKit.swift */; };
+		52A017081C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017041C01AD6F0045A9C8 /* Bundle+FileKit.swift */; };
+		52A0170A1C01D5400045A9C8 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017091C01D5400045A9C8 /* FileType.swift */; };
+		52A0170B1C01D5400045A9C8 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017091C01D5400045A9C8 /* FileType.swift */; };
+		52A0170C1C01D5400045A9C8 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017091C01D5400045A9C8 /* FileType.swift */; };
+		52A0170D1C01D5400045A9C8 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A017091C01D5400045A9C8 /* FileType.swift */; };
+		52A017101C024D840045A9C8 /* NSString+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A0170F1C024D840045A9C8 /* NSString+FileKit.swift */; };
+		52A017111C024D840045A9C8 /* NSString+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A0170F1C024D840045A9C8 /* NSString+FileKit.swift */; };
+		52A017121C024D840045A9C8 /* NSString+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A0170F1C024D840045A9C8 /* NSString+FileKit.swift */; };
+		52A017131C024D840045A9C8 /* NSString+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A0170F1C024D840045A9C8 /* NSString+FileKit.swift */; };
+		52B938BC1BF3C42A001B7AEB /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A01B9B772A00AB70E4 /* File.swift */; };
+		52B938BD1BF3C42A001B7AEB /* TextFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33771B9A7EFC00AB70E4 /* TextFile.swift */; };
+		52B938BE1BF3C42A001B7AEB /* DictionaryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C337A1B9B68D600AB70E4 /* DictionaryFile.swift */; };
+		52B938BF1BF3C42A001B7AEB /* ArrayFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D31561BC79067008B93D0 /* ArrayFile.swift */; };
+		52B938C01BF3C42A001B7AEB /* NSDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315A1BC79331008B93D0 /* NSDataFile.swift */; };
+		52B938C11BF3C42A001B7AEB /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7C31B968E8000AA473F /* Path.swift */; };
+		52B938C21BF3C42A001B7AEB /* DataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33981B9B764600AB70E4 /* DataType.swift */; };
+		52B938C31BF3C42A001B7AEB /* FileKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BF6BB01B99322000F07E13 /* FileKitError.swift */; };
+		52B938C41BF3C42A001B7AEB /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523C33A61B9B894A00AB70E4 /* Operators.swift */; };
+		52B938C51BF3C430001B7AEB /* String+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5204B7E51B96ADA100AA473F /* String+FileKit.swift */; };
+		52B938C71BF3C430001B7AEB /* Image+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524D315E1BC7A02A008B93D0 /* Image+FileKit.swift */; };
+		52B938C91BF402ED001B7AEB /* ImageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B938C81BF402ED001B7AEB /* ImageFile.swift */; };
+		52B938CA1BF402ED001B7AEB /* ImageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B938C81BF402ED001B7AEB /* ImageFile.swift */; };
+		52B938CB1BF402ED001B7AEB /* ImageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B938C81BF402ED001B7AEB /* ImageFile.swift */; };
+		52B938CC1BF402ED001B7AEB /* ImageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B938C81BF402ED001B7AEB /* ImageFile.swift */; };
+		52BF6BB11B99322000F07E13 /* FileKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BF6BB01B99322000F07E13 /* FileKitError.swift */; };
+		52BF6BB21B99322000F07E13 /* FileKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BF6BB01B99322000F07E13 /* FileKitError.swift */; };
+		82F4F2BD1CA95480002C8393 /* FileSystemWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F4F2BC1CA95480002C8393 /* FileSystemWatcher.swift */; };
+		82F4F2C01CA956F2002C8393 /* FileSystemEventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F4F2BF1CA956F2002C8393 /* FileSystemEventStream.swift */; };
+		C411910C1F804C8300978BA4 /* JSONType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41191091F804A6000978BA4 /* JSONType.swift */; };
+		C411910D1F804C8300978BA4 /* JSONType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41191091F804A6000978BA4 /* JSONType.swift */; };
+		C411910E1F804C8300978BA4 /* JSONType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41191091F804A6000978BA4 /* JSONType.swift */; };
+		C411910F1F804C8400978BA4 /* JSONType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41191091F804A6000978BA4 /* JSONType.swift */; };
+		C41191101F804C8600978BA4 /* PropertyListType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411910A1F804A6000978BA4 /* PropertyListType.swift */; };
+		C41191111F804C8600978BA4 /* PropertyListType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411910A1F804A6000978BA4 /* PropertyListType.swift */; };
+		C41191131F804C8700978BA4 /* PropertyListType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411910A1F804A6000978BA4 /* PropertyListType.swift */; };
+		C41191141F804C8800978BA4 /* PropertyListType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411910A1F804A6000978BA4 /* PropertyListType.swift */; };
+		C41FC73A1D9BDCB400C3A0F1 /* Data+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7391D9BDCB400C3A0F1 /* Data+FileKit.swift */; };
+		C41FC73B1D9BDCD100C3A0F1 /* Data+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7391D9BDCB400C3A0F1 /* Data+FileKit.swift */; };
+		C41FC73C1D9BDCD100C3A0F1 /* Data+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7391D9BDCB400C3A0F1 /* Data+FileKit.swift */; };
+		C41FC73D1D9BDCD200C3A0F1 /* Data+FileKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7391D9BDCB400C3A0F1 /* Data+FileKit.swift */; };
+		C41FC73F1D9BE44A00C3A0F1 /* Dictionary+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC73E1D9BE44A00C3A0F1 /* Dictionary+File.swift */; };
+		C41FC7411D9BE4DB00C3A0F1 /* DataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7401D9BE4DB00C3A0F1 /* DataFile.swift */; };
+		C41FC7421D9BEC7700C3A0F1 /* DataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7401D9BE4DB00C3A0F1 /* DataFile.swift */; };
+		C41FC7431D9BEC7800C3A0F1 /* DataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7401D9BE4DB00C3A0F1 /* DataFile.swift */; };
+		C41FC7441D9BEC7800C3A0F1 /* DataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7401D9BE4DB00C3A0F1 /* DataFile.swift */; };
+		C41FC7461D9BEEC000C3A0F1 /* Array+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7451D9BEEC000C3A0F1 /* Array+File.swift */; };
+		C41FC7471D9BEEF300C3A0F1 /* Array+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7451D9BEEC000C3A0F1 /* Array+File.swift */; };
+		C41FC7491D9BEEF400C3A0F1 /* Array+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7451D9BEEC000C3A0F1 /* Array+File.swift */; };
+		C41FC74A1D9BEEF400C3A0F1 /* Array+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC7451D9BEEC000C3A0F1 /* Array+File.swift */; };
+		C41FC74B1D9BF05A00C3A0F1 /* Dictionary+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC73E1D9BE44A00C3A0F1 /* Dictionary+File.swift */; };
+		C41FC74C1D9BF05A00C3A0F1 /* Dictionary+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC73E1D9BE44A00C3A0F1 /* Dictionary+File.swift */; };
+		C41FC74D1D9BF05A00C3A0F1 /* Dictionary+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41FC73E1D9BE44A00C3A0F1 /* Dictionary+File.swift */; };
+		C4F7D01F1C08C0DA00EF359B /* FileSystemEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7D01E1C08C0DA00EF359B /* FileSystemEvent.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		5263A90A1B96BA3D00635A93 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 5204B7AD1B968B8600AA473F /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 5204B84A1B96B83800AA473F;
+			remoteInfo = "FileKit-OSX";
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		17217D551CDCD13700723D11 /* RelativePathType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelativePathType.swift; sourceTree = "<group>"; };
+		17217D5C1CDCD74000723D11 /* DispatchEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchEvent.swift; sourceTree = "<group>"; };
+		17217D5D1CDCD74000723D11 /* DispatchWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchWatcher.swift; sourceTree = "<group>"; };
+		5204B7C31B968E8000AA473F /* Path.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = "<group>"; };
+		5204B7E51B96ADA100AA473F /* String+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+FileKit.swift"; sourceTree = "<group>"; };
+		5204B84B1B96B83800AA473F /* FileKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		5204B8581B96B85E00AA473F /* FileKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		520E0E2F1C110CA900BAEA99 /* Process+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Process+FileKit.swift"; sourceTree = "<group>"; };
+		521FC3FB1CDEEC12006C9C3C /* FileKit.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = FileKit.playground; sourceTree = "<group>"; };
+		521FC3FD1CDEFED6006C9C3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		521FC3FE1CDF0144006C9C3C /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
+		52255DCA1C042D6E00AC1DA5 /* FilePermissions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilePermissions.swift; sourceTree = "<group>"; };
+		523C33771B9A7EFC00AB70E4 /* TextFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFile.swift; sourceTree = "<group>"; };
+		523C337A1B9B68D600AB70E4 /* DictionaryFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryFile.swift; sourceTree = "<group>"; };
+		523C33981B9B764600AB70E4 /* DataType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataType.swift; sourceTree = "<group>"; };
+		523C33A01B9B772A00AB70E4 /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
+		523C33A61B9B894A00AB70E4 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = "<group>"; };
+		524D31561BC79067008B93D0 /* ArrayFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayFile.swift; sourceTree = "<group>"; };
+		524D315A1BC79331008B93D0 /* NSDataFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDataFile.swift; sourceTree = "<group>"; };
+		524D315E1BC7A02A008B93D0 /* Image+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+FileKit.swift"; sourceTree = "<group>"; };
+		5263A9041B96BA3D00635A93 /* FileKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FileKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		5263A9061B96BA3D00635A93 /* FileKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileKitTests.swift; sourceTree = "<group>"; };
+		5263A9081B96BA3D00635A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		527612421BAEA3EE00503D0A /* FileKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		527848661E18CADD007E14F0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
+		52A016C31C013F3D0045A9C8 /* DirectoryEnumerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryEnumerator.swift; sourceTree = "<group>"; };
+		52A016E81C01565C0045A9C8 /* FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileKit.swift; sourceTree = "<group>"; };
+		52A016F11C0184CA0045A9C8 /* FileProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileProtection.swift; sourceTree = "<group>"; };
+		52A016F51C01ACE50045A9C8 /* NSDictionary+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSDictionary+FileKit.swift"; sourceTree = "<group>"; };
+		52A016FA1C01AD1E0045A9C8 /* NSArray+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSArray+FileKit.swift"; sourceTree = "<group>"; };
+		52A016FF1C01AD4A0045A9C8 /* NSData+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSData+FileKit.swift"; sourceTree = "<group>"; };
+		52A017041C01AD6F0045A9C8 /* Bundle+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+FileKit.swift"; sourceTree = "<group>"; };
+		52A017091C01D5400045A9C8 /* FileType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
+		52A0170F1C024D840045A9C8 /* NSString+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSString+FileKit.swift"; sourceTree = "<group>"; };
+		52B938B31BF3C3E5001B7AEB /* FileKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		52B938C81BF402ED001B7AEB /* ImageFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFile.swift; sourceTree = "<group>"; };
+		52BF6BB01B99322000F07E13 /* FileKitError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileKitError.swift; sourceTree = "<group>"; };
+		52EF7A981DA58DE00093B983 /* .swift-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".swift-version"; sourceTree = "<group>"; };
+		82F4F2BB1CA94DEC002C8393 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = "<group>"; };
+		82F4F2BC1CA95480002C8393 /* FileSystemWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemWatcher.swift; sourceTree = "<group>"; };
+		82F4F2BF1CA956F2002C8393 /* FileSystemEventStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemEventStream.swift; sourceTree = "<group>"; };
+		C41191091F804A6000978BA4 /* JSONType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONType.swift; sourceTree = "<group>"; };
+		C411910A1F804A6000978BA4 /* PropertyListType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyListType.swift; sourceTree = "<group>"; };
+		C41FC7391D9BDCB400C3A0F1 /* Data+FileKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+FileKit.swift"; sourceTree = "<group>"; };
+		C41FC73E1D9BE44A00C3A0F1 /* Dictionary+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+File.swift"; sourceTree = "<group>"; };
+		C41FC7401D9BE4DB00C3A0F1 /* DataFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataFile.swift; sourceTree = "<group>"; };
+		C41FC7451D9BEEC000C3A0F1 /* Array+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+File.swift"; sourceTree = "<group>"; };
+		C4F7D01E1C08C0DA00EF359B /* FileSystemEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemEvent.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		5204B8471B96B83800AA473F /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5204B8541B96B85E00AA473F /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5263A9011B96BA3D00635A93 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5263A9091B96BA3D00635A93 /* FileKit.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5276123E1BAEA3EE00503D0A /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		52B938AF1BF3C3E5001B7AEB /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		5204B7AC1B968B8600AA473F = {
+			isa = PBXGroup;
+			children = (
+				5204B7B81B968B8600AA473F /* Sources */,
+				521FC3FC1CDEFED6006C9C3C /* Support */,
+				5263A9051B96BA3D00635A93 /* Tests */,
+				5204B7B71B968B8600AA473F /* Products */,
+				521FC3FB1CDEEC12006C9C3C /* FileKit.playground */,
+				521FC3FE1CDF0144006C9C3C /* Package.swift */,
+				52EF7A981DA58DE00093B983 /* .swift-version */,
+				82F4F2BB1CA94DEC002C8393 /* .swiftlint.yml */,
+				527848661E18CADD007E14F0 /* README.md */,
+			);
+			sourceTree = "<group>";
+		};
+		5204B7B71B968B8600AA473F /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				5204B84B1B96B83800AA473F /* FileKit.framework */,
+				5204B8581B96B85E00AA473F /* FileKit.framework */,
+				5263A9041B96BA3D00635A93 /* FileKitTests.xctest */,
+				527612421BAEA3EE00503D0A /* FileKit.framework */,
+				52B938B31BF3C3E5001B7AEB /* FileKit.framework */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		5204B7B81B968B8600AA473F /* Sources */ = {
+			isa = PBXGroup;
+			children = (
+				52A016E81C01565C0045A9C8 /* FileKit.swift */,
+				5204B7C31B968E8000AA473F /* Path.swift */,
+				17217D551CDCD13700723D11 /* RelativePathType.swift */,
+				52A016F11C0184CA0045A9C8 /* FileProtection.swift */,
+				52A017091C01D5400045A9C8 /* FileType.swift */,
+				52255DCA1C042D6E00AC1DA5 /* FilePermissions.swift */,
+				52A016C31C013F3D0045A9C8 /* DirectoryEnumerator.swift */,
+				523C33981B9B764600AB70E4 /* DataType.swift */,
+				C411910B1F804A9000978BA4 /* Codable Types */,
+				52F3AFA71CDFB48600C2BBBD /* File Types */,
+				52BF6BB01B99322000F07E13 /* FileKitError.swift */,
+				523C33A61B9B894A00AB70E4 /* Operators.swift */,
+				52F3AFA61CDFB46300C2BBBD /* Watching */,
+				52F3AFA51CDFB45100C2BBBD /* Extensions */,
+			);
+			path = Sources;
+			sourceTree = "<group>";
+		};
+		521FC3FC1CDEFED6006C9C3C /* Support */ = {
+			isa = PBXGroup;
+			children = (
+				521FC3FD1CDEFED6006C9C3C /* Info.plist */,
+			);
+			path = Support;
+			sourceTree = "<group>";
+		};
+		5263A9051B96BA3D00635A93 /* Tests */ = {
+			isa = PBXGroup;
+			children = (
+				5263A9061B96BA3D00635A93 /* FileKitTests.swift */,
+				5263A9081B96BA3D00635A93 /* Info.plist */,
+			);
+			path = Tests;
+			sourceTree = "<group>";
+		};
+		52F3AFA51CDFB45100C2BBBD /* Extensions */ = {
+			isa = PBXGroup;
+			children = (
+				520E0E2F1C110CA900BAEA99 /* Process+FileKit.swift */,
+				52A0170F1C024D840045A9C8 /* NSString+FileKit.swift */,
+				52A016F51C01ACE50045A9C8 /* NSDictionary+FileKit.swift */,
+				52A016FA1C01AD1E0045A9C8 /* NSArray+FileKit.swift */,
+				52A016FF1C01AD4A0045A9C8 /* NSData+FileKit.swift */,
+				52A017041C01AD6F0045A9C8 /* Bundle+FileKit.swift */,
+				5204B7E51B96ADA100AA473F /* String+FileKit.swift */,
+				C41FC7391D9BDCB400C3A0F1 /* Data+FileKit.swift */,
+				524D315E1BC7A02A008B93D0 /* Image+FileKit.swift */,
+				C41FC73E1D9BE44A00C3A0F1 /* Dictionary+File.swift */,
+				C41FC7451D9BEEC000C3A0F1 /* Array+File.swift */,
+			);
+			name = Extensions;
+			sourceTree = "<group>";
+		};
+		52F3AFA61CDFB46300C2BBBD /* Watching */ = {
+			isa = PBXGroup;
+			children = (
+				C4F7D01E1C08C0DA00EF359B /* FileSystemEvent.swift */,
+				82F4F2BF1CA956F2002C8393 /* FileSystemEventStream.swift */,
+				82F4F2BC1CA95480002C8393 /* FileSystemWatcher.swift */,
+				17217D5C1CDCD74000723D11 /* DispatchEvent.swift */,
+				17217D5D1CDCD74000723D11 /* DispatchWatcher.swift */,
+			);
+			name = Watching;
+			sourceTree = "<group>";
+		};
+		52F3AFA71CDFB48600C2BBBD /* File Types */ = {
+			isa = PBXGroup;
+			children = (
+				523C33A01B9B772A00AB70E4 /* File.swift */,
+				523C33771B9A7EFC00AB70E4 /* TextFile.swift */,
+				C41FC7401D9BE4DB00C3A0F1 /* DataFile.swift */,
+				524D315A1BC79331008B93D0 /* NSDataFile.swift */,
+				523C337A1B9B68D600AB70E4 /* DictionaryFile.swift */,
+				524D31561BC79067008B93D0 /* ArrayFile.swift */,
+				52B938C81BF402ED001B7AEB /* ImageFile.swift */,
+			);
+			name = "File Types";
+			sourceTree = "<group>";
+		};
+		C411910B1F804A9000978BA4 /* Codable Types */ = {
+			isa = PBXGroup;
+			children = (
+				C41191091F804A6000978BA4 /* JSONType.swift */,
+				C411910A1F804A6000978BA4 /* PropertyListType.swift */,
+			);
+			name = "Codable Types";
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+		5204B8481B96B83800AA473F /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5204B8551B96B85E00AA473F /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5276123F1BAEA3EE00503D0A /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		52B938B01BF3C3E5001B7AEB /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+		5204B84A1B96B83800AA473F /* FileKit-OSX */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 5204B8501B96B83800AA473F /* Build configuration list for PBXNativeTarget "FileKit-OSX" */;
+			buildPhases = (
+				5204B8461B96B83800AA473F /* Sources */,
+				5204B8471B96B83800AA473F /* Frameworks */,
+				5204B8481B96B83800AA473F /* Headers */,
+				5204B8491B96B83800AA473F /* Resources */,
+				52995ACA1C0D967100A1AD23 /* SwiftLint */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "FileKit-OSX";
+			productName = "FileKit-OSX";
+			productReference = 5204B84B1B96B83800AA473F /* FileKit.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+		5204B8571B96B85E00AA473F /* FileKit-iOS */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 5204B85D1B96B85E00AA473F /* Build configuration list for PBXNativeTarget "FileKit-iOS" */;
+			buildPhases = (
+				5204B8531B96B85E00AA473F /* Sources */,
+				5204B8541B96B85E00AA473F /* Frameworks */,
+				5204B8551B96B85E00AA473F /* Headers */,
+				5204B8561B96B85E00AA473F /* Resources */,
+				52995ACB1C0D967700A1AD23 /* SwiftLint */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "FileKit-iOS";
+			productName = "FileKit-iOS";
+			productReference = 5204B8581B96B85E00AA473F /* FileKit.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+		5263A9031B96BA3D00635A93 /* FileKitTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 5263A90C1B96BA3D00635A93 /* Build configuration list for PBXNativeTarget "FileKitTests" */;
+			buildPhases = (
+				5263A9001B96BA3D00635A93 /* Sources */,
+				5263A9011B96BA3D00635A93 /* Frameworks */,
+				5263A9021B96BA3D00635A93 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				5263A90B1B96BA3D00635A93 /* PBXTargetDependency */,
+			);
+			name = FileKitTests;
+			productName = "FileKit-OSX-Tests";
+			productReference = 5263A9041B96BA3D00635A93 /* FileKitTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		527612411BAEA3EE00503D0A /* FileKit-watchOS */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 527612491BAEA3EE00503D0A /* Build configuration list for PBXNativeTarget "FileKit-watchOS" */;
+			buildPhases = (
+				5276123D1BAEA3EE00503D0A /* Sources */,
+				5276123E1BAEA3EE00503D0A /* Frameworks */,
+				5276123F1BAEA3EE00503D0A /* Headers */,
+				527612401BAEA3EE00503D0A /* Resources */,
+				52995ACC1C0D967D00A1AD23 /* SwiftLint */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "FileKit-watchOS";
+			productName = FileKit;
+			productReference = 527612421BAEA3EE00503D0A /* FileKit.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+		52B938B21BF3C3E5001B7AEB /* FileKit-tvOS */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 52B938BA1BF3C3E5001B7AEB /* Build configuration list for PBXNativeTarget "FileKit-tvOS" */;
+			buildPhases = (
+				52B938AE1BF3C3E5001B7AEB /* Sources */,
+				52B938AF1BF3C3E5001B7AEB /* Frameworks */,
+				52B938B01BF3C3E5001B7AEB /* Headers */,
+				52B938B11BF3C3E5001B7AEB /* Resources */,
+				52995ACD1C0D968400A1AD23 /* SwiftLint */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "FileKit-tvOS";
+			productName = "FileKit-tvOS";
+			productReference = 52B938B31BF3C3E5001B7AEB /* FileKit.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		5204B7AD1B968B8600AA473F /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0700;
+				LastUpgradeCheck = 1020;
+				ORGANIZATIONNAME = "Nikolai Vazquez";
+				TargetAttributes = {
+					5204B84A1B96B83800AA473F = {
+						CreatedOnToolsVersion = 7.0;
+						LastSwiftMigration = 1020;
+					};
+					5204B8571B96B85E00AA473F = {
+						CreatedOnToolsVersion = 7.0;
+						LastSwiftMigration = 1000;
+					};
+					5263A9031B96BA3D00635A93 = {
+						CreatedOnToolsVersion = 7.0;
+						LastSwiftMigration = 1020;
+					};
+					527612411BAEA3EE00503D0A = {
+						CreatedOnToolsVersion = 7.0;
+					};
+					52B938B21BF3C3E5001B7AEB = {
+						CreatedOnToolsVersion = 7.1;
+					};
+				};
+			};
+			buildConfigurationList = 5204B7B01B968B8600AA473F /* Build configuration list for PBXProject "FileKit" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 5204B7AC1B968B8600AA473F;
+			productRefGroup = 5204B7B71B968B8600AA473F /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				5204B84A1B96B83800AA473F /* FileKit-OSX */,
+				5204B8571B96B85E00AA473F /* FileKit-iOS */,
+				527612411BAEA3EE00503D0A /* FileKit-watchOS */,
+				52B938B21BF3C3E5001B7AEB /* FileKit-tvOS */,
+				5263A9031B96BA3D00635A93 /* FileKitTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		5204B8491B96B83800AA473F /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5204B8561B96B85E00AA473F /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5263A9021B96BA3D00635A93 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		527612401BAEA3EE00503D0A /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		52B938B11BF3C3E5001B7AEB /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		52995ACA1C0D967100A1AD23 /* SwiftLint */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = SwiftLint;
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "if which swiftlint >/dev/null; then\n    swiftlint\nelse\n    echo \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi";
+		};
+		52995ACB1C0D967700A1AD23 /* SwiftLint */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = SwiftLint;
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "if which swiftlint >/dev/null; then\n    swiftlint\nelse\n    echo \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi";
+		};
+		52995ACC1C0D967D00A1AD23 /* SwiftLint */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = SwiftLint;
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "if which swiftlint >/dev/null; then\n    swiftlint\nelse\n    echo \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi";
+		};
+		52995ACD1C0D968400A1AD23 /* SwiftLint */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = SwiftLint;
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "if which swiftlint >/dev/null; then\n    swiftlint\nelse\n    echo \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi";
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		5204B8461B96B83800AA473F /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5263A8FE1B96B95000635A93 /* String+FileKit.swift in Sources */,
+				52A016E91C01565C0045A9C8 /* FileKit.swift in Sources */,
+				17217D561CDCD13700723D11 /* RelativePathType.swift in Sources */,
+				52A017101C024D840045A9C8 /* NSString+FileKit.swift in Sources */,
+				52A017051C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */,
+				523C33991B9B764600AB70E4 /* DataType.swift in Sources */,
+				82F4F2BD1CA95480002C8393 /* FileSystemWatcher.swift in Sources */,
+				523C33A71B9B894A00AB70E4 /* Operators.swift in Sources */,
+				523C33781B9A7EFC00AB70E4 /* TextFile.swift in Sources */,
+				5263A8FC1B96B94D00635A93 /* Path.swift in Sources */,
+				82F4F2C01CA956F2002C8393 /* FileSystemEventStream.swift in Sources */,
+				52A016C41C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */,
+				524D315F1BC7A02A008B93D0 /* Image+FileKit.swift in Sources */,
+				C411910C1F804C8300978BA4 /* JSONType.swift in Sources */,
+				C41FC73F1D9BE44A00C3A0F1 /* Dictionary+File.swift in Sources */,
+				524D315B1BC79331008B93D0 /* NSDataFile.swift in Sources */,
+				520E0E301C110CA900BAEA99 /* Process+FileKit.swift in Sources */,
+				52A016FB1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */,
+				52BF6BB11B99322000F07E13 /* FileKitError.swift in Sources */,
+				C41FC7411D9BE4DB00C3A0F1 /* DataFile.swift in Sources */,
+				52A017001C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */,
+				52A016F61C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */,
+				C41191131F804C8700978BA4 /* PropertyListType.swift in Sources */,
+				C41FC7461D9BEEC000C3A0F1 /* Array+File.swift in Sources */,
+				52B938C91BF402ED001B7AEB /* ImageFile.swift in Sources */,
+				17217D5E1CDCD74000723D11 /* DispatchEvent.swift in Sources */,
+				52255DCB1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */,
+				C4F7D01F1C08C0DA00EF359B /* FileSystemEvent.swift in Sources */,
+				524D31571BC79067008B93D0 /* ArrayFile.swift in Sources */,
+				523C33A11B9B772A00AB70E4 /* File.swift in Sources */,
+				52A0170A1C01D5400045A9C8 /* FileType.swift in Sources */,
+				C41FC73A1D9BDCB400C3A0F1 /* Data+FileKit.swift in Sources */,
+				17217D5F1CDCD74000723D11 /* DispatchWatcher.swift in Sources */,
+				523C337B1B9B68D600AB70E4 /* DictionaryFile.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5204B8531B96B85E00AA473F /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				17217D601CDCD79C00723D11 /* DispatchEvent.swift in Sources */,
+				17217D611CDCD79C00723D11 /* DispatchWatcher.swift in Sources */,
+				17217D571CDCD1BD00723D11 /* RelativePathType.swift in Sources */,
+				52A016EA1C01565C0045A9C8 /* FileKit.swift in Sources */,
+				52A016FC1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */,
+				5263A8FF1B96B95100635A93 /* String+FileKit.swift in Sources */,
+				C41FC73B1D9BDCD100C3A0F1 /* Data+FileKit.swift in Sources */,
+				52A016F21C0184CA0045A9C8 /* FileProtection.swift in Sources */,
+				52A016F71C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */,
+				52A017061C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */,
+				524D315C1BC79331008B93D0 /* NSDataFile.swift in Sources */,
+				52A0170B1C01D5400045A9C8 /* FileType.swift in Sources */,
+				523C33A81B9B894A00AB70E4 /* Operators.swift in Sources */,
+				C411910D1F804C8300978BA4 /* JSONType.swift in Sources */,
+				C41FC74D1D9BF05A00C3A0F1 /* Dictionary+File.swift in Sources */,
+				C41FC7471D9BEEF300C3A0F1 /* Array+File.swift in Sources */,
+				52A017111C024D840045A9C8 /* NSString+FileKit.swift in Sources */,
+				520E0E311C110CA900BAEA99 /* Process+FileKit.swift in Sources */,
+				523C33791B9A7EFC00AB70E4 /* TextFile.swift in Sources */,
+				52A017011C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */,
+				C41191141F804C8800978BA4 /* PropertyListType.swift in Sources */,
+				5263A8FD1B96B94D00635A93 /* Path.swift in Sources */,
+				52A016C51C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */,
+				524D31601BC7A02A008B93D0 /* Image+FileKit.swift in Sources */,
+				523C339A1B9B764600AB70E4 /* DataType.swift in Sources */,
+				52BF6BB21B99322000F07E13 /* FileKitError.swift in Sources */,
+				52B938CA1BF402ED001B7AEB /* ImageFile.swift in Sources */,
+				52255DCC1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */,
+				524D31581BC79067008B93D0 /* ArrayFile.swift in Sources */,
+				523C33A21B9B772A00AB70E4 /* File.swift in Sources */,
+				523C337C1B9B68D600AB70E4 /* DictionaryFile.swift in Sources */,
+				C41FC7421D9BEC7700C3A0F1 /* DataFile.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5263A9001B96BA3D00635A93 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5263A9071B96BA3D00635A93 /* FileKitTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5276123D1BAEA3EE00503D0A /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				17217D621CDCD90B00723D11 /* DispatchEvent.swift in Sources */,
+				17217D631CDCD90B00723D11 /* DispatchWatcher.swift in Sources */,
+				17217D581CDCD1C300723D11 /* RelativePathType.swift in Sources */,
+				52A016EB1C01565C0045A9C8 /* FileKit.swift in Sources */,
+				52A016FD1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */,
+				527612521BAEA45D00503D0A /* String+FileKit.swift in Sources */,
+				C41FC73C1D9BDCD100C3A0F1 /* Data+FileKit.swift in Sources */,
+				52A016F31C0184CA0045A9C8 /* FileProtection.swift in Sources */,
+				52A016F81C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */,
+				52A017071C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */,
+				524D315D1BC79331008B93D0 /* NSDataFile.swift in Sources */,
+				52A0170C1C01D5400045A9C8 /* FileType.swift in Sources */,
+				5276124D1BAEA43600503D0A /* DictionaryFile.swift in Sources */,
+				C411910E1F804C8300978BA4 /* JSONType.swift in Sources */,
+				C41FC74C1D9BF05A00C3A0F1 /* Dictionary+File.swift in Sources */,
+				C41FC74A1D9BEEF400C3A0F1 /* Array+File.swift in Sources */,
+				52A017121C024D840045A9C8 /* NSString+FileKit.swift in Sources */,
+				520E0E321C110CA900BAEA99 /* Process+FileKit.swift in Sources */,
+				5276124B1BAEA43600503D0A /* File.swift in Sources */,
+				52A017021C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */,
+				C41191111F804C8600978BA4 /* PropertyListType.swift in Sources */,
+				5276124C1BAEA43600503D0A /* TextFile.swift in Sources */,
+				52A016C61C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */,
+				524D31611BC7A02A008B93D0 /* Image+FileKit.swift in Sources */,
+				5276124E1BAEA43600503D0A /* Path.swift in Sources */,
+				527612511BAEA43600503D0A /* Operators.swift in Sources */,
+				527612501BAEA43600503D0A /* FileKitError.swift in Sources */,
+				52255DCD1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */,
+				52B938CB1BF402ED001B7AEB /* ImageFile.swift in Sources */,
+				524D31591BC79067008B93D0 /* ArrayFile.swift in Sources */,
+				5276124F1BAEA43600503D0A /* DataType.swift in Sources */,
+				C41FC7431D9BEC7800C3A0F1 /* DataFile.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		52B938AE1BF3C3E5001B7AEB /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				17217D641CDCD91200723D11 /* DispatchEvent.swift in Sources */,
+				17217D651CDCD91200723D11 /* DispatchWatcher.swift in Sources */,
+				17217D591CDCD1C900723D11 /* RelativePathType.swift in Sources */,
+				52B938C51BF3C430001B7AEB /* String+FileKit.swift in Sources */,
+				52A016EC1C01565C0045A9C8 /* FileKit.swift in Sources */,
+				52A017131C024D840045A9C8 /* NSString+FileKit.swift in Sources */,
+				C41FC73D1D9BDCD200C3A0F1 /* Data+FileKit.swift in Sources */,
+				52A017081C01AD6F0045A9C8 /* Bundle+FileKit.swift in Sources */,
+				52A016FE1C01AD1E0045A9C8 /* NSArray+FileKit.swift in Sources */,
+				52B938C21BF3C42A001B7AEB /* DataType.swift in Sources */,
+				52B938C41BF3C42A001B7AEB /* Operators.swift in Sources */,
+				52A016F41C0184CA0045A9C8 /* FileProtection.swift in Sources */,
+				52B938BD1BF3C42A001B7AEB /* TextFile.swift in Sources */,
+				C411910F1F804C8400978BA4 /* JSONType.swift in Sources */,
+				C41FC74B1D9BF05A00C3A0F1 /* Dictionary+File.swift in Sources */,
+				C41FC7491D9BEEF400C3A0F1 /* Array+File.swift in Sources */,
+				52B938C11BF3C42A001B7AEB /* Path.swift in Sources */,
+				52A016F91C01ACE50045A9C8 /* NSDictionary+FileKit.swift in Sources */,
+				52A0170D1C01D5400045A9C8 /* FileType.swift in Sources */,
+				52B938BE1BF3C42A001B7AEB /* DictionaryFile.swift in Sources */,
+				C41191101F804C8600978BA4 /* PropertyListType.swift in Sources */,
+				520E0E331C110CA900BAEA99 /* Process+FileKit.swift in Sources */,
+				52A017031C01AD4A0045A9C8 /* NSData+FileKit.swift in Sources */,
+				52A016C71C013F3D0045A9C8 /* DirectoryEnumerator.swift in Sources */,
+				52B938C71BF3C430001B7AEB /* Image+FileKit.swift in Sources */,
+				52B938C01BF3C42A001B7AEB /* NSDataFile.swift in Sources */,
+				52255DCE1C042D6E00AC1DA5 /* FilePermissions.swift in Sources */,
+				52B938CC1BF402ED001B7AEB /* ImageFile.swift in Sources */,
+				52B938C31BF3C42A001B7AEB /* FileKitError.swift in Sources */,
+				52B938BC1BF3C42A001B7AEB /* File.swift in Sources */,
+				52B938BF1BF3C42A001B7AEB /* ArrayFile.swift in Sources */,
+				C41FC7441D9BEC7800C3A0F1 /* DataFile.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		5263A90B1B96BA3D00635A93 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 5204B84A1B96B83800AA473F /* FileKit-OSX */;
+			targetProxy = 5263A90A1B96BA3D00635A93 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		5204B7BC1B968B8600AA473F /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_RELEASE_NUMBER = 12;
+				CURRENT_RELEASE_VERSION = 4.0.1;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MACOSX_DEPLOYMENT_TARGET = 10.9;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Debug;
+		};
+		5204B7BD1B968B8600AA473F /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_RELEASE_NUMBER = 12;
+				CURRENT_RELEASE_VERSION = 4.0.1;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MACOSX_DEPLOYMENT_TARGET = 10.9;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Release;
+		};
+		5204B8511B96B83800AA473F /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				COMBINE_HIDPI_IMAGES = YES;
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				FRAMEWORK_VERSION = A;
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		5204B8521B96B83800AA473F /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				COMBINE_HIDPI_IMAGES = YES;
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				FRAMEWORK_VERSION = A;
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		5204B85E1B96B85E00AA473F /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SDKROOT = iphoneos;
+				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		5204B85F1B96B85E00AA473F /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SDKROOT = iphoneos;
+				SKIP_INSTALL = YES;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		5263A90D1B96BA3D00635A93 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = "$(SRCROOT)/Tests/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.nikolaivazquez.FileKitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		5263A90E1B96BA3D00635A93 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = "$(SRCROOT)/Tests/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.nikolaivazquez.FileKitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		527612471BAEA3EE00503D0A /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				WATCHOS_DEPLOYMENT_TARGET = 2.0;
+			};
+			name = Debug;
+		};
+		527612481BAEA3EE00503D0A /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				VALIDATE_PRODUCT = YES;
+				WATCHOS_DEPLOYMENT_TARGET = 2.0;
+			};
+			name = Release;
+		};
+		52B938B81BF3C3E5001B7AEB /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SDKROOT = appletvos;
+				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 3;
+				TVOS_DEPLOYMENT_TARGET = 9.0;
+			};
+			name = Debug;
+		};
+		52B938B91BF3C3E5001B7AEB /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/Support/Info.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.nikolaivazquez.$(PROJECT_NAME)";
+				PRODUCT_NAME = "$(PROJECT_NAME)";
+				SDKROOT = appletvos;
+				SKIP_INSTALL = YES;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 3;
+				TVOS_DEPLOYMENT_TARGET = 9.0;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		5204B7B01B968B8600AA473F /* Build configuration list for PBXProject "FileKit" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5204B7BC1B968B8600AA473F /* Debug */,
+				5204B7BD1B968B8600AA473F /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		5204B8501B96B83800AA473F /* Build configuration list for PBXNativeTarget "FileKit-OSX" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5204B8511B96B83800AA473F /* Debug */,
+				5204B8521B96B83800AA473F /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		5204B85D1B96B85E00AA473F /* Build configuration list for PBXNativeTarget "FileKit-iOS" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5204B85E1B96B85E00AA473F /* Debug */,
+				5204B85F1B96B85E00AA473F /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		5263A90C1B96BA3D00635A93 /* Build configuration list for PBXNativeTarget "FileKitTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5263A90D1B96BA3D00635A93 /* Debug */,
+				5263A90E1B96BA3D00635A93 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		527612491BAEA3EE00503D0A /* Build configuration list for PBXNativeTarget "FileKit-watchOS" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				527612471BAEA3EE00503D0A /* Debug */,
+				527612481BAEA3EE00503D0A /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		52B938BA1BF3C3E5001B7AEB /* Build configuration list for PBXNativeTarget "FileKit-tvOS" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				52B938B81BF3C3E5001B7AEB /* Debug */,
+				52B938B91BF3C3E5001B7AEB /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 5204B7AD1B968B8600AA473F /* Project object */;
+}

+ 7 - 0
Frameworks/FileKit/FileKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:FileKit.xcodeproj">
+   </FileRef>
+</Workspace>

+ 21 - 0
Frameworks/FileKit/LICENSE.md

@@ -0,0 +1,21 @@
+# The MIT License (MIT)
+
+**Copyright (c) 2015-2016 Nikolai Vazquez**
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 52 - 0
Frameworks/FileKit/Package.swift

@@ -0,0 +1,52 @@
+// swift-tools-version:5.0
+//
+//  Package.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import PackageDescription
+
+let package = Package(
+    name: "FileKit",
+    products: [
+        // Products define the executables and libraries produced by a package, and make them visible to other packages.
+        .library(
+            name: "FileKit",
+            targets: ["FileKit"]),
+    ],
+    dependencies: [],
+    targets: [
+        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
+        .target(
+            name: "FileKit",
+            dependencies: [],
+            path: "Sources"),
+        .testTarget(
+            name: "FileKitTests",
+            dependencies: ["FileKit"],
+            path: "Tests")
+    ]
+)

+ 451 - 0
Frameworks/FileKit/README.md

@@ -0,0 +1,451 @@
+<div align="center">
+    <img src="https://github.com/nvzqz/FileKit/raw/assets/banner.png">
+</div>
+
+<div align="center">
+    <img src="https://img.shields.io/badge/platform-osx%20%7C%20ios%20%7C%20watchos%20%7C%20tvos-lightgrey.svg"
+         alt="Platform">
+    <img src="https://img.shields.io/badge/language-swift-orange.svg"
+         alt="Language: Swift">
+    <a href="https://cocoapods.org/pods/FileKit">
+        <img src="https://img.shields.io/cocoapods/v/FileKit.svg"
+             alt="CocoaPods">
+    </a>
+    <a href="https://github.com/Carthage/Carthage">
+        <img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"
+             alt="Carthage">
+    </a>
+    <br>
+    <img src="https://img.shields.io/cocoapods/dt/FileKit.svg"
+         alt="downloads">
+    <a href="https://trello.com/b/s1MOyp2h/filekit">
+        <img src="https://img.shields.io/badge/Trello-filekit-blue.svg"
+             alt="Trello Board">
+    </a>
+    <a href="https://gitter.im/nvzqz/FileKit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge">
+        <img src="https://img.shields.io/badge/GITTER-join%20chat-00D06F.svg"
+             alt="GITTER: join chat">
+    </a>
+    <img src="https://img.shields.io/badge/license-MIT-000000.svg"
+         alt="License">
+    <br>
+    <a href="https://www.patreon.com/nvzqz">
+        <img src="https://c5.patreon.com/external/logo/become_a_patron_button.png" alt="Become a Patron!" height="35">
+    </a>
+    <a href="https://www.paypal.me/nvzqz">
+        <img src="https://buymecoffee.intm.org/img/button-paypal-white.png" alt="Buy me a coffee" height="35">
+    </a>
+</div>
+
+<div align="center">
+    <a href="#installation">Installation</a>
+  • <a href="#usage">Usage</a>
+  • <a href="#license">License</a>
+  • <a href="https://nvzqz.github.io/FileKit/docs/">Documentation</a>
+</div>
+
+
+FileKit is a Swift framework that allows for simple and expressive file management.
+
+Development happens in the
+[`develop`](https://github.com/nvzqz/FileKit/tree/develop) branch.
+
+## Installation
+
+### Compatibility
+
+- OS X 10.9+ / iOS 8.0+ / watchOS 2.0 / tvOS 9.0
+
+- Xcode 7.1+, Swift 2.1+
+
+### Install Using CocoaPods
+[CocoaPods](https://cocoapods.org/) is a centralized dependency manager for
+Objective-C and Swift. Go [here](https://guides.cocoapods.org/using/index.html)
+to learn more.
+
+1. Add the project to your [Podfile](https://guides.cocoapods.org/using/the-podfile.html).
+
+    ```ruby
+    use_frameworks!
+
+    pod 'FileKit', '~> 5.0.0'
+    ```
+
+2. Run `pod install` and open the `.xcworkspace` file to launch Xcode.
+
+3. Import the FileKit framework.
+
+    ```swift
+    import FileKit
+    ```
+
+### Install Using Carthage
+[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency
+manager for Objective-C and Swift.
+
+1. Add the project to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile).
+
+    ```
+    github "nvzqz/FileKit"
+    ```
+
+2. Run `carthage update` and follow [the additional steps](https://github.com/Carthage/Carthage#getting-started)
+   in order to add FileKit to your project.
+
+3. Import the FileKit framework.
+
+    ```swift
+    import FileKit
+    ```
+
+## Usage
+
+### Paths
+
+Paths are handled with the `Path` structure.
+
+```swift
+let home = Path("~")
+let drive: Path = "/Volumes/Macintosh HD"
+let file:  Path = "~/Desktop/file\(1)"
+```
+
+#### Operations
+
+##### New Files
+
+A blank file can be written by calling `createFile()` on an `Path`.
+
+```swift
+try Path(".gitignore").createFile()
+```
+
+##### New Directories
+
+A directory can be created by calling `createDirectory()` on an `Path`.
+
+```swift
+try Path("~/Files").createDirectory()
+try Path("~/Books").createDirectory(withIntermediateDirectories: false)
+```
+
+Intermediate directories are created by default.
+
+##### New Symlinks
+
+A symbolic link can be created by calling `createSymlinkToPath(_:)` on an `Path`.
+
+```swift
+try Path("path/to/MyApp.app").symlinkFile(to: "~/Applications")
+print(Path("~/Applications/MyApp.app").exists)  // true
+```
+
+##### Finding Paths
+
+You can find all paths with the ".txt" extension five folders deep into the
+Desktop with:
+
+```swift
+let textFiles = Path.userDesktop.find(searchDepth: 5) { path in
+    path.pathExtension == "txt"
+}
+```
+
+A negative `searchDepth` will make it run until every path in `self` is checked
+against.
+
+You can even map a function to paths found and get the non-nil results:
+
+```swift
+let documents = Path.userDocuments.find(searchDepth: 1) { path in
+    String(path)
+}
+```
+
+##### Iterating Through Paths
+
+Because `Path` conforms to `SequenceType`, it can be iterated through with a
+`for` loop.
+
+```swift
+for download in Path.userDownloads {
+    print("Downloaded file: \(download)")
+}
+```
+
+##### Current Working Directory
+
+The current working directory for the process can be changed with `Path.Current`.
+
+To quickly change the current working directory to a path and back, there's the
+`changeDirectory(_:)` method:
+
+```swift
+Path.userDesktop.changeDirectory {
+    print(Path.current)  // "/Users/nvzqz/Desktop"
+}
+```
+
+##### Common Ancestor
+
+A common ancestor between two paths can be obtained:
+
+```swift
+print(Path.root.commonAncestor(.userHome))       // "/"
+print("~/Desktop"  <^> "~/Downloads")            // "~"
+print(.UserLibrary <^> .UserApplicationSupport)  // "/Users/nvzqz/Library"
+```
+
+##### `+` Operator
+
+Appends two paths and returns the result
+
+```swift
+// ~/Documents/My Essay.docx
+let essay = Path.userDocuments + "My Essay.docx"
+```
+
+It can also be used to concatenate a string and a path, making the string value
+a `Path` beforehand.
+
+```swift
+let numberedFile: Path = "path/to/dir" + String(10)  // "path/to/dir/10"
+```
+
+##### `+=` Operator
+
+Appends the right path to the left path. Also works with a `String`.
+
+```swift
+var photos = Path.userPictures + "My Photos"  // ~/Pictures/My Photos
+photos += "../My Other Photos"                // ~/Pictures/My Photos/../My Other Photos
+```
+
+##### `%` Operator
+
+Returns the standardized version of the path.
+
+```swift
+let path: Path = "~/Desktop"
+path% == path.standardized  // true
+```
+
+##### `*` Operator
+
+Returns the resolved version of the path.
+
+```swift
+let path: Path = "~/Documents"
+path* == path.resolved  // true
+```
+
+##### `^` Operator
+
+Returns the path's parent path.
+
+```swift
+let path: Path = "~/Movies"
+path^ == "~"  // true
+```
+
+##### `->>` Operator
+
+Moves the file at the left path to the right path.
+
+`Path` counterpart: **`moveFile(to:)`**
+
+`File` counterpart: **`move(to:)`**
+
+##### `->!` Operator
+
+Forcibly moves the file at the left path to the right path by deleting anything
+at the left path before moving the file.
+
+##### `+>>` Operator
+
+Copies the file at the left path to the right path.
+
+`Path` counterpart: **`copyFile(to:)`**
+
+`File` counterpart: **`copy(to:)`**
+
+##### `+>!` Operator
+
+Forcibly copies the file at the left path to the right path by deleting anything
+at the left path before copying the file.
+
+##### `=>>` Operator
+
+Creates a symlink of the left path at the right path.
+
+`Path` counterpart: **`symlinkFile(to:)`**
+
+`File` counterpart: **`symlink(to:)`**
+
+##### `=>!` Operator
+
+Forcibly creates a symlink of the left path at the right path by deleting
+anything at the left path before creating the symlink.
+
+##### Subscripting
+
+Subscripting an `Path` will return all of its components up to and including
+the index.
+
+```swift
+let users = Path("/Users/me/Desktop")[1]  // /Users
+```
+
+##### `standardize()`
+
+Standardizes the path.
+
+The same as doing:
+```swift
+somePath = somePath.standardized
+```
+
+##### `resolve()`
+
+Resolves the path's symlinks.
+
+The same as doing:
+```swift
+somePath = somePath.resolved
+```
+
+### Files
+
+A file can be made using `File` with a `DataType` for its data type.
+
+```swift
+let plistFile = File<Dictionary>(path: Path.userDesktop + "sample.plist")
+```
+
+Files can be compared by size.
+
+#### Operators
+
+##### `|>` Operator
+
+Writes the data on the left to the file on the right.
+
+```swift
+do {
+    try "My name is Bob." |> TextFile(path: Path.userDesktop + "name.txt")
+} catch {
+    print("I can't write to a desktop file?!")
+}
+```
+
+#### TextFile
+
+The `TextFile` class allows for reading and writing strings to a file.
+
+Although it is a subclass of `File<String>`, `TextFile` offers some functionality
+that `File<String>` doesn't.
+
+##### `|>>` Operator
+
+Appends the string on the left to the `TextFile` on the right.
+
+```swift
+let readme = TextFile(path: "README.txt")
+try "My Awesome Project" |> readme
+try "This is an awesome project." |>> readme
+```
+
+#### NSDictionaryFile
+
+A typealias to `File<NSDictionary>`.
+
+#### NSArrayFile
+
+A typealias to `File<NSArray>`
+
+#### NSDataFile
+
+A typealias to `File<NSData>`
+
+#### DataFile
+
+The `DataFile` class allows for reading and writing `Data` to a file.
+
+Although it is a subclass of `File<Data>`, `DataFile` offers some functionality
+that `File<Data>` doesn't. You could specify `Data.ReadingOptions` and `Data.WritingOptions`
+
+#### Encodable/Decodable
+
+You can use any `Codable` object with `File`.
+
+```swift
+extension AnyCodableClass: JSONReadableWritable {} // if you want json encoding/decoding
+
+let codableFile = File<AnyCodableClass>(path: path)
+try codableFile.write(toEncode)
+let decoded: AnyCodableClass = try codableFile.read()
+```
+
+Alternatively you can use utility methods
+
+```swift
+try FileKit.write(toEncode, to: path)
+let decoded: AnyCodableClass = try FileKit.read(from: path)
+```
+
+### File Permissions
+
+The `FilePermissions` struct allows for seeing the permissions of the current
+process for a given file.
+
+```swift
+let swift: Path = "/usr/bin/swift"
+print(swift.filePermissions)  // FilePermissions[read, execute]
+```
+
+### Data Types
+
+All types that conform to `DataType` can be used to satisfy the generic type for
+`File`.
+
+#### Readable Protocol
+
+A `Readable` type must implement the static method `read(from: Path)`.
+
+All `Readable` types can be initialized with `init(contentsOfPath:)`.
+
+#### Writable Protocol
+
+A `Writable` type must implement `write(to: Path, atomically: Bool)`.
+
+Writing done by `write(to: Path)` is done atomically by default.
+
+##### WritableToFile
+
+Types that have a `write(toFile:atomically:)` method that takes in a `String`
+for the file path can conform to `Writable` by simply conforming to
+`WritableToFile`.
+
+##### WritableConvertible
+
+If a type itself cannot be written to a file but can output a writable type,
+then it can conform to `WritableConvertible` and become a `Writable` that way.
+
+### FileKitError
+
+The type for all errors thrown by FileKit operations is `FileKitError`.
+
+Errors can be converted to `String` directly for any logging. If only the error
+message is needed, `FileKitError` has a `message` property that states why the
+error occurred.
+
+```swift
+// FileKitError(Could not copy file from "path/to/file" to "path/to/destination")
+String(FileKitError.copyFileFail(from: "path/to/file", to: "path/to/destination"))
+```
+
+## License
+
+FileKit and its assets are released under the [MIT License](LICENSE.md). Assets
+can be found in the [`assets`](https://github.com/nvzqz/FileKit/tree/assets)
+branch.

+ 53 - 0
Frameworks/FileKit/Sources/Array+File.swift

@@ -0,0 +1,53 @@
+//
+//  Array+File.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2017 Nikolai Vazquez
+//  Copyright (c) 2017 Marchand Eric
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension Array: ReadableWritable, WritableConvertible {
+
+    /// Returns an array from the given path.
+    ///
+    /// - Parameter path: The path to be returned the array for.
+    /// - Throws: FileKitError.ReadFromFileFail
+    ///
+    public static func read(from path: Path) throws -> Array {
+        guard let contents = NSArray(contentsOfFile: path._safeRawValue) else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(NSArray.self))
+        }
+        guard let dict = contents as? Array else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(Array.self))
+        }
+        return dict
+    }
+
+    // Return an bridged NSArray value
+    public var writable: NSArray {
+        return self as NSArray
+    }
+
+}

+ 38 - 0
Frameworks/FileKit/Sources/ArrayFile.swift

@@ -0,0 +1,38 @@
+//
+//  NSArrayFile.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A representation of a filesystem array file.
+///
+/// The data type is NSArray.
+public typealias NSArrayFile = File<NSArray>
+
+/// A representation of a filesystem array file.
+///
+/// The data type is Array.
+public typealias ArrayFile<T> = File<[T]>

+ 37 - 0
Frameworks/FileKit/Sources/Bundle+FileKit.swift

@@ -0,0 +1,37 @@
+//
+//  Bundle+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension Bundle {
+
+    /// Returns an NSBundle for the given directory path.
+    public convenience init?(path: Path) {
+        self.init(path: path.absolute.rawValue)
+    }
+
+}

+ 82 - 0
Frameworks/FileKit/Sources/Data+FileKit.swift

@@ -0,0 +1,82 @@
+//
+//  Data+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2017 Nikolai Vazquez
+//  Copyright (c) 2017 Marchand Eric
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension Data: ReadableWritable {
+
+    /// Returns data read from the given path.
+    public static func read(from path: Path) throws -> Data {
+        do {
+            return try self.init(contentsOf: path.url, options: [])
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+    /// Returns data read from the given path using Data.ReadingOptions.
+    public static func read(from path: Path, options: Data.ReadingOptions) throws -> Data {
+        do {
+            return try self.init(contentsOf: path.url, options: options)
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+    /// Writes `self` to a Path.
+    public func write(to path: Path) throws {
+        try write(to: path, atomically: true)
+    }
+
+    /// Writes `self` to a path.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        let options: Data.WritingOptions = useAuxiliaryFile ? [.atomic] : []
+        try self.write(to: path, options: options)
+    }
+
+    /// Writes `self` to a path.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter options: writing options.
+    ///
+    public func write(to path: Path, options: Data.WritingOptions) throws {
+        do {
+            try self.write(to: path.url, options: options)
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+}

+ 91 - 0
Frameworks/FileKit/Sources/DataFile.swift

@@ -0,0 +1,91 @@
+//
+//  DataFile.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A representation of a filesystem data file.
+///
+/// The data type is Data.
+open class DataFile: File<Data> {
+
+    /// Reads the file and returns its data.
+    /// - Parameter options: A mask that specifies write options
+    ///                      described in `Data.ReadingOptions`.
+    ///
+    /// - Throws: `FileKitError.ReadFromFileFail`
+    /// - Returns: The data read from file.
+    public func read(_ options: Data.ReadingOptions) throws -> Data {
+        return try Data.read(from: path, options: options)
+    }
+
+    /// Writes data to the file.
+    ///
+    /// - Parameter data: The data to be written to the file.
+    /// - Parameter options: A mask that specifies write options
+    ///                      described in `Data.WritingOptions`.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    public func write(_ data: Data, options: Data.WritingOptions) throws {
+        try data.write(to: self.path, options: options)
+    }
+
+}
+
+/// A representation of a filesystem data file,
+/// with options to read or write.
+///
+/// The data type is Data.
+open class DataFileWithOptions: DataFile {
+
+    open var readingOptions: Data.ReadingOptions = []
+    open var writingOptions: Data.WritingOptions?
+
+    /// Initializes a file from a path with options.
+    ///
+    /// - Parameter path: The path to be created a text file from.
+    /// - Parameter readingOptions: The options to be used to read file.
+    /// - Parameter writingOptions: The options to be used to write file.
+    ///                             If nil take into account `useAuxiliaryFile`
+    public init(path: Path, readingOptions: Data.ReadingOptions = [], writingOptions: Data.WritingOptions? = nil) {
+        self.readingOptions = readingOptions
+        self.writingOptions = writingOptions
+        super.init(path: path)
+    }
+
+    open override func read() throws -> Data {
+        return try read(readingOptions)
+    }
+
+    open override func write(_ data: Data, atomically useAuxiliaryFile: Bool) throws {
+        // If no option take into account useAuxiliaryFile
+        let options: Data.WritingOptions = (writingOptions == nil) ?
+            (useAuxiliaryFile ? Data.WritingOptions.atomic : [])
+            : writingOptions!
+        try self.write(data, options: options)
+    }
+}

+ 153 - 0
Frameworks/FileKit/Sources/DataType.swift

@@ -0,0 +1,153 @@
+//
+//  ReadableWritable.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A type that can be used to read from and write to File instances.
+public typealias ReadableWritable = Readable & Writable
+
+/// A type that can be used to read from File instances.
+public protocol Readable {
+
+    /// Creates `Self` from the contents of a Path.
+    ///
+    /// - Parameter path: The path being read from.
+    ///
+    static func read(from path: Path) throws -> Self
+
+}
+
+extension Readable {
+
+    /// Initializes `self` from the contents of a Path.
+    ///
+    /// - Parameter path: The path being read from.
+    ///
+    public init(contentsOfPath path: Path) throws {
+        self = try Self.read(from: path)
+    }
+
+}
+
+/// A type that can be used to write to File instances.
+public protocol Writable {
+
+    /// Writes `self` to a Path.
+    func write(to path: Path) throws
+
+    /// Writes `self` to a Path.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    func write(to path: Path, atomically useAuxiliaryFile: Bool) throws
+
+}
+
+extension Writable {
+
+    /// Writes `self` to a Path atomically.
+    ///
+    /// - Parameter path: The path being written to.
+    ///
+    public func write(to path: Path) throws {
+        try write(to: path, atomically: true)
+    }
+
+}
+
+/// A type that can be used to write to a String file path.
+public protocol WritableToFile: Writable {
+
+    /// Writes `self` to a String path.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    /// - Returns: `true` if the writing completed successfully, or `false` if
+    ///            the writing failed.
+    ///
+    func write(toFile path: String, atomically useAuxiliaryFile: Bool) -> Bool
+
+}
+
+extension WritableToFile {
+
+    /// Writes `self` to a Path.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        guard write(toFile: path._safeRawValue, atomically: useAuxiliaryFile) else {
+            throw FileKitError.writeToFileFail(path: path, error: FileKitError.ReasonError.conversion(type(of: self)))
+        }
+    }
+
+}
+
+/// A type that can be converted to a Writable.
+public protocol WritableConvertible: Writable {
+
+    /// The type that allows `Self` to be `Writable`.
+    associatedtype WritableType: Writable
+
+    /// Allows `self` to be written to a path.
+    var writable: WritableType { get }
+
+}
+
+extension WritableConvertible {
+
+    /// Writes `self` to a Path.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    /// - Throws:
+    ///     `FileKitError.WriteToFileFail`,
+    ///     `FileKitError.WritableConvertiblePropertyNil`
+    ///
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        try writable.write(to: path, atomically: useAuxiliaryFile)
+    }
+
+}

+ 53 - 0
Frameworks/FileKit/Sources/Dictionary+File.swift

@@ -0,0 +1,53 @@
+//
+//  Dictionary+File.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2017 Nikolai Vazquez
+//  Copyright (c) 2017 Marchand Eric
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension Dictionary: ReadableWritable, WritableConvertible {
+
+    /// Returns a dictionary from the given path.
+    ///
+    /// - Parameter path: The path to be returned the dictionary for.
+    /// - Throws: FileKitError.ReadFromFileFail
+    ///
+    public static func read(from path: Path) throws -> Dictionary {
+        guard let contents = NSDictionary(contentsOfFile: path._safeRawValue) else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(NSDictionary.self))
+        }
+        guard let dict = contents as? Dictionary else {
+             throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(Dictionary.self))
+        }
+        return dict
+    }
+
+    // Return an bridged NSDictionary value
+    public var writable: NSDictionary {
+        return self as NSDictionary
+    }
+
+}

+ 38 - 0
Frameworks/FileKit/Sources/DictionaryFile.swift

@@ -0,0 +1,38 @@
+//
+//  NSDictionaryFile.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A representation of a filesystem dictionary file.
+///
+/// The data type is NSDictionary.
+public typealias NSDictionaryFile = File<NSDictionary>
+
+/// A representation of a filesystem dictionary file.
+///
+/// The data type is DictionaryFile.
+public typealias DictionaryFile<K: Hashable, V> = File<[K: V]>

+ 56 - 0
Frameworks/FileKit/Sources/DirectoryEnumerator.swift

@@ -0,0 +1,56 @@
+//
+//  DirectoryEnumerator.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// An enumerator for the contents of a directory that returns the paths of all
+/// files and directories contained within that directory.
+public struct DirectoryEnumerator: IteratorProtocol {
+
+    fileprivate let _path: Path, _enumerator: FileManager.DirectoryEnumerator?
+
+    /// Creates a directory enumerator for the given path.
+    ///
+    /// - Parameter path: The path a directory enumerator to be created for.
+    public init(path: Path) {
+        _path = path
+        _enumerator = FileManager().enumerator(atPath: path._safeRawValue)
+    }
+
+    /// Returns the next path in the enumeration.
+    public func next() -> Path? {
+        guard let next = _enumerator?.nextObject() as? String else {
+            return nil
+        }
+        return _path + next
+    }
+
+    /// Skip recursion into the most recently obtained subdirectory.
+    public func skipDescendants() {
+        _enumerator?.skipDescendants()
+    }
+}

+ 93 - 0
Frameworks/FileKit/Sources/DispatchEvent.swift

@@ -0,0 +1,93 @@
+//
+//  GCDFSEvent.swift
+//  FileKit
+//
+//  Created by ijump on 5/2/16.
+//  Copyright © 2017 Nikolai Vazquez. All rights reserved.
+//
+
+import Foundation
+
+/// File System Events.
+public struct DispatchFileSystemEvents: OptionSet, CustomStringConvertible, CustomDebugStringConvertible {
+
+    // MARK: - Events
+
+    /// The file-system object was deleted from the namespace.
+    public static let Delete = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.delete.rawValue)
+
+    /// The file-system object data changed.
+    public static let Write = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.write.rawValue)
+
+    /// The file-system object changed in size.
+    public static let Extend = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.extend.rawValue)
+
+    /// The file-system object metadata changed.
+    public static let Attribute = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.attrib.rawValue)
+
+    /// The file-system object link count changed.
+    public static let Link = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.link.rawValue)
+
+    /// The file-system object was renamed in the namespace.
+    public static let Rename = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.rename.rawValue)
+
+    /// The file-system object was revoked.
+    public static let Revoke = DispatchFileSystemEvents(rawValue: DispatchSource.FileSystemEvent.revoke.rawValue)
+
+    /// The file-system object was created.
+    public static let Create = DispatchFileSystemEvents(rawValue: 0x1000)
+
+    /// All of the event IDs.
+    public static let All: DispatchFileSystemEvents = [.Delete, .Write, .Extend, .Attribute, .Link, .Rename, .Revoke, .Create]
+
+    // MARK: - All Events
+
+    /// An array of all of the events.
+    public static let allEvents: [DispatchFileSystemEvents] = [
+        .Delete, .Write, .Extend, .Attribute, .Link, .Rename, .Revoke, .Create
+    ]
+
+    /// The names of all of the events.
+    public static let allEventNames: [String] = [
+        "Delete", "Write", "Extend", "Attribute", "Link", "Rename", "Revoke", "Create"
+    ]
+
+    // MARK: - Properties
+
+    /// The raw event value.
+    public let rawValue: UInt
+
+    /// A textual representation of `self`.
+    public var description: String {
+        var result = ""
+        for (index, element) in DispatchFileSystemEvents.allEvents.enumerated() {
+            if self.contains(element) {
+                let name = DispatchFileSystemEvents.allEventNames[index]
+                result += result.isEmpty ? "\(name)": ", \(name)"
+            }
+        }
+        return String(describing: type(of: self)) + "[\(result)]"
+    }
+
+    /// A textual representation of `self`, suitable for debugging.
+    public var debugDescription: String {
+        var result = ""
+        for (index, element) in DispatchFileSystemEvents.allEvents.enumerated() {
+            if self.contains(element) {
+                let name = DispatchFileSystemEvents.allEventNames[index] + "(\(element.rawValue))"
+                result += result.isEmpty ? "\(name)": ", \(name)"
+            }
+        }
+        return String(describing: type(of: self)) + "[\(result)]"
+    }
+
+    // MARK: - Initialization
+
+    /// Creates a set of events from a raw value.
+    ///
+    /// - Parameter rawValue: The raw value to initialize from.
+    public init(rawValue: UInt) {
+        self.rawValue = rawValue
+    }
+
+}

+ 338 - 0
Frameworks/FileKit/Sources/DispatchWatcher.swift

@@ -0,0 +1,338 @@
+//
+//  GCDFSWatcher.swift
+//  FileKit
+//
+//  Created by ijump on 5/2/16.
+//  Copyright © 2017 Nikolai Vazquez. All rights reserved.
+//
+
+import Foundation
+
+/// Delegate for `DispatchFileSystemWatcher`
+public protocol DispatchFileSystemWatcherDelegate: class {
+
+    // MARK: - Protocol
+
+    /// Call when the file-system object was deleted from the namespace.
+    func fsWatcherDidObserveDelete(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object data changed.
+    func fsWatcherDidObserveWrite(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object changed in size.
+    func fsWatcherDidObserveExtend(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object metadata changed.
+    func fsWatcherDidObserveAttrib(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object link count changed.
+    func fsWatcherDidObserveLink(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object was renamed in the namespace.
+    func fsWatcherDidObserveRename(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object was revoked.
+    func fsWatcherDidObserveRevoke(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the file-system object was created.
+    func fsWatcherDidObserveCreate(_ watch: DispatchFileSystemWatcher)
+
+    /// Call when the directory changed (additions, deletions, and renamings).
+    ///
+    /// Calls `fsWatcherDidObserveWrite` by default.
+    func fsWatcherDidObserveDirectoryChange(_ watch: DispatchFileSystemWatcher)
+}
+
+// Optional func and default func for `GCDFSWatcherDelegate`
+// Empty func treated as Optional func
+public extension DispatchFileSystemWatcherDelegate {
+
+    // MARK: - Extension
+
+    /// Call when the file-system object was deleted from the namespace.
+    func fsWatcherDidObserveDelete(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object data changed.
+    func fsWatcherDidObserveWrite(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object changed in size.
+    func fsWatcherDidObserveExtend(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object metadata changed.
+    func fsWatcherDidObserveAttrib(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object link count changed.
+    func fsWatcherDidObserveLink(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object was renamed in the namespace.
+    func fsWatcherDidObserveRename(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object was revoked.
+    func fsWatcherDidObserveRevoke(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the file-system object was created.
+    func fsWatcherDidObserveCreate(_ watch: DispatchFileSystemWatcher) {}
+
+    /// Call when the directory changed (additions, deletions, and renamings).
+    ///
+    /// Calls `fsWatcherDidObserveWrite` by default.
+    func fsWatcherDidObserveDirectoryChange(_ watch: DispatchFileSystemWatcher) {
+        fsWatcherDidObserveWrite(watch)
+    }
+}
+
+/// Watcher for Vnode events
+open class DispatchFileSystemWatcher {
+
+    // MARK: - Properties
+
+    /// The paths being watched.
+    public let path: Path
+
+    /// The events used to create the watcher.
+    public let events: DispatchFileSystemEvents
+
+    /// The delegate to call when events happen
+    weak var delegate: DispatchFileSystemWatcherDelegate?
+
+    /// The watcher for watching creation event
+    weak var createWatcher: DispatchFileSystemWatcher?
+
+    /// The callback for file system events.
+    fileprivate let callback: ((DispatchFileSystemWatcher) -> Void)?
+
+    /// The queue for the watcher.
+    fileprivate let queue: DispatchQueue?
+
+    /// A file descriptor for the path.
+    fileprivate var fileDescriptor: CInt = -1
+
+    /// A dispatch source to monitor a file descriptor created from the path.
+    fileprivate var source: DispatchSourceProtocol?
+
+    /// Current events
+    open var currentEvent: DispatchFileSystemEvents? {
+        if let source = source {
+            return DispatchFileSystemEvents(rawValue: source.data)
+        }
+        if createWatcher != nil {
+            return .Create
+        }
+        return nil
+    }
+
+    // MARK: - Initialization
+
+    /// Creates a watcher for the given paths.
+    ///
+    /// - Parameter paths: The paths.
+    /// - Parameter events: The create events.
+    /// - Parameter queue: The queue to be run within.
+    /// - Parameter callback: The callback to be called on changes.
+    ///
+    /// This method does follow links.
+    init(path: Path,
+         events: DispatchFileSystemEvents,
+         queue: DispatchQueue,
+         callback: ((DispatchFileSystemWatcher) -> Void)?
+        ) {
+        self.path = path.absolute
+        self.events = events
+        self.queue = queue
+        self.callback = callback
+    }
+
+    // MARK: - Deinitialization
+
+    deinit {
+        // print("\(path): Deinit")
+        close()
+    }
+
+    // MARK: - Private Methods
+
+    /// Dispatch the event.
+    ///
+    /// If `callback` is set, call the `callback`. Else if `delegate` is set, call the `delegate`
+    ///
+    /// - Parameter eventType: The current event to be watched.
+    fileprivate func dispatchDelegate(_ eventType: DispatchFileSystemEvents) {
+        if let callback = self.callback {
+            callback(self)
+        } else if let delegate = self.delegate {
+            if eventType.contains(.Delete) {
+                delegate.fsWatcherDidObserveDelete(self)
+            }
+            if eventType.contains(.Write) {
+                if path.isDirectoryFile {
+                    delegate.fsWatcherDidObserveDirectoryChange(self)
+                } else {
+                    delegate.fsWatcherDidObserveWrite(self)
+                }
+            }
+            if eventType.contains(.Extend) {
+                delegate.fsWatcherDidObserveExtend(self)
+            }
+            if eventType.contains(.Attribute) {
+                delegate.fsWatcherDidObserveAttrib(self)
+            }
+            if eventType.contains(.Link) {
+                delegate.fsWatcherDidObserveLink(self)
+            }
+            if eventType.contains(.Rename) {
+                delegate.fsWatcherDidObserveRename(self)
+            }
+            if eventType.contains(.Revoke) {
+                delegate.fsWatcherDidObserveRevoke(self)
+            }
+            if eventType.contains(.Create) {
+                delegate.fsWatcherDidObserveCreate(self)
+            }
+        }
+
+    }
+
+    // MARK: - Methods
+
+    /// Start watching.
+    ///
+    /// This method does follow links.
+    @discardableResult
+    open func startWatching() -> Bool {
+
+        // create a watcher for CREATE event if path not exists and events contains CREATE
+        if !path.exists {
+            if events.contains(.Create) {
+                let parent = path.parent.absolute
+                var _events = events
+                _events.remove(.Create)
+                // only watch a CREATE event if parent exists and is a directory
+                if parent.isDirectoryFile {
+                    #if os(OSX)
+                        let watch = { parent.watch2($0, callback: $1) }
+                    #else
+                        let watch = { parent.watch($0, callback: $1) }
+                    #endif
+                    createWatcher = watch(.Write) { [weak self] watch in
+                        // stop watching when path created
+                        if self?.path.isRegular == true || self?.path.isDirectoryFile == true {
+                            self?.dispatchDelegate(.Create)
+                            // self.delegate?.fsWatcherDidObserveCreate(self)
+                            self?.createWatcher = nil
+                            self?.startWatching()
+                            watch.stopWatching()
+                        }
+                    }
+                    return true
+                }
+            }
+        }
+
+            // Only watching for regular file and directory
+        else if path.isRegular || path.isDirectoryFile {
+
+            if source == nil && fileDescriptor == -1 {
+                fileDescriptor = open(path._safeRawValue, O_EVTONLY)
+                if fileDescriptor == -1 { return false }
+                var _events = events
+                _events.remove(.Create)
+                source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: DispatchSource.FileSystemEvent(rawValue: _events.rawValue), queue: queue)
+
+                // Recheck if open success and source create success
+                if source != nil && fileDescriptor != -1 {
+                    guard callback != nil || delegate != nil else {
+                        return false
+                    }
+
+                    // Define the block to call when a file change is detected.
+                    source!.setEventHandler { // [unowned self] () in
+                        let eventType = DispatchFileSystemEvents(rawValue: self.source!.data)
+                        self.dispatchDelegate(eventType)
+                    }
+
+                    // Define a cancel handler to ensure the path is closed when the source is cancelled.
+                    source!.setCancelHandler { // [unowned self] () in
+                        _ = Darwin.close(self.fileDescriptor)
+                        self.fileDescriptor = -1
+                        self.source = nil
+                    }
+
+                    // Start monitoring the path via the source.
+                    source!.resume()
+                    return true
+                }
+            }
+        }
+
+        return false
+    }
+
+    /// Stop watching.
+    ///
+    /// **Note:** make sure call this func, or `self` will not release
+    open func stopWatching() {
+        if source != nil {
+            source!.cancel()
+        }
+    }
+
+    /// Closes the watcher.
+    open func close() {
+        createWatcher?.stopWatching()
+        _ = Darwin.close(self.fileDescriptor)
+        self.fileDescriptor = -1
+        self.source = nil
+    }
+
+}
+
+extension Path {
+
+    #if os(OSX)
+    // MARK: - Watching
+
+    /// Watches a path for filesystem events and handles them in the callback or delegate.
+    ///
+    /// - Parameter events: The create events.
+    /// - Parameter queue: The queue to be run within.
+    /// - Parameter delegate: The delegate to call when events happen.
+    /// - Parameter callback: The callback to be called on changes.
+    public func watch2(_ events: DispatchFileSystemEvents = .All,
+                       queue: DispatchQueue? = nil,
+                       delegate: DispatchFileSystemWatcherDelegate? = nil,
+                       callback: ((DispatchFileSystemWatcher) -> Void)? = nil
+        ) -> DispatchFileSystemWatcher {
+        let dispatchQueue: DispatchQueue
+        if #available(OSX 10.10, *) {
+            dispatchQueue = queue ?? DispatchQueue.global(qos: .default)
+        } else {
+            dispatchQueue = queue ?? DispatchQueue.global(priority: .default)
+        }
+        let watcher = DispatchFileSystemWatcher(path: self, events: events, queue: dispatchQueue, callback: callback)
+        watcher.delegate = delegate
+        watcher.startWatching()
+        return watcher
+    }
+
+    #else
+
+    // MARK: - Watching
+
+    /// Watches a path for filesystem events and handles them in the callback or delegate.
+    ///
+    /// - Parameter events: The create events.
+    /// - Parameter queue: The queue to be run within.
+    /// - Parameter delegate: The delegate to call when events happen.
+    /// - Parameter callback: The callback to be called on changes.
+    public func watch(_ events: DispatchFileSystemEvents = .All,
+                      queue: DispatchQueue = DispatchQueue.global(qos: .default),
+                      delegate: DispatchFileSystemWatcherDelegate? = nil,
+                      callback: ((DispatchFileSystemWatcher) -> Void)? = nil
+        ) -> DispatchFileSystemWatcher {
+        let watcher = DispatchFileSystemWatcher(path: self, events: events, queue: queue, callback: callback)
+        watcher.delegate = delegate
+        watcher.startWatching()
+        return watcher
+    }
+    #endif
+}

+ 277 - 0
Frameworks/FileKit/Sources/File.swift

@@ -0,0 +1,277 @@
+//
+//  File.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A representation of a filesystem file of a given data type.
+///
+/// - Precondition: The data type must conform to ReadableWritable.
+///
+/// All method do not follow links.
+open class File<DataType: ReadableWritable>: Comparable {
+
+    // MARK: - Properties
+
+    /// The file's filesystem path.
+    open var path: Path
+
+    /// The file's name.
+    open var name: String {
+        return path.fileName
+    }
+
+    /// The file's name without extension.
+    open var nameWithoutExtension: String {
+        return path.fileNameWithoutExtension
+    }
+
+    /// The file's filesystem path extension.
+    public final var pathExtension: String {
+        get {
+            return path.pathExtension
+        }
+        set {
+            path.pathExtension = newValue
+        }
+    }
+
+    /// True if the item exists and is a regular file.
+    ///
+    /// this method does not follow links.
+    open var exists: Bool {
+        return path.isRegular
+    }
+
+    /// The size of `self` in bytes.
+    open var size: UInt64? {
+        return path.fileSize
+    }
+
+    // MARK: - Initialization
+
+    /// Initializes a file from a path.
+    ///
+    /// - Parameter path: The path a file to initialize from.
+    public init(path: Path) {
+        self.path = path
+    }
+
+    // MARK: - Filesystem Operations
+
+    /// Reads the file and returns its data.
+    ///
+    /// - Throws: `FileKitError.ReadFromFileFail`
+    /// - Returns: The data read from file.
+    open func read() throws -> DataType {
+        return try DataType.read(from: path)
+    }
+
+    /// Writes data to the file.
+    ///
+    /// Writing is done atomically by default.
+    ///
+    /// - Parameter data: The data to be written to the file.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    open func write(_ data: DataType) throws {
+        try self.write(data, atomically: true)
+    }
+
+    /// Writes data to the file.
+    ///
+    /// - Parameter data: The data to be written to the file.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    open func write(_ data: DataType, atomically useAuxiliaryFile: Bool) throws {
+        try data.write(to: path, atomically: useAuxiliaryFile)
+    }
+
+    /// Creates the file.
+    ///
+    /// Throws an error if the file cannot be created.
+    ///
+    /// - Throws: `FileKitError.CreateFileFail`
+    ///
+    open func create() throws {
+        try path.createFile()
+    }
+
+    /// Deletes the file.
+    ///
+    /// Throws an error if the file could not be deleted.
+    ///
+    /// - Throws: `FileKitError.DeleteFileFail`
+    ///
+    open func delete() throws {
+        try path.deleteFile()
+    }
+
+    /// Moves the file to a path.
+    ///
+    /// Changes the path property to the given path.
+    ///
+    /// Throws an error if the file cannot be moved.
+    ///
+    /// - Parameter path: The path to move the file to.
+    /// - Throws: `FileKitError.MoveFileFail`
+    ///
+    open func move(to path: Path) throws {
+        try self.path.moveFile(to: path)
+        self.path = path
+    }
+
+    /// Copies the file to a path.
+    ///
+    /// Throws an error if the file could not be copied or if a file already
+    /// exists at the destination path.
+    ///
+    ///
+    /// - Parameter path: The path to copy the file to.
+    /// - Throws: `FileKitError.FileDoesNotExist`, `FileKitError.CopyFileFail`
+    ///
+    open func copy(to path: Path) throws {
+        try self.path.copyFile(to: path)
+    }
+
+    /// Symlinks the file to a path.
+    ///
+    /// If the path already exists and _is not_ a directory, an error will be
+    /// thrown and a link will not be created.
+    ///
+    /// If the path already exists and _is_ a directory, the link will be made
+    /// to `self` in that directory.
+    ///
+    ///
+    /// - Parameter path: The path to symlink the file to.
+    /// - Throws:
+    ///     `FileKitError.FileDoesNotExist`,
+    ///     `FileKitError.CreateSymlinkFail`
+    ///
+    open func symlink(to path: Path) throws {
+        try self.path.symlinkFile(to: path)
+    }
+
+    /// Hardlinks the file to a path.
+    ///
+    /// If the path already exists and _is not_ a directory, an error will be
+    /// thrown and a link will not be created.
+    ///
+    /// If the path already exists and _is_ a directory, the link will be made
+    /// to `self` in that directory.
+    ///
+    ///
+    /// - Parameter path: The path to hardlink the file to.
+    /// - Throws:
+    ///     `FileKitError.FileDoesNotExist`,
+    ///     `FileKitError.CreateHardlinkFail`
+    ///
+    open func hardlink(to path: Path) throws {
+        try self.path.hardlinkFile(to: path)
+    }
+
+    // MARK: - FileType
+
+    /// The FileType attribute for `self`.
+    open var type: FileType? {
+        return path.fileType
+    }
+
+    // MARK: - FilePermissions
+
+    /// The file permissions for `self`.
+    open var permissions: FilePermissions {
+        return FilePermissions(forFile: self)
+    }
+
+    // MARK: - FileHandle
+
+    /// Returns a file handle for reading from `self`, or `nil` if `self`
+    /// doesn't exist.
+    open var handleForReading: FileHandle? {
+        return path.fileHandleForReading
+    }
+
+    /// Returns a file handle for writing to `self`, or `nil` if `self` doesn't
+    /// exist.
+    open var handleForWriting: FileHandle? {
+        return path.fileHandleForWriting
+    }
+
+    /// Returns a file handle for reading from and writing to `self`, or `nil`
+    /// if `self` doesn't exist.
+    open var handleForUpdating: FileHandle? {
+        return path.fileHandleForUpdating
+    }
+
+    // MARK: - Stream
+
+    /// Returns an input stream that reads data from `self`, or `nil` if `self`
+    /// doesn't exist.
+    open func inputStream() -> InputStream? {
+        return path.inputStream()
+    }
+
+    /// Returns an input stream that writes data to `self`, or `nil` if `self`
+    /// doesn't exist.
+    ///
+    /// - Parameter shouldAppend: `true` if newly written data should be
+    ///                           appended to any existing file contents,
+    ///                           `false` otherwise. Default value is `false`.
+    ///
+    open func outputStream(append shouldAppend: Bool = false) -> OutputStream? {
+        return path.outputStream(append: shouldAppend)
+    }
+
+}
+
+extension File: CustomStringConvertible {
+
+    // MARK: - CustomStringConvertible
+
+    /// A textual representation of `self`.
+    public var description: String {
+        return String(describing: Swift.type(of: self)) + "('" + path.description + "')"
+    }
+
+}
+
+extension File: CustomDebugStringConvertible {
+
+    // MARK: - CustomDebugStringConvertible
+
+    /// A textual representation of `self`, suitable for debugging.
+    public var debugDescription: String {
+        return description
+    }
+
+}

+ 107 - 0
Frameworks/FileKit/Sources/FileKit.swift

@@ -0,0 +1,107 @@
+//
+//  FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+/// Information regarding [FileKit](https://github.com/nvzqz/FileKit).
+///
+/// - Author: [Nikolai Vazquez](https://github.com/nvzqz)
+///
+/// - Copyright: [MIT License](https://opensource.org/licenses/MIT)
+///
+/// - Version: [v5.0.0](https://github.com/nvzqz/FileKit/releases/tag/v4.0.0)
+///
+/// - Requires: Xcode 9, Swift 4.0
+///
+public enum FileKitInfo {
+
+    /// The current version.
+    ///
+    /// FileKit follows [Semantic Versioning v2.0.0](http://semver.org/).
+    public static let version = "v5.0.0"
+
+    /// The current release.
+    public static let release = 12
+
+    /// FileKit is licensed under the [MIT License](https://opensource.org/licenses/MIT).
+    public static let license = "MIT"
+
+    /// A brief description of FileKit.
+    public static let description = "A Swift framework that allows for simple and expressive file management."
+
+    /// Where the project can be found.
+    public static let projectURL = "https://github.com/nvzqz/FileKit"
+
+}
+
+import Foundation
+
+public struct FileKit {
+
+    /// Shared json decoder instance
+    public static var jsonDecoder = JSONDecoder()
+    /// Shared json encoder instance
+    public static var jsonEncoder = JSONEncoder()
+    /// Shared property list decoder instance
+    public static var propertyListDecoder = PropertyListDecoder()
+    /// Shared property list encoder instance
+    public static var propertyListEncoder = PropertyListEncoder()
+
+}
+
+extension FileKit {
+
+    /// Write an `Encodable` object to path
+    ///
+    /// - Parameter codable: The codable object to write.
+    /// - Parameter path: The destination path for write operation.
+    /// - Parameter encoder: A specific JSON encoder (default: FileKit.jsonEncoder).
+    ///
+    public static func write<T: Encodable>(_ codable: T, to path: Path, with encoder: JSONEncoder = FileKit.jsonEncoder) throws {
+        do {
+            let data = try encoder.encode(codable)
+            try DataFile(path: path).write(data)
+        } catch let error as FileKitError {
+            throw error
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+    /// Read an `Encodable` object from path
+    ///
+    /// - Parameter path: The destination path for write operation.
+    /// - Parameter decoder: A specific JSON decoder (default: FileKit.jsonDecoder).
+    ///
+    public static func read<T: Decodable>(from path: Path, with decoder: JSONDecoder = FileKit.jsonDecoder) throws -> T {
+        let data = try DataFile(path: path).read()
+        do {
+            return try decoder.decode(T.self, from: data)
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+}

+ 169 - 0
Frameworks/FileKit/Sources/FileKitError.swift

@@ -0,0 +1,169 @@
+//
+//  FileKitErrorType.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+// MARK: FileKitError
+
+/// An error that can be thrown by FileKit.
+public enum FileKitError: Error {
+
+    /// A file does not exist.
+    case fileDoesNotExist(path: Path)
+
+    /// A file already exists at operation destination.
+    case fileAlreadyExists(path: Path)
+
+    /// Could not change the current directory.
+    case changeDirectoryFail(from: Path, to: Path, error: Error)
+
+    /// A symbolic link could not be created.
+    case createSymlinkFail(from: Path, to: Path, error: Error)
+
+    /// A hard link could not be created.
+    case createHardlinkFail(from: Path, to: Path, error: Error)
+
+    /// A file could not be created.
+    case createFileFail(path: Path)
+
+    /// A directory could not be created.
+    case createDirectoryFail(path: Path, error: Error)
+
+    /// A file could not be deleted.
+    case deleteFileFail(path: Path, error: Error)
+
+    /// A file could not be read from.
+    case readFromFileFail(path: Path, error: Error)
+
+    /// A file could not be written to.
+    case writeToFileFail(path: Path, error: Error)
+
+    /// A file could not be moved.
+    case moveFileFail(from: Path, to: Path, error: Error)
+
+    /// A file could not be copied.
+    case copyFileFail(from: Path, to: Path, error: Error)
+
+    /// One or many attributes could not be changed.
+    case attributesChangeFail(path: Path, error: Error)
+
+    // MARK: - Reason
+
+    /// An error that could be cause of `FileKitError`
+    enum ReasonError: Error {
+        /// Failed to read or convert to specific type.
+        case conversion(Any)
+        /// A file stream/handle is alread closed.
+        case closed
+        /// Failed to encode string using specific encoding.
+        case encoding(String.Encoding, data: String)
+    }
+}
+
+// MARK: - Message
+extension FileKitError {
+
+    /// The reason for why the error occured.
+    public var message: String {
+        switch self {
+        case let .fileDoesNotExist(path):
+            return "File does not exist at \"\(path)\""
+        case let .fileAlreadyExists(path):
+            return "File already exists at \"\(path)\""
+        case let .changeDirectoryFail(fromPath, toPath, _):
+            return "Could not change the directory from \"\(fromPath)\" to \"\(toPath)\""
+        case let .createSymlinkFail(fromPath, toPath, _):
+            return "Could not create symlink from \"\(fromPath)\" to \"\(toPath)\""
+        case let .createHardlinkFail(fromPath, toPath, _):
+            return "Could not create a hard link from \"\(fromPath)\" to \"\(toPath)\""
+        case let .createFileFail(path):
+            return "Could not create file at \"\(path)\""
+        case let .createDirectoryFail(path, _):
+            return "Could not create a directory at \"\(path)\""
+        case let .deleteFileFail(path, _):
+            return "Could not delete file at \"\(path)\""
+        case let .readFromFileFail(path, _):
+            return "Could not read from file at \"\(path)\""
+        case let .writeToFileFail(path, _):
+            return "Could not write to file at \"\(path)\""
+        case let .moveFileFail(fromPath, toPath, _):
+            return "Could not move file at \"\(fromPath)\" to \"\(toPath)\""
+        case let .copyFileFail(fromPath, toPath, _):
+            return "Could not copy file from \"\(fromPath)\" to \"\(toPath)\""
+        case let .attributesChangeFail(path, _):
+            return "Could not change file attrubutes at \"\(path)\""
+        }
+    }
+}
+
+// MARK: - CustomStringConvertible
+extension FileKitError: CustomStringConvertible {
+
+    /// A textual representation of `self`.
+    public var description: String {
+        return String(describing: type(of: self)) + "(" + message + ")"
+    }
+
+}
+
+// MARK: - CustomDebugStringConvertible
+extension FileKitError: CustomDebugStringConvertible {
+
+    /// A textual representation of this instance, suitable for debugging.
+    public var debugDescription: String {
+        if let error = error {
+            return "\(self.description) \(error)"
+        }
+        return self.description
+    }
+
+}
+
+// MARK: - underlying error
+extension FileKitError {
+
+    /// Return the underlying error if any
+    public var error: Error? {
+        switch self {
+        case .changeDirectoryFail(_, _, let error),
+             .createSymlinkFail(_, _, let error),
+             .createHardlinkFail(_, _, let error),
+             .createDirectoryFail(_, let error),
+             .deleteFileFail(_, let error),
+             .readFromFileFail(_, let error),
+             .writeToFileFail(_, let error),
+             .moveFileFail(_, _, let error),
+             .copyFileFail(_, _, let error):
+            return error
+        case .fileDoesNotExist,
+             .fileAlreadyExists,
+             .createFileFail,
+             .attributesChangeFail:
+            return nil
+        }
+    }
+}

+ 93 - 0
Frameworks/FileKit/Sources/FilePermissions.swift

@@ -0,0 +1,93 @@
+//
+//  FilePermissions.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// The permissions of a file.
+public struct FilePermissions: OptionSet, CustomStringConvertible {
+
+    /// The file can be read from.
+    public static let read = FilePermissions(rawValue: 1)
+
+    /// The file can be written to.
+    public static let write = FilePermissions(rawValue: 2)
+
+    /// The file can be executed.
+    public static let execute = FilePermissions(rawValue: 4)
+
+    /// All FilePermissions
+    public static let all: [FilePermissions] =  [.read, .write, .execute]
+
+    /// The raw integer value of `self`.
+    public let rawValue: Int
+
+    /// A textual representation of `self`.
+    public var description: String {
+        var description = ""
+        for permission in FilePermissions.all {
+            if self.contains(permission) {
+                description += !description.isEmpty ? ", " : ""
+                if permission == .read {
+                    description += "Read"
+                } else if permission == .write {
+                    description += "Write"
+                } else if permission == .execute {
+                    description += "Execute"
+                }
+            }
+        }
+        return String(describing: type(of: self)) + "[" + description + "]"
+    }
+
+    /// Creates a set of file permissions.
+    ///
+    /// - Parameter rawValue: The raw value to initialize from.
+    ///
+    public init(rawValue: Int) {
+        self.rawValue = rawValue
+    }
+
+    /// Creates a set of permissions for the file at `path`.
+    ///
+    /// - Parameter path: The path to the file to create a set of persmissions for.
+    ///
+    public init(forPath path: Path) {
+        var permissions = FilePermissions(rawValue: 0)
+        if path.isReadable { permissions.formUnion(.read) }
+        if path.isWritable { permissions.formUnion(.write) }
+        if path.isExecutable { permissions.formUnion(.execute) }
+        self = permissions
+    }
+
+    /// Creates a set of permissions for `file`.
+    ///
+    /// - Parameter file: The file to create a set of persmissions for.
+    public init<DataType>(forFile file: File<DataType>) {
+        self.init(forPath: file.path)
+    }
+
+}

+ 179 - 0
Frameworks/FileKit/Sources/FileProtection.swift

@@ -0,0 +1,179 @@
+//
+//  FileProtection.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+#if os(iOS) || os(watchOS) || os(tvOS)
+
+/// The values that can be obtained from `FileAttributeKey.protectionKey` on a
+/// file's attributes. Only available on iOS, watchOS, and tvOS.
+public enum FileProtection: String {
+
+    /// The file has no special protections associated with it.
+    case none
+
+    /// The file is stored in an encrypted format on disk and cannot be read
+    /// from or written to while the device is locked or booting.
+    case complete
+
+    /// The file is stored in an encrypted format on disk. Files can be created
+    /// while the device is locked, but once closed, cannot be opened again
+    /// until the device is unlocked.
+    case completeUnlessOpen
+
+    /// The file is stored in an encrypted format on disk and cannot be accessed
+    /// until after the device has booted.
+    case completeUntilFirstUserAuthentication
+
+    /// Initializes `self` from a file protection value.
+    ///
+    /// - Parameter rawValue: The raw value to initialize from.
+    ///
+    public init?(rawValue: String) {
+        switch rawValue {
+        case FileProtectionType.none.rawValue:
+            self = .none
+        case FileProtectionType.complete.rawValue:
+            self = .complete
+        case FileProtectionType.completeUnlessOpen.rawValue:
+            self = .completeUnlessOpen
+        case FileProtectionType.completeUntilFirstUserAuthentication.rawValue:
+            self = .completeUntilFirstUserAuthentication
+        default:
+            return nil
+        }
+    }
+
+    /// The file protection string value of `self`.
+    public var rawValue: String {
+        switch self {
+        case .none:
+            return FileProtectionType.none.rawValue
+        case .complete:
+            return FileProtectionType.complete.rawValue
+        case .completeUnlessOpen:
+            return FileProtectionType.completeUnlessOpen.rawValue
+        case .completeUntilFirstUserAuthentication:
+            return FileProtectionType.completeUntilFirstUserAuthentication.rawValue
+        }
+    }
+
+    ///  Return the equivalent Data.WritingOptions
+    public var dataWritingOption: NSData.WritingOptions {
+        switch self {
+        case .none:
+            return .noFileProtection
+        case .complete:
+            return .completeFileProtection
+        case .completeUnlessOpen:
+            return .completeFileProtectionUnlessOpen
+        case .completeUntilFirstUserAuthentication:
+            return .completeFileProtectionUntilFirstUserAuthentication
+        }
+    }
+
+}
+
+extension Path {
+
+    // MARK: File Protection
+
+    /// The protection of the file at the path.
+    public var fileProtection: FileProtection? {
+        guard let value = attributes[FileAttributeKey.protectionKey] as? String,
+            let protection  = FileProtection(rawValue: value) else {
+            return nil
+        }
+        return protection
+    }
+
+    /// Creates a file at path with specified file protection.
+    ///
+    /// - Parameter fileProtection: the protection to apply to the file.
+    ///
+    /// Throws an error if the file cannot be created.
+    ///
+    /// - Throws: `FileKitError.CreateFileFail`
+    ///
+    public func createFile(_ fileProtection: FileProtection) throws {
+        let manager = FileManager()
+        let attributes: [FileAttributeKey: Any] = [.protectionKey: fileProtection] // todo test
+
+        if !manager.createFile(atPath: _safeRawValue, contents: nil, attributes: attributes) {
+            throw FileKitError.createFileFail(path: self)
+        }
+    }
+
+}
+
+extension File {
+
+    // MARK: File Protection
+
+    /// The protection of `self`.
+    public var protection: FileProtection? {
+        return path.fileProtection
+    }
+
+    /// Creates the file with specified file protection.
+    ///
+    /// - Parameter fileProtection: the protection to apply to the file.
+    ///
+    /// Throws an error if the file cannot be created.
+    ///
+    /// - Throws: `FileKitError.CreateFileFail`
+    ///
+    public func create(_ fileProtection: FileProtection) throws {
+        try path.createFile(fileProtection)
+    }
+
+}
+
+extension File where DataType: NSData {
+
+    /// Writes data to the file.
+    ///
+    /// - Parameter data: The data to be written to the file.
+    /// - Parameter fileProtection: the protection to apply to the file.
+    /// - Parameter atomically: If `true`, the data is written to an
+    ///                         auxiliary file that is then renamed to the
+    ///                         file. If `false`, the data is written to
+    ///                         the file directly.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    public func write(_ data: DataType, fileProtection: FileProtection, atomically: Bool = true) throws {
+        var options = fileProtection.dataWritingOption
+        if atomically {
+            options.formUnion(Foundation.Data.WritingOptions.atomic)
+        }
+        try self.write(data, options: options)
+    }
+
+}
+
+#endif

+ 341 - 0
Frameworks/FileKit/Sources/FileSystemEvent.swift

@@ -0,0 +1,341 @@
+//
+//  FileSystemEvent.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+#if os(OSX)
+
+/// A filesystem event.
+public struct FileSystemEvent {
+
+    // MARK: - Static Properties
+
+    /// All of the event IDs.
+    public static let allEventId = 0
+
+    /// The last event ID since now.
+    public static let nowEventId = FSEventStreamEventId(kFSEventStreamEventIdSinceNow)
+
+    // MARK: - Properties
+
+    /// The ID of the event.
+    public var id: FSEventStreamEventId // swiftlint:disable:this variable_name
+
+    /// The path for the event.
+    public var path: Path
+
+    /// The flags of the event.
+    public var flags: FileSystemEventFlags
+}
+
+extension Path {
+
+    // MARK: - Watching
+
+    /// Watches a path for filesystem events and handles them in the callback.
+    ///
+    /// - Parameter latency: The latency in seconds.
+    /// - Parameter queue: The queue to be run within.
+    /// - Parameter callback: The callback to handle events.
+    /// - Returns: The `FileSystemWatcher` object.
+    public func watch(_ latency: TimeInterval = 0, queue: DispatchQueue = DispatchQueue.main, callback: @escaping (FileSystemEvent) -> Void) -> FileSystemWatcher {
+        let watcher = FileSystemWatcher(paths: [self], latency: latency, queue: queue, callback: callback)
+        watcher.watch()
+        return watcher
+    }
+}
+
+extension Sequence where Self.Iterator.Element == Path {
+
+    // MARK: - Watching
+
+    /// Watches the sequence of paths for filesystem events and handles them in
+    /// the callback.
+    ///
+    /// - Parameter latency: The latency in seconds.
+    /// - Parameter queue: The queue to be run within.
+    /// - Parameter callback: The callback to handle events.
+    /// - Returns: The `FileSystemWatcher` object.
+    public func watch(_ latency: TimeInterval = 0, queue: DispatchQueue = DispatchQueue.main, callback: @escaping (FileSystemEvent) -> Void) -> FileSystemWatcher {
+        let watcher = FileSystemWatcher(paths: Array(self), latency: latency, queue: queue, callback: callback)
+        watcher.watch()
+        return watcher
+    }
+
+}
+
+/// A set of fileystem event flags.
+public struct FileSystemEventFlags: OptionSet, CustomStringConvertible, CustomDebugStringConvertible {
+
+    // MARK: - Options
+
+    /// There was some change in the directory at the specific path supplied in
+    /// this event.
+    public static let None = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagNone)
+
+    /// Your application must rescan not just the directory given in the event,
+    /// but all its children, recursively.
+    public static let MustScanSubDirs = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagMustScanSubDirs)
+
+    /// May be set in addition to `MustScanSubDirs` indicate that a problem
+    /// occurred in buffering the events (the particular flag set indicates
+    /// where the problem occurred) and that the client must do a full scan of
+    /// any directories (and their subdirectories, recursively) being monitored
+    /// by this stream.
+    public static let UserDropped = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagUserDropped)
+
+    /// May be set in addition to `MustScanSubDirs` indicate that a problem
+    /// occurred in buffering the events (the particular flag set indicates
+    /// where the problem occurred) and that the client must do a full scan of
+    /// any directories (and their subdirectories, recursively) being monitored
+    /// by this stream.
+    public static let KernelDropped = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagKernelDropped)
+
+    /// The 64-bit event ID counter wrapped around.
+    public static let EventIdsWrapped = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagEventIdsWrapped)
+
+    /// Denotes a sentinel event sent to mark the end of the "historical" events
+    /// sent as a result of specifying a `sinceWhen` value in the
+    /// FSEventStreamCreate...() call that created this event stream.
+    public static let HistoryDone = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagHistoryDone)
+
+    /// Denotes a special event sent when there is a change to one of the
+    /// directories along the path to one of the directories asked to watch.
+    public static let RootChanged = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagRootChanged)
+
+    /// Denotes a special event sent when a volume is mounted underneath one of
+    /// the paths being monitored.
+    public static let Mount = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagMount)
+
+    /// Denotes a special event sent when a volume is unmounted underneath one
+    /// of the paths being monitored.
+    public static let Unmount = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagUnmount)
+
+    /// A file system object was created at the specific path supplied in this
+    /// event.
+    public static let Created = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemCreated)
+
+    /// A file system object was removed at the specific path supplied in this
+    /// event.
+    public static let ItemRemoved = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemRemoved)
+
+    /// A file system object at the specific path supplied in this event had its
+    /// metadata modified.
+    public static let ItemInodeMetaMod = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemInodeMetaMod)
+
+    /// A file system object was renamed at the specific path supplied in this
+    /// event.
+    public static let ItemRenamed = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemRenamed)
+
+    /// A file system object at the specific path supplied in this event had its
+    /// data modified.
+    public static let ItemModified = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemModified)
+
+    /// A file system object at the specific path supplied in this event had its
+    /// FinderInfo data modified.
+    public static let ItemFinderInfoMod = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemFinderInfoMod)
+
+    /// A file system object at the specific path supplied in this event had its
+    /// ownership changed.
+    public static let ItemChangeOwner = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemChangeOwner)
+
+    /// A file system object at the specific path supplied in this event had its
+    /// extended attributes modified.
+    public static let ItemXattrMod = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemXattrMod)
+
+    /// The file system object at the specific path supplied in this event is a
+    /// regular file.
+    public static let ItemIsFile = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemIsFile)
+
+    /// The file system object at the specific path supplied in this event is a
+    /// directory.
+    public static let ItemIsDir = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemIsDir)
+
+    /// The file system object at the specific path supplied in this event is a
+    /// symbolic link.
+    public static let ItemIsSymlink = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemIsSymlink)
+
+    /// Indicates the event was triggered by the current process.
+    public static let OwnEvent = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagOwnEvent)
+
+    /// Flag for if the item is a hardlink.
+    @available(iOS 9, OSX 10.10, *)
+    public static let ItemIsHardlink = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemIsHardlink)
+
+    /// Flag for if the item was the last hardlink.
+    @available(iOS 9, OSX 10.10, *)
+    public static let ItemIsLastHardlink = FileSystemEventFlags(rawValue: kFSEventStreamEventFlagItemIsLastHardlink)
+
+    // MARK: - All Flags
+
+    /// An array of all of the flags.
+    public static var allFlags: [FileSystemEventFlags] = {
+        var array: [FileSystemEventFlags] = [ // swiftlint:disable comma
+            .None,              .MustScanSubDirs,       .UserDropped,
+            .KernelDropped,     .EventIdsWrapped,       .HistoryDone,
+            .RootChanged,       .Mount,                 .Unmount,
+            .ItemRemoved,       .ItemInodeMetaMod,      .ItemRenamed,
+            .ItemModified,      .ItemFinderInfoMod,     .ItemChangeOwner,
+            .ItemXattrMod,      .ItemIsFile,            .ItemIsDir,
+            .ItemIsSymlink,     .OwnEvent
+        ] // swiftlint:enable comma
+        if #available(iOS 9, OSX 10.10, *) {
+            array += [.ItemIsHardlink, .ItemIsLastHardlink ]
+        }
+        return array
+    }()
+
+    /// The names of all of the flags.
+    public static let allFlagNames: [String] = {
+        var array: [String] = [ // swiftlint:disable comma
+            "None",             "MustScanSubDirs",      "UserDropped",
+            "KernelDropped",    "EventIdsWrapped",      "HistoryDone",
+            "RootChanged",      "Mount",                "Unmount",
+            "ItemRemoved",      "ItemInodeMetaMod",     "ItemRenamed",
+            "ItemModified",     "ItemFinderInfoMod",    "ItemChangeOwner",
+            "ItemXattrMod",     "ItemIsFile",           "ItemIsDir",
+            "ItemIsSymlink",    "OwnEvent"
+        ] // swiftlint:enable comma
+        if #available(iOS 9, OSX 10.10, *) {
+            array += ["ItemIsHardlink", "ItemIsLastHardlink"]
+        }
+        return array
+    }()
+
+    // MARK: - Properties
+
+    /// The raw event stream flag values.
+    public let rawValue: Int
+
+    /// A textual representation of `self`.
+    public var description: String {
+        var result = ""
+        for (index, element) in FileSystemEventFlags.allFlags.enumerated() {
+            if self.contains(element) {
+                let name = FileSystemEventFlags.allFlagNames[index]
+                result += result.isEmpty ? "\(name)": ", \(name)"
+            }
+        }
+        return String(describing: type(of: self)) + "[\(result)]"
+    }
+
+    /// A textual representation of `self`, suitable for debugging.
+    public var debugDescription: String {
+        var result = ""
+        for (index, element) in FileSystemEventFlags.allFlags.enumerated() {
+            if self.contains(element) {
+                let name = FileSystemEventFlags.allFlagNames[index] + "(\(element.rawValue))"
+                result += result.isEmpty ? "\(name)": ", \(name)"
+            }
+        }
+        return String(describing: type(of: self)) + "[\(result)]"
+    }
+
+    // MARK: - Initialization
+
+    /// Creates a set of event stream flags from a raw value.
+    ///
+    /// - Parameter rawValue: The raw value to initialize from.
+    public init(rawValue: Int) { self.rawValue = rawValue }
+
+}
+
+/// Flags for creating an event stream.
+public struct FileSystemEventStreamCreateFlags: OptionSet, CustomStringConvertible, CustomDebugStringConvertible {
+
+    // MARK: - Options
+
+    /// The default.
+    public static let None = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagNone)
+
+    /// The callback function will be invoked with CF types rather than raw C
+    /// types.
+    public static let UseCFTypes = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagUseCFTypes)
+
+    /// Affects the meaning of the latency parameter.
+    public static let FlagNoDefer = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagNoDefer)
+
+    /// Request notifications of changes along the path to the path(s) watched.
+    public static let WatchRoot = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagWatchRoot)
+
+    /// Don't send events that were triggered by the current process.
+    public static let IgnoreSelf = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagIgnoreSelf)
+
+    /// Request file-level notifications.
+    public static let FileEvents = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagFileEvents)
+
+    /// Tag events that were triggered by the current process with the
+    /// `OwnEvent` flag.
+    public static let MarkSelf = FileSystemEventStreamCreateFlags(rawValue: kFSEventStreamCreateFlagMarkSelf)
+
+    // MARK: - All Flags
+
+    /// All of the event stream creation flags.
+    public static let allFlags: [FileSystemEventStreamCreateFlags] = [.None, .UseCFTypes, .FlagNoDefer, .WatchRoot, .IgnoreSelf, .FileEvents, .MarkSelf]
+
+    /// All of the names of the event stream creation flags.
+    public static let allFlagNames: [String] = ["None", "UseCFTypes", "FlagNoDefer", "WatchRoot", "IgnoreSelf", "FileEvents", "MarkSelf" ]
+
+    // MARK: - Properties
+
+    /// The raw event stream creation flags.
+    public let rawValue: Int
+
+    /// A textual representation of `self`.
+    public var description: String {
+        var result = ""
+        for (index, element) in FileSystemEventStreamCreateFlags.allFlags.enumerated() {
+            if self.contains(element) {
+                let name = FileSystemEventStreamCreateFlags.allFlagNames[index]
+                result += result.isEmpty ? "\(name)": ", \(name)"
+            }
+        }
+        return String(describing: type(of: self)) + "[\(result)]"
+    }
+
+    /// A textual representation of `self`, suitable for debugging.
+    public var debugDescription: String {
+        var result = ""
+        for (index, element) in FileSystemEventStreamCreateFlags.allFlags.enumerated() {
+            if self.contains(element) {
+                let name = FileSystemEventStreamCreateFlags.allFlagNames[index] + "(\(element.rawValue))"
+                result += result.isEmpty ? "\(name)": ", \(name)"
+            }
+        }
+        return String(describing: type(of: self)) + "[\(result)]"
+    }
+
+    // MARK: - Initialization
+
+    /// Creates a set of event stream creation flags from a raw value.
+    ///
+    /// - Parameter rawValue: The raw value to initialize from.
+    public init(rawValue: Int) { self.rawValue = rawValue }
+
+}
+
+#endif

+ 115 - 0
Frameworks/FileKit/Sources/FileSystemEventStream.swift

@@ -0,0 +1,115 @@
+//
+//  FileSystemEvent.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+#if os(OSX)
+
+/// A filesystem event stream.
+internal struct FileSystemEventStream: RawRepresentable {
+
+    /// The raw FSEventStreamRef value of `self`.
+    var rawValue: FSEventStreamRef
+
+    /// Schedules the stream on the specified run loop.
+    ///
+    /// - Parameter runLoop: The run loop.
+    /// - Parameter runLoopMode: The run loop mode.
+    func scheduleWithRunLoop(_ runLoop: CFRunLoop, runLoopMode: CFRunLoopMode) {
+        FSEventStreamScheduleWithRunLoop(rawValue, runLoop, runLoopMode.rawValue)
+    }
+
+    /// Invalidates the stream.
+    func invalidate() {
+        FSEventStreamInvalidate(rawValue)
+    }
+
+    /// Registers the stream.
+    func start() {
+        FSEventStreamStart(rawValue)
+    }
+
+    /// Unregisters the stream.
+    func stop() {
+        FSEventStreamStop(rawValue)
+    }
+
+    /// Removes the stream from the specified run loop.
+    ///
+    /// - Parameter runLoop: The run loop.
+    /// - Parameter runLoopMode: The run loop mode.
+    func unscheduleFromRunLoop(_ runLoop: CFRunLoop, runLoopMode: CFString) {
+        FSEventStreamUnscheduleFromRunLoop(rawValue, runLoop, runLoopMode)
+    }
+
+    /// Schedules the stream on the specified dispatch queue
+    ///
+    /// - Parameter queue: The queue to be run within.
+    func setDispatchQueue(_ queue: DispatchQueue) {
+        FSEventStreamSetDispatchQueue(rawValue, queue)
+    }
+
+    /// Decrements the FSEventStreamRef's refcount.
+    func release() {
+        FSEventStreamRelease(rawValue)
+    }
+
+    /// Asks the FS Events service to flush out any events that have occurred
+    /// but have not yet been delivered, due to the latency parameter that was
+    /// supplied when the stream was created. This flushing occurs
+    /// asynchronously.
+    func flushAsync() {
+        FSEventStreamFlushAsync(rawValue)
+    }
+
+    /// Asks the FS Events service to flush out any events that have occurred
+    /// but have not yet been delivered, due to the latency parameter that was
+    /// supplied when the stream was created. This flushing occurs
+    /// synchronously.
+    func flushSync() {
+        FSEventStreamFlushSync(rawValue)
+    }
+
+    /// Prints a description of the stream to stderr.
+    func show() {
+        FSEventStreamShow(rawValue)
+    }
+
+    /// The dev_t for a device-relative stream, otherwise 0.
+    ///
+    /// - Returns: The dev_t for a device-relative stream or 0.
+    func deviceBeingWatched() -> dev_t {
+        return FSEventStreamGetDeviceBeingWatched(rawValue)
+    }
+
+    /// The sinceWhen attribute of the stream.
+    var lastEventId: FSEventStreamEventId {
+        return FSEventStreamGetLatestEventId(rawValue)
+    }
+}
+
+#endif

+ 229 - 0
Frameworks/FileKit/Sources/FileSystemWatcher.swift

@@ -0,0 +1,229 @@
+//
+//  FileSystemEvent.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+#if os(OSX)
+
+/// Watches a given set of paths and runs a callback per event.
+public class FileSystemWatcher {
+
+    // MARK: - Private Static Properties
+
+    /// The event stream callback for when events occur.
+    private static let _eventCallback: FSEventStreamCallback = { // swiftlint:disable all
+        (stream: ConstFSEventStreamRef,
+         contextInfo: UnsafeMutableRawPointer?,
+         numEvents: Int,
+         eventPaths: UnsafeMutableRawPointer,
+         eventFlags: UnsafePointer<FSEventStreamEventFlags>,
+         eventIds: UnsafePointer<FSEventStreamEventId>) in  // swiftlint:enable all
+
+        FileSystemWatcher.log("Callback Fired")
+
+        let watcher: FileSystemWatcher = unsafeBitCast(contextInfo, to: FileSystemWatcher.self)
+
+        defer {
+            watcher.lastEventId = eventIds[numEvents - 1]
+        }
+
+        guard let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] else {
+            return
+        }
+
+        for index in 0..<numEvents {
+            let id = eventIds[index]
+            let path = paths[index]
+            let flags = eventFlags[index]
+
+            let event = FileSystemEvent(
+                id: id,
+                path: Path(path),
+                flags: FileSystemEventFlags(rawValue: Int(flags)))
+            watcher._processEvent(event)
+        }
+    }
+
+    // MARK: - Properties
+
+    /// The paths being watched.
+    public let paths: [Path]
+
+    /// How often the watcher updates.
+    public let latency: CFTimeInterval
+
+    /// The queue for the watcher.
+    public let queue: DispatchQueue?
+
+    /// The flags used to create the watcher.
+    public let flags: FileSystemEventStreamCreateFlags
+
+    /// The run loop mode for the watcher.
+    public var runLoopMode: CFRunLoopMode = CFRunLoopMode.defaultMode
+
+    /// The run loop for the watcher.
+    public var runLoop: CFRunLoop = CFRunLoopGetMain()
+
+    /// The callback for filesystem events.
+    private let _callback: (FileSystemEvent) -> Void
+
+    /// The last event ID for the watcher.
+    public private(set) var lastEventId: FSEventStreamEventId
+
+    /// Whether or not the watcher has started yet.
+    private var _started = false
+
+    /// The event stream for the watcher.
+    private var _stream: FileSystemEventStream?
+
+    // MARK: - Initialization
+
+    /// Creates a watcher for the given paths.
+    ///
+    /// - Parameter paths: The paths.
+    /// - Parameter sinceWhen: The date to start at.
+    /// - Parameter flags: The create flags.
+    /// - Parameter latency: The latency.
+    /// - Parameter queue: The queue to be run within.
+    /// - Parameter callback: The callback to be called on changes.
+    public init(paths: [Path],
+                sinceWhen: FSEventStreamEventId = FileSystemEvent.nowEventId,
+                flags: FileSystemEventStreamCreateFlags = [.UseCFTypes, .FileEvents],
+                latency: CFTimeInterval = 0,
+                queue: DispatchQueue? = nil,
+                callback: @escaping (FileSystemEvent) -> Void
+        ) {
+        self.lastEventId = sinceWhen
+        self.paths       = paths
+        self.flags       = flags
+        self.latency     = latency
+        self.queue       = queue
+        self._callback   = callback
+    }
+
+    // MARK: - Deinitialization
+
+    deinit {
+        self.close()
+    }
+
+    // MARK: - Private Methods
+
+    /// Processes the event by logging it and then running the callback.
+    ///
+    /// - Parameter event: The file system event to be logged.
+    private func _processEvent(_ event: FileSystemEvent) {
+        FileSystemWatcher.log("\t\(event.id) - \(event.flags) - \(event.path)")
+        self._callback(event)
+    }
+
+    /// Prints the message when in debug mode.
+    ///
+    /// - Parameter message: The message to be logged.
+    private static func log(_ message: String) {
+        #if DEBUG
+            print(message)
+        #endif
+    }
+
+    // MARK: - Methods
+
+    // Start watching by creating the stream
+    /// Starts watching.
+    public func watch() {
+        guard _started == false else { return }
+
+        var context = FSEventStreamContext(
+            version: 0,
+            info: nil,
+            retain: nil,
+            release: nil,
+            copyDescription: nil
+        )
+        // add self into context
+        context.info = Unmanaged.passUnretained(self).toOpaque()
+
+        guard let streamRef = FSEventStreamCreate(
+            kCFAllocatorDefault,
+            FileSystemWatcher._eventCallback,
+            &context,
+            paths.map {$0.rawValue} as CFArray,
+            // since when
+            lastEventId,
+            // how long to wait after an event occurs before forwarding it
+            latency,
+            UInt32(flags.rawValue)
+            ) else {
+                return
+        }
+        _stream = FileSystemEventStream(rawValue: streamRef)
+
+        _stream?.scheduleWithRunLoop(runLoop, runLoopMode: runLoopMode)
+        if let q = queue {
+            _stream?.setDispatchQueue(q)
+        }
+        _stream?.start()
+
+        _started = true
+    }
+
+    // Stops, invalidates and releases the stream
+    /// Closes the watcher.
+    public func close() {
+        guard _started == true else { return }
+
+        _stream?.stop()
+        _stream?.invalidate()
+        _stream?.release()
+        _stream = nil
+
+        _started = false
+    }
+
+    /// Requests that the fseventsd daemon send any events it has already
+    /// buffered (via the latency parameter).
+    ///
+    /// This occurs asynchronously; clients will not have received all the
+    /// callbacks by the time this call returns to them.
+    public func flushAsync() {
+        _stream?.flushAsync()
+    }
+
+    /// Requests that the fseventsd daemon send any events it has already
+    /// buffered (via the latency). Then runs the runloop in its private mode
+    /// till all events that have occurred have been reported (via the client's
+    /// callback).
+    ///
+    /// This occurs synchronously; clients will have received all the callbacks
+    /// by the time this call returns to them.
+    public func flushSync() {
+        _stream?.flushSync()
+    }
+
+}
+
+#endif

+ 98 - 0
Frameworks/FileKit/Sources/FileType.swift

@@ -0,0 +1,98 @@
+//
+//  FileType.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// The type attribute for a file at a path.
+public enum FileType: String {
+
+    /// The file is a directory.
+    case directory
+
+    /// The file is a regular file.
+    case regular
+
+    /// The file is a symbolic link.
+    case symbolicLink
+
+    /// The file is a socket.
+    case socket
+
+    /// The file is a characer special file.
+    case characterSpecial
+
+    /// The file is a block special file.
+    case blockSpecial
+
+    /// The type of the file is unknown.
+    case unknown
+
+    /// Creates a FileType from an `FileAttributeType` attribute.
+    ///
+    /// - Parameter rawValue: The raw value to create from.
+    public init?(rawValue: String) {
+        switch rawValue {
+        case FileAttributeType.typeDirectory.rawValue:
+            self = .directory
+        case FileAttributeType.typeRegular.rawValue:
+            self = .regular
+        case FileAttributeType.typeSymbolicLink.rawValue:
+            self = .symbolicLink
+        case FileAttributeType.typeSocket.rawValue:
+            self = .socket
+        case FileAttributeType.typeCharacterSpecial.rawValue:
+            self = .characterSpecial
+        case FileAttributeType.typeBlockSpecial.rawValue:
+            self = .blockSpecial
+        case FileAttributeType.typeUnknown.rawValue:
+            self = .unknown
+        default:
+            return nil
+        }
+    }
+
+    /// The `FileAttributeType` attribute for `self`.
+    public var rawValue: String {
+        switch self {
+        case .directory:
+            return FileAttributeType.typeDirectory.rawValue
+        case .regular:
+            return FileAttributeType.typeRegular.rawValue
+        case .symbolicLink:
+            return FileAttributeType.typeSymbolicLink.rawValue
+        case .socket:
+            return FileAttributeType.typeSocket.rawValue
+        case .characterSpecial:
+            return FileAttributeType.typeCharacterSpecial.rawValue
+        case .blockSpecial:
+            return FileAttributeType.typeBlockSpecial.rawValue
+        case .unknown:
+            return FileAttributeType.typeUnknown.rawValue
+        }
+    }
+
+}

+ 92 - 0
Frameworks/FileKit/Sources/Image+FileKit.swift

@@ -0,0 +1,92 @@
+//
+//  Image.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+#if os(OSX)
+import Cocoa
+#elseif os(iOS) || os(tvOS)
+import UIKit
+#elseif os(watchOS)
+import WatchKit
+#endif
+
+#if os(OSX)
+/// The image type for the current platform.
+public typealias Image = NSImage
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+/// The image type for the current platform.
+public typealias Image = UIImage
+#endif
+
+#if os(OSX) || os(iOS) || os(tvOS) || os(watchOS)
+
+extension Image: ReadableWritable, WritableConvertible {
+
+    /// Returns an image from the given path.
+    ///
+    /// - Parameter path: The path to be returned the image for.
+    /// - Throws: FileKitError.ReadFromFileFail
+    ///
+    public class func read(from path: Path) throws -> Self {
+        guard let contents = self.init(contentsOfFile: path._safeRawValue) else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(Image.self))
+        }
+        return contents
+    }
+
+    /// Returns `TIFFRepresentation` on OS X and `UIImagePNGRepresentation` on
+    /// iOS, watchOS, and tvOS.
+    public var writable: Data {
+        #if os(OSX)
+        return self.tiffRepresentation ?? Data()
+        #else
+        return self.pngData() ?? Data()
+        #endif
+    }
+
+    /// Retrieves an image from a URL.
+    public convenience init?(url: URL) {
+        #if os(OSX)
+            self.init(contentsOf: url)
+        #else
+            guard let data = try? Data(contentsOf: url) else {
+                return nil
+            }
+            self.init(data: data)
+        #endif
+    }
+
+    /// Retrieves an image from a URL string.
+    public convenience init?(urlString string: String) {
+        guard let url = URL(string: string) else {
+            return nil
+        }
+        self.init(url: url)
+    }
+
+}
+
+#endif

+ 36 - 0
Frameworks/FileKit/Sources/ImageFile.swift

@@ -0,0 +1,36 @@
+//
+//  ImageFile.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
+/// A representation of a filesystem image file.
+///
+/// The data type is Image.
+public typealias ImageFile = File<Image>
+
+#endif

+ 66 - 0
Frameworks/FileKit/Sources/JSONType.swift

@@ -0,0 +1,66 @@
+//
+//  JSONFile.swift
+//  FileKit
+//
+//  Created by phimage on 30/09/2017.
+//  Copyright © 2017 Nikolai Vazquez. All rights reserved.
+//
+
+import Foundation
+
+public typealias JSONReadableWritable = JSONReadable & JSONWritable
+
+// MARK: JSONReadable
+
+/// A JSON readable object is `Decodable`and provide it`s own decoder
+public protocol JSONReadable: Readable, Decodable {
+    static var jsonDecoder: JSONDecoder { get }
+}
+extension JSONReadable {
+    // default implementation return the shared one
+    public static var jsonDecoder: JSONDecoder {
+        return FileKit.jsonDecoder
+    }
+}
+// Implement Readable
+extension JSONReadable {
+
+    /// Read a Decodable object
+    public static func read(from path: Path) throws -> Self {
+        let data = try DataFile(path: path).read()
+        do {
+            return try jsonDecoder.decode(self, from: data)
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+}
+
+// MARK: JSONWritable
+
+/// A JSON readable object is `Decodable`and provide it`s own decoder
+public protocol JSONWritable: Writable, Encodable {
+    var jsonEncoder: JSONEncoder { get }
+}
+extension JSONWritable {
+    // default implementation return the shared one
+    public var jsonEncoder: JSONEncoder {
+        return FileKit.jsonEncoder
+    }
+}
+// Implement Writable
+extension JSONWritable {
+
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        do {
+            let data = try jsonEncoder.encode(self)
+            try DataFile(path: path).write(data, atomically: useAuxiliaryFile)
+        } catch let error as FileKitError {
+            throw error
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+}

+ 42 - 0
Frameworks/FileKit/Sources/NSArray+FileKit.swift

@@ -0,0 +1,42 @@
+//
+//  NSArray+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension NSArray: ReadableWritable, WritableToFile {
+
+    /// Returns an array read from the given path.
+    ///
+    /// - Parameter path: The path an array to be read from.
+    public class func read(from path: Path) throws -> Self {
+        guard let contents = self.init(contentsOfFile: path._safeRawValue) else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(NSArray.self))
+        }
+        return contents
+    }
+
+}

+ 49 - 0
Frameworks/FileKit/Sources/NSData+FileKit.swift

@@ -0,0 +1,49 @@
+//
+//  Data+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension NSData: ReadableWritable, WritableToFile {
+
+    /// Returns data read from the given path.
+    public class func read(from path: Path) throws -> Self {
+        guard let contents = self.init(contentsOfFile: path._safeRawValue) else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(NSData.self))
+        }
+        return contents
+    }
+
+    /// Returns data read from the given path using Data.ReadingOptions.
+    public class func read(from path: Path, options: NSData.ReadingOptions) throws -> Self {
+        do {
+            return try self.init(contentsOfFile: path._safeRawValue, options: options)
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+}

+ 63 - 0
Frameworks/FileKit/Sources/NSDataFile.swift

@@ -0,0 +1,63 @@
+//
+//  DataFile.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A representation of a filesystem data file.
+///
+/// The data type is NSData.
+public typealias NSDataFile = File<NSData>
+
+extension File where DataType: NSData {
+
+    /// Reads the file and returns its data.
+    /// - Parameter options: A mask that specifies write options
+    ///                      described in `NSData.ReadingOptions`.
+    ///
+    /// - Throws: `FileKitError.ReadFromFileFail`
+    /// - Returns: The data read from file.
+    public func read(_ options: NSData.ReadingOptions) throws -> NSData {
+        return try NSData.read(from: path, options: options)
+    }
+
+    /// Writes data to the file.
+    ///
+    /// - Parameter data: The data to be written to the file.
+    /// - Parameter options: A mask that specifies write options
+    ///                      described in `NSData.WritingOptions`.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    public func write(_ data: NSData, options: NSData.WritingOptions) throws {
+        do {
+            try data.write(toFile: self.path._safeRawValue, options: options)
+        } catch {
+            throw FileKitError.writeToFileFail(path: self.path, error: error)
+        }
+    }
+
+}

+ 37 - 0
Frameworks/FileKit/Sources/NSDictionary+FileKit.swift

@@ -0,0 +1,37 @@
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension NSDictionary: ReadableWritable, WritableToFile {
+
+    /// Returns a dictionary read from the given path.
+    public class func read(from path: Path) throws -> Self {
+        guard let contents = self.init(contentsOfFile: path._safeRawValue) else {
+            throw FileKitError.readFromFileFail(path: path, error: FileKitError.ReasonError.conversion(NSDictionary.self))
+        }
+        return contents
+    }
+
+}

+ 83 - 0
Frameworks/FileKit/Sources/NSString+FileKit.swift

@@ -0,0 +1,83 @@
+//
+//  NSString+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension NSString {
+
+    /// Returns an String object initialized by copying the characters from
+    /// the raw value of a given path.
+    public convenience init(path: Path) {
+        self.init(string: path.rawValue)
+    }
+
+}
+
+extension NSString: Writable {
+    /// Writes the string to a path atomically.
+    ///
+    /// - Parameter path: The path being written to.
+    ///
+    public func write(to path: Path) throws {
+        try write(to: path, atomically: true)
+    }
+
+    /// Writes the string to a path with `NSUTF8StringEncoding` encoding.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        do {
+            try self.write(toFile: path._safeRawValue,
+                           atomically: useAuxiliaryFile,
+                           encoding: String.Encoding.utf8.rawValue)
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+}
+
+/*
+ extension NSString: Readable {
+
+ /// Creates a string from a path.
+ public class func read(from path: Path) throws -> Self {
+ let possibleContents = try? NSString(
+ contentsOfFile: path._safeRawValue,
+ encoding: String.Encoding.utf8.rawValue)
+ guard let contents = possibleContents else {
+ throw FileKitError.readFromFileFail(path: path)
+ }
+ return contents
+ }
+ }
+ */

+ 437 - 0
Frameworks/FileKit/Sources/Operators.swift

@@ -0,0 +1,437 @@
+//
+//  Operators.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+// swiftlint:disable file_length
+
+import Foundation
+
+private func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
+    switch (lhs, rhs) {
+    case let (l?, r?):
+        return l < r
+    case (nil, _?):
+        return true
+    default:
+        return false
+    }
+}
+
+// MARK: - File
+
+/// Returns `true` if both files' paths are the same.
+
+public func ==<DataType>(lhs: File<DataType>, rhs: File<DataType>) -> Bool {
+    return lhs.path == rhs.path
+}
+
+/// Returns `true` if `lhs` is smaller than `rhs` in size.
+
+public func < <DataType>(lhs: File<DataType>, rhs: File<DataType>) -> Bool {
+    return lhs.size < rhs.size
+}
+
+infix operator |>
+
+/// Writes data to a file.
+///
+/// - Throws: `FileKitError.WriteToFileFail`
+///
+public func |> <DataType>(data: DataType, file: File<DataType>) throws {
+    try file.write(data)
+}
+
+// MARK: - TextFile
+
+/// Returns `true` if both text files have the same path and encoding.
+
+public func == (lhs: TextFile, rhs: TextFile) -> Bool {
+    return lhs.path == rhs.path && lhs.encoding == rhs.encoding
+}
+
+infix operator |>>
+
+/// Appends a string to a text file.
+///
+/// If the text file can't be read from, such in the case that it doesn't exist,
+/// then it will try to write the data directly to the file.
+///
+/// - Throws: `FileKitError.WriteToFileFail`
+///
+public func |>> (data: String, file: TextFile) throws {
+    let textStreamWriter = try file.streamWriter(append: true)
+
+    guard textStreamWriter.writeDelimiter(), textStreamWriter.write(line: data, delim: false) else {
+        let reason: FileKitError.ReasonError
+        if textStreamWriter.isClosed {
+            reason = .closed
+        } else {
+            reason = .encoding(textStreamWriter.encoding, data: data)
+        }
+        throw FileKitError.writeToFileFail(path: file.path, error: reason)
+    }
+}
+
+/// Return lines of file that match the motif.
+
+public func | (file: TextFile, motif: String) -> [String] {
+    return file.grep(motif)
+}
+
+infix operator |-
+/// Return lines of file that does'nt match the motif.
+
+public func |- (file: TextFile, motif: String) -> [String] {
+    return file.grep(motif, include: false)
+}
+
+infix operator |~
+/// Return lines of file that match the regex motif.
+
+public func |~ (file: TextFile, motif: String) -> [String] {
+    return file.grep(motif, options: .regularExpression)
+}
+
+// MARK: - Path
+
+/// Returns `true` if the standardized form of one path equals that of another
+/// path.
+
+public func == (lhs: Path, rhs: Path) -> Bool {
+    if lhs.isAbsolute || rhs.isAbsolute {
+        return lhs.absolute.rawValue == rhs.absolute.rawValue
+    }
+    return lhs.standardRawValueWithTilde == rhs.standardRawValueWithTilde
+}
+
+/// Returns `true` if the standardized form of one path not equals that of another
+/// path.
+
+public func != (lhs: Path, rhs: Path) -> Bool {
+    return !(lhs == rhs)
+}
+
+/// Concatenates two `Path` instances and returns the result.
+///
+/// ```swift
+/// let systemLibrary: Path = "/System/Library"
+/// print(systemLib + "Fonts")  // "/System/Library/Fonts"
+/// ```
+///
+
+public func + (lhs: Path, rhs: Path) -> Path {
+    if lhs.rawValue.isEmpty || lhs.rawValue == "." { return rhs }
+    if rhs.rawValue.isEmpty || rhs.rawValue == "." { return lhs }
+    switch (lhs.rawValue.hasSuffix(Path.separator), rhs.rawValue.hasPrefix(Path.separator)) {
+    case (true, true):
+        let rhsRawValue = rhs.dropFirst()
+        return Path("\(lhs.rawValue)\(rhsRawValue)")
+    case (false, false):
+        return Path("\(lhs.rawValue)\(Path.separator)\(rhs.rawValue)")
+    default:
+        return Path("\(lhs.rawValue)\(rhs.rawValue)")
+    }
+}
+
+/// Converts a `String` to a `Path` and returns the concatenated result.
+
+public func + (lhs: String, rhs: Path) -> Path {
+    return Path(lhs) + rhs
+}
+
+/// Converts a `String` to a `Path` and returns the concatenated result.
+
+public func + (lhs: Path, rhs: String) -> Path {
+    return lhs + Path(rhs)
+}
+
+/// Appends the right path to the left path.
+public func += (lhs: inout Path, rhs: Path) {
+    // swiftlint:disable:next shorthand_operator
+    lhs = lhs + rhs
+}
+
+/// Appends the path value of the String to the left path.
+public func += (lhs: inout Path, rhs: String) {
+    // swiftlint:disable:next shorthand_operator
+    lhs = lhs + rhs
+}
+
+/// Concatenates two `Path` instances and returns the result.
+
+public func / (lhs: Path, rhs: Path) -> Path {
+    return lhs + rhs
+}
+
+/// Converts a `String` to a `Path` and returns the concatenated result.
+
+public func / (lhs: Path, rhs: String) -> Path {
+    return lhs + rhs
+}
+
+/// Converts a `String` to a `Path` and returns the concatenated result.
+
+public func / (lhs: String, rhs: Path) -> Path {
+    return lhs + rhs
+}
+
+/// Appends the right path to the left path.
+public func /= (lhs: inout Path, rhs: Path) {
+    lhs += rhs
+}
+
+/// Appends the path value of the String to the left path.
+public func /= (lhs: inout Path, rhs: String) {
+    lhs += rhs
+}
+
+precedencegroup FileCommonAncestorPrecedence {
+    associativity: left
+}
+
+infix operator <^> : FileCommonAncestorPrecedence
+
+/// Returns the common ancestor between the two paths.
+
+public func <^> (lhs: Path, rhs: Path) -> Path {
+    return lhs.commonAncestor(rhs)
+}
+
+infix operator </>
+
+/// Runs `closure` with the path as its current working directory.
+public func </> (path: Path, closure: () throws -> Void) rethrows {
+    try path.changeDirectory(closure)
+}
+
+infix operator ->>
+
+/// Moves the file at the left path to a path.
+///
+/// Throws an error if the file at the left path could not be moved or if a file
+/// already exists at the right path.
+///
+/// - Throws: `FileKitError.FileDoesNotExist`, `FileKitError.MoveFileFail`
+///
+public func ->> (lhs: Path, rhs: Path) throws {
+    try lhs.moveFile(to: rhs)
+}
+
+/// Moves a file to a path.
+///
+/// Throws an error if the file could not be moved or if a file already
+/// exists at the destination path.
+///
+/// - Throws: `FileKitError.FileDoesNotExist`, `FileKitError.MoveFileFail`
+///
+public func ->> <DataType>(lhs: File<DataType>, rhs: Path) throws {
+    try lhs.move(to: rhs)
+}
+
+infix operator ->!
+
+/// Forcibly moves the file at the left path to the right path.
+///
+/// - Warning: If a file at the right path already exists, it will be deleted.
+///
+/// - Throws:
+///     `FileKitError.DeleteFileFail`,
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func ->! (lhs: Path, rhs: Path) throws {
+    if rhs.isAny {
+        try rhs.deleteFile()
+    }
+    try lhs ->> rhs
+}
+
+/// Forcibly moves a file to a path.
+///
+/// - Warning: If a file at the right path already exists, it will be deleted.
+///
+/// - Throws:
+///     `FileKitError.DeleteFileFail`,
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func ->! <DataType>(lhs: File<DataType>, rhs: Path) throws {
+    if rhs.isAny {
+        try rhs.deleteFile()
+    }
+    try lhs ->> rhs
+}
+
+infix operator +>>
+
+/// Copies the file at the left path to the right path.
+///
+/// Throws an error if the file at the left path could not be copied or if a file
+/// already exists at the right path.
+///
+/// - Throws: `FileKitError.FileDoesNotExist`, `FileKitError.CopyFileFail`
+///
+public func +>> (lhs: Path, rhs: Path) throws {
+    try lhs.copyFile(to: rhs)
+}
+
+/// Copies a file to a path.
+///
+/// Throws an error if the file could not be copied or if a file already
+/// exists at the destination path.
+///
+/// - Throws: `FileKitError.FileDoesNotExist`, `FileKitError.CopyFileFail`
+///
+public func +>> <DataType>(lhs: File<DataType>, rhs: Path) throws {
+    try lhs.copy(to: rhs)
+}
+
+infix operator +>!
+
+/// Forcibly copies the file at the left path to the right path.
+///
+/// - Warning: If a file at the right path already exists, it will be deleted.
+///
+/// - Throws:
+///     `FileKitError.DeleteFileFail`,
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func +>! (lhs: Path, rhs: Path) throws {
+    if rhs.isAny {
+        try rhs.deleteFile()
+    }
+    try lhs +>> rhs
+}
+
+/// Forcibly copies a file to a path.
+///
+/// - Warning: If a file at the right path already exists, it will be deleted.
+///
+/// - Throws:
+///     `FileKitError.DeleteFileFail`,
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func +>! <DataType>(lhs: File<DataType>, rhs: Path) throws {
+    if rhs.isAny {
+        try rhs.deleteFile()
+    }
+    try lhs +>> rhs
+}
+
+infix operator =>>
+
+/// Creates a symlink of the left path at the right path.
+///
+/// If the symbolic link path already exists and _is not_ a directory, an
+/// error will be thrown and a link will not be created.
+///
+/// If the symbolic link path already exists and _is_ a directory, the link
+/// will be made to a file in that directory.
+///
+/// - Throws:
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func =>> (lhs: Path, rhs: Path) throws {
+    try lhs.symlinkFile(to: rhs)
+}
+
+/// Symlinks a file to a path.
+///
+/// If the path already exists and _is not_ a directory, an error will be
+/// thrown and a link will not be created.
+///
+/// If the path already exists and _is_ a directory, the link will be made
+/// to the file in that directory.
+///
+/// - Throws: `FileKitError.FileDoesNotExist`, `FileKitError.CreateSymlinkFail`
+///
+public func =>> <DataType>(lhs: File<DataType>, rhs: Path) throws {
+    try lhs.symlink(to: rhs)
+}
+
+infix operator =>!
+
+/// Forcibly creates a symlink of the left path at the right path by deleting
+/// anything at the right path before creating the symlink.
+///
+/// - Warning: If the symbolic link path already exists, it will be deleted.
+///
+/// - Throws:
+///     `FileKitError.DeleteFileFail`,
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func =>! (lhs: Path, rhs: Path) throws {
+    //    guard lhs.exists else {
+    //        throw FileKitError.FileDoesNotExist(path: lhs)
+    //    }
+
+    let linkPath = rhs.isDirectory ? rhs + lhs.fileName : rhs
+    if linkPath.isAny { try linkPath.deleteFile() }
+
+    try lhs =>> rhs
+}
+
+/// Forcibly creates a symlink of a file at a path by deleting anything at the
+/// path before creating the symlink.
+///
+/// - Warning: If the path already exists, it will be deleted.
+///
+/// - Throws:
+///     `FileKitError.DeleteFileFail`,
+///     `FileKitError.FileDoesNotExist`,
+///     `FileKitError.CreateSymlinkFail`
+///
+public func =>! <DataType>(lhs: File<DataType>, rhs: Path) throws {
+    try lhs.path =>! rhs
+}
+
+postfix operator %
+
+/// Returns the standardized version of the path.
+
+public postfix func % (path: Path) -> Path {
+    return path.standardized
+}
+
+postfix operator *
+
+/// Returns the resolved version of the path.
+
+public postfix func * (path: Path) -> Path {
+    return path.resolved
+}
+
+postfix operator ^
+
+/// Returns the path's parent path.
+
+public postfix func ^ (path: Path) -> Path {
+    return path.parent
+}

+ 1381 - 0
Frameworks/FileKit/Sources/Path.swift

@@ -0,0 +1,1381 @@
+//
+//  Path.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+//  swiftlint:disable file_length
+//
+
+import Foundation
+
+/// A representation of a filesystem path.
+///
+/// An Path instance lets you manage files in a much easier way.
+///
+
+public struct Path {
+
+    // MARK: - Static Methods and Properties
+
+    /// The standard separator for path components.
+    public static let separator = "/"
+
+    /// The root path.
+    public static let root = Path(separator)
+
+    /// The path of the program's current working directory.
+    public static var current: Path {
+        get {
+            return Path(FileManager.default.currentDirectoryPath)
+        }
+        set {
+            FileManager.default.changeCurrentDirectoryPath(newValue._safeRawValue)
+        }
+    }
+
+    /// The paths of the mounted volumes available.
+    public static func volumes(_ options: FileManager.VolumeEnumerationOptions = []) -> [Path] {
+        let volumes = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil,
+            options: options)
+        return (volumes ?? []).compactMap { Path(url: $0) }
+    }
+
+    // MARK: - Properties
+
+    fileprivate var _fmWraper = _FMWrapper()
+
+    fileprivate class _FMWrapper {
+        let unsafeFileManager = FileManager()
+        weak var delegate: FileManagerDelegate?
+        /// Safe way to use fileManager
+        var fileManager: FileManager {
+            // if delegate == nil {
+            //   print("\n\nDelegate is nil\n\n")
+            // }
+            unsafeFileManager.delegate = delegate
+            return unsafeFileManager
+        }
+    }
+
+    /// The delegate for the file manager used by the path.
+    ///
+    /// **Note:** no strong reference stored in path, so make sure keep the delegate or it will be `nil`
+    public var fileManagerDelegate: FileManagerDelegate? {
+        get {
+            return _fmWraper.delegate
+        }
+        set {
+            if !isKnownUniquelyReferenced(&_fmWraper) {
+                _fmWraper = _FMWrapper()
+            }
+            _fmWraper.delegate = newValue
+        }
+    }
+
+    /// The stored path string value.
+    public fileprivate(set) var rawValue: String
+
+    /// The non-empty path string value. For internal use only.
+    ///
+    /// Some NSAPI may throw `NSInvalidArgumentException` when path is `""`, which can't catch in swift
+    /// and cause crash
+    internal var _safeRawValue: String {
+        return rawValue.isEmpty ? "." : rawValue
+    }
+
+    /// The standardized path string value
+    public var standardRawValue: String {
+        return (self.rawValue as NSString).standardizingPath
+    }
+
+    /// The standardized path string value without expanding tilde
+    public var standardRawValueWithTilde: String {
+        let comps = components
+        if comps.isEmpty {
+            return ""
+        } else {
+            return self[comps.count - 1].rawValue
+        }
+    }
+
+    /// The components of the path.
+    ///
+    /// Return [] if path is `.` or `""`
+    public var components: [Path] {
+        if rawValue == "" || rawValue == "." {
+            return []
+        }
+        if isAbsolute {
+            return (absolute.rawValue as NSString).pathComponents.enumerated().compactMap {
+                (($0 == 0 || $1 != "/") && $1 != ".") ? Path($1) : nil
+            }
+        } else {
+            let comps = (self.rawValue as NSString).pathComponents.enumerated()
+            // remove extraneous `/` and `.`
+            let cleanComps = comps.compactMap {
+                (($0 == 0 || $1 != "/") && $1 != ".") ? Path($1) : nil
+            }
+            return _cleanComponents(cleanComps)
+        }
+    }
+
+    /// resolving `..` if possible
+    fileprivate func _cleanComponents(_ comps: [Path]) -> [Path] {
+        var isContinue = false
+        let count = comps.count
+        let cleanComps: [Path] = comps.enumerated().compactMap {
+            if ($1.rawValue != ".." && $0 < count - 1 && comps[$0 + 1].rawValue == "..") || ($1.rawValue == ".." && $0 > 0 && comps[$0 - 1].rawValue != "..") {
+                isContinue = true
+                return nil
+            } else {
+                return $1
+            }
+        }
+        return isContinue ? _cleanComponents(cleanComps) : cleanComps
+    }
+
+    /// The name of the file at `self`.
+    public var fileName: String {
+        return self.absolute.components.last?.rawValue ?? ""
+    }
+
+    /// The name of the file without extension.
+    public var fileNameWithoutExtension: String {
+        return ((rawValue as NSString).lastPathComponent as NSString).deletingPathExtension
+    }
+
+    /// A new path created by removing extraneous components from the path.
+    public var standardized: Path {
+        return Path((self.rawValue as NSString).standardizingPath)
+    }
+
+    /// The standardized path string value without expanding tilde
+    public var standardWithTilde: Path {
+        let comps = components
+        if comps.isEmpty {
+            return Path("")
+        } else {
+            return self[comps.count - 1]
+        }
+    }
+
+    /// A new path created by resolving all symlinks and standardizing the path.
+    public var resolved: Path {
+        return Path((self.rawValue as NSString).resolvingSymlinksInPath)
+    }
+
+    /// A new path created by making the path absolute.
+    ///
+    /// - Returns: If `self` begins with "/", then the standardized path is
+    ///            returned. Otherwise, the path is assumed to be relative to
+    ///            the current working directory and the standardized version of
+    ///            the path added to the current working directory is returned.
+    ///
+    public var absolute: Path {
+        return self.isAbsolute
+            ? self.standardized
+            : (Path.current + self).standardized
+    }
+
+    /// Returns `true` if the path is equal to "/".
+    public var isRoot: Bool {
+        return resolved.rawValue == Path.separator
+    }
+
+    /// Returns `true` if the path begins with "/".
+    public var isAbsolute: Bool {
+        return rawValue.hasPrefix(Path.separator)
+    }
+
+    /// Returns `true` if the path does not begin with "/".
+    public var isRelative: Bool {
+        return !isAbsolute
+    }
+
+    /// Returns `true` if a file or directory exists at the path.
+    ///
+    /// this method does follow links.
+    public var exists: Bool {
+        return _fmWraper.fileManager.fileExists(atPath: _safeRawValue)
+    }
+
+    /// Returns `true` if a file or directory or symbolic link exists at the path
+    ///
+    /// this method does **not** follow links.
+//    public var existsOrLink: Bool {
+//        return self.isSymbolicLink || _fmWraper.fileManager.fileExistsAtPath(_safeRawValue)
+//    }
+
+    /// Returns `true` if the current process has write privileges for the file
+    /// at the path.
+    ///
+    /// this method does follow links.
+    public var isWritable: Bool {
+        return _fmWraper.fileManager.isWritableFile(atPath: _safeRawValue)
+    }
+
+    /// Returns `true` if the current process has read privileges for the file
+    /// at the path.
+    ///
+    /// this method does follow links.
+    public var isReadable: Bool {
+        return _fmWraper.fileManager.isReadableFile(atPath: _safeRawValue)
+    }
+
+    /// Returns `true` if the current process has execute privileges for the
+    /// file at the path.
+    ///
+    /// this method does follow links.
+    public var isExecutable: Bool {
+        return  _fmWraper.fileManager.isExecutableFile(atPath: _safeRawValue)
+    }
+
+    /// Returns `true` if the current process has delete privileges for the file
+    /// at the path.
+    ///
+    /// this method does **not** follow links.
+    public var isDeletable: Bool {
+        return  _fmWraper.fileManager.isDeletableFile(atPath: _safeRawValue)
+    }
+
+    /// Returns `true` if the path points to a directory.
+    ///
+    /// this method does follow links.
+    public var isDirectory: Bool {
+        var isDirectory: ObjCBool = false
+        return _fmWraper.fileManager.fileExists(atPath: _safeRawValue, isDirectory: &isDirectory)
+            && isDirectory.boolValue
+    }
+
+    /// Returns `true` if the path is a directory file.
+    ///
+    /// this method does not follow links.
+    public var isDirectoryFile: Bool {
+        return fileType == .directory
+    }
+
+    /// Returns `true` if the path is a symbolic link.
+    ///
+    /// this method does not follow links.
+    public var isSymbolicLink: Bool {
+        return fileType == .symbolicLink
+    }
+
+    /// Returns `true` if the path is a regular file.
+    ///
+    /// this method does not follow links.
+    public var isRegular: Bool {
+        return fileType == .regular
+    }
+
+    /// Returns `true` if the path exists any fileType item.
+    ///
+    /// this method does not follow links.
+    public var isAny: Bool {
+        return fileType != nil
+    }
+
+    /// The path's extension.
+    public var pathExtension: String {
+        get {
+            return (rawValue as NSString).pathExtension
+        }
+        set {
+            let path = (rawValue as NSString).deletingPathExtension
+            rawValue = path + ".\(newValue)"
+        }
+    }
+    mutating func appendToExtension(_ toAppend: String) {
+        self.pathExtension = pathExtension + toAppend
+    }
+
+    /// The path's parent path.
+    public var parent: Path {
+        if isAbsolute {
+            return Path((absolute.rawValue as NSString).deletingLastPathComponent)
+        } else {
+            let comps = components
+            if comps.isEmpty {
+                return Path("..")
+            } else if comps.last!.rawValue == ".." {
+                return ".." + self[comps.count - 1]
+            } else if comps.count == 1 {
+                return Path("")
+            } else {
+                return self[comps.count - 2]
+            }
+        }
+    }
+
+    // MARK: - Initialization
+
+    /// Initializes a path to root.
+    public init() {
+        self = .root
+    }
+
+    /// Initializes a path to the string's value.
+    public init(_ path: String, expandingTilde: Bool = false) {
+        // empty path may cause crash
+        if expandingTilde {
+            self.rawValue = (path as NSString).expandingTildeInPath
+        } else {
+            self.rawValue = path
+        }
+    }
+
+}
+
+extension Path {
+
+    // MARK: - Methods
+
+    /// Runs `closure` with `self` as its current working directory.
+    ///
+    /// - Parameter closure: The block to run while `Path.Current` is changed.
+    ///
+    public func changeDirectory(_ closure: () throws -> Void) rethrows {
+        let previous = Path.current
+        defer { Path.current = previous }
+        if _fmWraper.fileManager.changeCurrentDirectoryPath(_safeRawValue) {
+            try closure()
+        }
+    }
+
+    /// Returns the path's children paths.
+    ///
+    /// - Parameter recursive: Whether to obtain the paths recursively.
+    ///                        Default value is `false`.
+    ///
+    /// this method follow links if recursive is `false`, otherwise not follow links
+    public func children(recursive: Bool = false) -> [Path] {
+        let obtainFunc = recursive
+            ? _fmWraper.fileManager.subpathsOfDirectory(atPath:)
+            : _fmWraper.fileManager.contentsOfDirectory(atPath:)
+        return (try? obtainFunc(_safeRawValue))?.map { self + Path($0) } ?? []
+    }
+
+    /// Returns true if `path` is a child of `self`.
+    ///
+    /// - Parameter recursive: Whether to check the paths recursively.
+    ///                        Default value is `true`.
+    ///
+    public func isChildOfPath(_ path: Path, recursive: Bool = true) -> Bool {
+        if !(isRelative && path.isRelative) && !(isAbsolute && path.isAbsolute) {
+            return self.absolute.isChildOfPath(path.absolute)
+        }
+        if isRoot {
+            return true
+        }
+        if recursive {
+            return path.isAncestorOfPath(self)
+        } else {
+            return path.parent == self
+        }
+    }
+
+    /// Returns true if `path` is a parent of `self`.
+    ///
+    /// Relative paths can't be compared return `false`. like `../../path1/path2` and `../path2`
+    ///
+    public func isAncestorOfPath(_ path: Path) -> Bool {
+        if !(isRelative && path.isRelative) && !(isAbsolute && path.isAbsolute) {
+            return self.absolute.isAncestorOfPath(path.absolute)
+        }
+        if path.isRoot {
+            return true
+        }
+        if self != path && self.commonAncestor(path) == path {
+            return true
+        }
+        return false
+    }
+
+    /// Returns the common ancestor between `self` and `path`.
+    ///
+    /// Relative path return the most precise path if possible
+    ///
+    public func commonAncestor(_ path: Path) -> Path {
+        if !(isRelative && path.isRelative) && !(isAbsolute && path.isAbsolute) {
+            return self.absolute.commonAncestor(path.absolute)
+        }
+        let selfComponents = self.components
+        let pathComponents = path.components
+
+        let minCount = Swift.min(selfComponents.count, pathComponents.count)
+        var total = minCount
+
+        for index in 0 ..< total
+            where selfComponents[index].rawValue != pathComponents[index].rawValue {
+                total = index
+                break
+        }
+
+        let ancestorComponents = selfComponents[0..<total]
+        let common =  ancestorComponents.reduce("") { $0 + $1 }
+        switch (self.relativePathType, path.relativePathType) {
+        case (.absolute, .absolute), (.normal, .normal), (.normal, .current), (.current, .normal), (.current, .current):
+            return common
+        case (.normal, .parent), (.current, .parent), (.parent, .normal), (.parent, .current), (.parent, .parent):
+            return Path("..")
+        default:
+            // count for prefix ".." in components
+            var n1 = 0, n2 = 0
+            for elem in selfComponents {
+                if elem.rawValue == ".." {
+                    n1 += 1
+                } else {
+                    break
+                }
+            }
+            for elem in pathComponents {
+                if elem.rawValue == ".." {
+                    n2 += 1
+                } else {
+                    break
+                }
+            }
+            if n1 == n2 {
+                // paths like "../../common/path1" and "../../common/path2"
+                return common
+            } else {    // paths like "../path" and "../../path2/path1"
+                let maxCount = Swift.max(n1, n2)
+                var dotPath: Path = ""
+                for _ in 0..<maxCount {
+                    dotPath += ".."
+                }
+                return dotPath
+            }
+        }
+    }
+
+    /// Returns the relative path type.
+    ///
+    public var relativePathType: RelativePathType {
+        if isAbsolute {
+            return .absolute
+        } else {
+            let comp = self.components
+            switch comp.first?.rawValue {
+            case nil:
+                return .current
+            case ".."? where comp.count > 1:
+                return .ancestor
+            case ".."?:
+                return .parent
+            default:
+                return .normal
+            }
+        }
+    }
+
+    /// Returns paths in `self` that match a condition.
+    ///
+    /// - Parameter searchDepth: How deep to search before exiting. A negative
+    ///                          value will cause the search to exit only when
+    ///                          every subdirectory has been searched through.
+    ///                          Default value is `-1`.
+    /// - Parameter condition: If `true`, the path is added.
+    ///
+    /// - Returns: An Array containing the paths in `self` that match the
+    ///            condition.
+    ///
+    public func find(searchDepth depth: Int = -1, condition: (Path) throws -> Bool) rethrows -> [Path] {
+        return try self.find(searchDepth: depth) { path in
+            try condition(path) ? path : nil
+        }
+    }
+
+    /// Returns non-nil values for paths found in `self`.
+    ///
+    /// - Parameter searchDepth: How deep to search before exiting. A negative
+    ///                          value will cause the search to exit only when
+    ///                          every subdirectory has been searched through.
+    ///                          Default value is `-1`.
+    /// - Parameter transform: The transform run on each path found.
+    ///
+    /// - Returns: An Array containing the non-nil values for paths found in
+    ///            `self`.
+    ///
+    public func find<T>(searchDepth depth: Int = -1, transform: (Path) throws -> T?) rethrows -> [T] {
+        return try self.children().reduce([]) { values, child in
+            if let value = try transform(child) {
+                return values + [value]
+            } else if depth != 0 {
+                return try values + child.find(searchDepth: depth - 1, transform: transform)
+            } else {
+                return values
+            }
+        }
+    }
+
+    // swiftlint:enable line_length
+
+    /// Standardizes the path.
+    public mutating func standardize() {
+        self = self.standardized
+    }
+
+    /// Resolves the path's symlinks and standardizes it.
+    public mutating func resolve() {
+        self = self.resolved
+    }
+
+    /// Creates a symbolic link at a path that points to `self`.
+    ///
+    /// - Parameter path: The Path to which at which the link of the file at
+    ///                   `self` will be created.
+    ///                   If `path` exists and is a directory, then the link
+    ///                   will be made inside of `path`. Otherwise, an error
+    ///                   will be thrown.
+    ///
+    /// - Throws:
+    ///     `FileKitError.fileAlreadyExists`,
+    ///     `FileKitError.createSymlinkFail`
+    ///
+    public func symlinkFile(to path: Path) throws {
+        // it's possible to create symbolic links to locations that do not yet exist.
+//        guard self.exists else {
+//            throw FileKitError.FileDoesNotExist(path: self)
+//        }
+
+        let linkPath = path.isDirectory ? path + self.fileName : path
+
+        // Throws if linking to an existing non-directory file.
+        guard !linkPath.isAny else {
+            throw  FileKitError.fileAlreadyExists(path: linkPath)
+        }
+
+        do {
+            try _fmWraper.fileManager.createSymbolicLink(
+                atPath: linkPath._safeRawValue, withDestinationPath: self._safeRawValue)
+        } catch {
+            throw FileKitError.createSymlinkFail(from: self, to: linkPath, error: error)
+        }
+    }
+
+    /// Creates a hard link at a path that points to `self`.
+    ///
+    /// - Parameter path: The Path to which the link of the file at
+    ///                   `self` will be created.
+    ///                   If `path` exists and is a directory, then the link
+    ///                   will be made inside of `path`. Otherwise, an error
+    ///                   will be thrown.
+    ///
+    /// - Throws:
+    ///     `FileKitError.fileAlreadyExists`,
+    ///     `FileKitError.createHardlinkFail`
+    ///
+    public func hardlinkFile(to path: Path) throws {
+        let linkPath = path.isDirectory ? path + self.fileName : path
+
+        guard !linkPath.isAny else {
+            throw FileKitError.fileAlreadyExists(path: linkPath)
+        }
+
+        do {
+            try _fmWraper.fileManager.linkItem(atPath: self._safeRawValue, toPath: linkPath._safeRawValue)
+        } catch {
+            throw FileKitError.createHardlinkFail(from: self, to: path, error: error)
+        }
+    }
+
+    /// Creates a file at path.
+    ///
+    /// Throws an error if the file cannot be created.
+    ///
+    /// - Throws: `FileKitError.CreateFileFail`
+    ///
+    /// this method does not follow links.
+    ///
+    /// If a file or symlink exists, this method removes the file or symlink and create regular file
+    public func createFile() throws {
+        if !_fmWraper.fileManager.createFile(atPath: _safeRawValue, contents: nil, attributes: nil) {
+            throw FileKitError.createFileFail(path: self)
+        }
+    }
+
+    /// Creates a file at path if not exist
+    /// or update the modification date.
+    ///
+    /// Throws an error if the file cannot be created
+    /// or if modification date could not be modified.
+    ///
+    /// - Throws:
+    ///     `FileKitError.CreateFileFail`,
+    ///     `FileKitError.AttributesChangeFail`
+    ///
+    public func touch(_ updateModificationDate: Bool = true) throws {
+        if self.exists {
+            if updateModificationDate {
+                try _setAttribute(FileAttributeKey.modificationDate, value: Date())
+            }
+        } else {
+            try createFile()
+        }
+    }
+
+    /// Creates a directory at the path.
+    ///
+    /// Throws an error if the directory cannot be created.
+    ///
+    /// - Parameter createIntermediates: If `true`, any non-existent parent
+    ///                                  directories are created along with that
+    ///                                  of `self`. Default value is `true`.
+    ///
+    /// - Throws: `FileKitError.CreateDirectoryFail`
+    ///
+    /// this method does not follow links.
+    ///
+    public func createDirectory(withIntermediateDirectories createIntermediates: Bool = true) throws {
+        do {
+            let manager = _fmWraper.fileManager
+            try manager.createDirectory(atPath: _safeRawValue,
+                withIntermediateDirectories: createIntermediates,
+                attributes: nil)
+        } catch {
+            throw FileKitError.createDirectoryFail(path: self, error: error)
+        }
+    }
+
+    // swiftlint:enable line_length
+
+    /// Deletes the file or directory at the path.
+    ///
+    /// Throws an error if the file or directory cannot be deleted.
+    ///
+    /// - Throws: `FileKitError.DeleteFileFail`
+    ///
+    /// this method does not follow links.
+    public func deleteFile() throws {
+        do {
+            try _fmWraper.fileManager.removeItem(atPath: _safeRawValue)
+        } catch {
+            throw FileKitError.deleteFileFail(path: self, error: error)
+        }
+    }
+
+    /// Moves the file at `self` to a path.
+    ///
+    /// Throws an error if the file cannot be moved.
+    ///
+    /// - Throws: `FileKitError.fileDoesNotExist`, `FileKitError.fileAlreadyExists`, `FileKitError.moveFileFail`
+    ///
+    /// this method does not follow links.
+    public func moveFile(to path: Path) throws {
+        if self.isAny {
+            if !path.isAny {
+                do {
+                    try _fmWraper.fileManager.moveItem(atPath: self._safeRawValue, toPath: path._safeRawValue)
+                } catch {
+                    throw FileKitError.moveFileFail(from: self, to: path, error: error)
+                }
+            } else {
+                throw FileKitError.fileAlreadyExists(path: path)
+            }
+        } else {
+            throw FileKitError.fileDoesNotExist(path: self)
+        }
+    }
+
+    /// Copies the file at `self` to a path.
+    ///
+    /// Throws an error if the file at `self` could not be copied or if a file
+    /// already exists at the destination path.
+    ///
+    /// - Throws: `FileKitError.fileDoesNotExist`, `FileKitError.fileAlreadyExists`, `FileKitError.copyFileFail`
+    ///
+    /// this method does not follow links.
+    public func copyFile(to path: Path) throws {
+        if self.isAny {
+            if !path.isAny {
+                do {
+                    try _fmWraper.fileManager.copyItem(atPath: self._safeRawValue, toPath: path._safeRawValue)
+                } catch {
+                    throw FileKitError.copyFileFail(from: self, to: path, error: error)
+                }
+            } else {
+                throw FileKitError.fileAlreadyExists(path: path)
+            }
+        } else {
+            throw FileKitError.fileDoesNotExist(path: self)
+        }
+    }
+
+}
+
+extension Path: ExpressibleByStringLiteral {
+
+    // MARK: - ExpressibleByStringLiteral
+
+    public typealias ExtendedGraphemeClusterLiteralType = StringLiteralType
+
+    public typealias UnicodeScalarLiteralType = StringLiteralType
+
+    /// Initializes a path to the literal.
+    public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
+        self.rawValue = value
+    }
+
+    /// Initializes a path to the literal.
+    public init(stringLiteral value: StringLiteralType) {
+        self.rawValue = value
+    }
+
+    /// Initializes a path to the literal.
+    public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
+        self.rawValue = value
+    }
+
+}
+
+extension Path: RawRepresentable {
+
+    // MARK: - RawRepresentable
+
+    /// Initializes a path to the string value.
+    ///
+    /// - Parameter rawValue: The raw value to initialize from.
+    public init(rawValue: String) {
+        self.rawValue = rawValue
+    }
+
+}
+
+extension Path: Hashable {
+
+    // MARK: - Hashable
+
+    /// To compute the hash value of the path.
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(rawValue)
+    }
+
+}
+
+extension Path { // : Indexable {
+
+    // MARK: - Indexable
+
+    /// The path's start index.
+    public var startIndex: Int {
+        return components.startIndex
+    }
+
+    /// The path's end index; the successor of the last valid subscript argument.
+    public var endIndex: Int {
+        return components.endIndex
+    }
+
+    /// The path's subscript. (read-only)
+    ///
+    /// - Returns: All of the path's elements up to and including the index.
+    ///
+    public subscript(position: Int) -> Path {
+        let components = self.components
+        if position < 0 || position >= components.count {
+            fatalError("Path index out of range")
+        } else {
+            var result = components.first!
+            for i in 1 ..< position + 1 {
+                result += components[i]
+            }
+            return result
+        }
+    }
+
+    public subscript(bounds: Range<Int>) -> Path {
+        let components = self.components
+        if bounds.lowerBound < 0 || bounds.upperBound >= components.count {
+            fatalError("Path bounds out of range")
+        }
+        var result = components[bounds.lowerBound]
+        for i in (bounds.lowerBound + 1) ..< bounds.upperBound {
+            result += components[i]
+        }
+        return result
+    }
+
+    public func index(after i: Int) -> Int {
+        return components.index(after: i)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - Attributes
+
+    /// Returns the path's attributes.
+    ///
+    /// this method does not follow links.
+    public var attributes: [FileAttributeKey: Any] {
+        return (try? _fmWraper.fileManager.attributesOfItem(atPath: _safeRawValue)) ?? [:]
+    }
+
+    /// Modify attributes
+    ///
+    /// this method does not follow links.
+    fileprivate func _setAttributes(_ attributes: [FileAttributeKey: Any]) throws {
+        do {
+            try _fmWraper.fileManager.setAttributes(attributes, ofItemAtPath: self._safeRawValue)
+        } catch {
+            throw FileKitError.attributesChangeFail(path: self, error: error)
+        }
+    }
+
+    /// Modify one attribute
+    fileprivate func _setAttribute(_ key: FileAttributeKey, value: Any) throws {
+        try _setAttributes([key: value])
+    }
+
+    /// The creation date of the file at the path.
+    public var creationDate: Date? {
+        return attributes[FileAttributeKey.creationDate] as? Date
+    }
+
+    /// The modification date of the file at the path.
+    public var modificationDate: Date? {
+        return attributes[FileAttributeKey.modificationDate] as? Date
+    }
+
+    /// The name of the owner of the file at the path.
+    public var ownerName: String? {
+        return attributes[FileAttributeKey.ownerAccountName] as? String
+    }
+
+    /// The ID of the owner of the file at the path.
+    public var ownerID: UInt? {
+        if let value = attributes[FileAttributeKey.ownerAccountID] as? NSNumber {
+            return value.uintValue
+        }
+        return nil
+    }
+
+    /// The group name of the owner of the file at the path.
+    public var groupName: String? {
+        return attributes[FileAttributeKey.groupOwnerAccountName] as? String
+    }
+
+    /// The group ID of the owner of the file at the path.
+    public var groupID: UInt? {
+        if let value = attributes[FileAttributeKey.groupOwnerAccountID] as? NSNumber {
+            return value.uintValue
+        }
+        return nil
+    }
+
+    /// Indicates whether the extension of the file at the path is hidden.
+    public var extensionIsHidden: Bool? {
+        if let value = attributes[FileAttributeKey.extensionHidden] as? NSNumber {
+            return value.boolValue
+        }
+        return nil
+    }
+
+    /// The POSIX permissions of the file at the path.
+    public var posixPermissions: Int16? {
+        if let value = attributes[FileAttributeKey.posixPermissions] as? NSNumber {
+            return value.int16Value
+        }
+        return nil
+    }
+
+    /// The number of hard links to a file.
+    public var fileReferenceCount: UInt? {
+        if let value = attributes[FileAttributeKey.referenceCount] as? NSNumber {
+            return value.uintValue
+        }
+        return nil
+    }
+
+    /// The size of the file at the path in bytes.
+    public var fileSize: UInt64? {
+        if let value = attributes[FileAttributeKey.size] as? NSNumber {
+            return value.uint64Value
+        }
+        return nil
+    }
+
+    /// The filesystem number of the file at the path.
+    public var filesystemFileNumber: UInt? {
+        if let value = attributes[FileAttributeKey.systemFileNumber] as? NSNumber {
+            return value.uintValue
+        }
+        return nil
+    }
+}
+
+extension Path {
+
+    // MARK: - FileType
+
+    /// The FileType attribute for the file at the path.
+    public var fileType: FileType? {
+        guard let value = attributes[FileAttributeKey.type] as? String else {
+            return nil
+        }
+        return FileType(rawValue: value)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - FilePermissions
+
+    /// The permissions for the file at the path.
+    public var filePermissions: FilePermissions {
+        return FilePermissions(forPath: self)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - NSURL
+
+    /// Creates a new path with given url if possible.
+    ///
+    /// - Parameter url: The url to create a path for.
+    public init?(url: URL) {
+        guard url.isFileURL else {
+            return nil
+        }
+        rawValue = url.path
+    }
+
+    /// - Returns: The `Path` objects url.
+    public var url: URL {
+        return URL(fileURLWithPath: _safeRawValue, isDirectory: self.isDirectory)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - BookmarkData
+
+    /// Creates a new path with given url if possible.
+    ///
+    /// - Parameter bookmarkData: The bookmark data to create a path for.
+    public init?(bookmarkData bookData: Data) {
+        var isStale: ObjCBool = false
+        let url = try? (NSURL(
+            resolvingBookmarkData: bookData,
+            options: [],
+            relativeTo: nil,
+            bookmarkDataIsStale: &isStale) as URL)
+        guard let fullURL = url else {
+            return nil
+        }
+        self.init(url: fullURL)
+    }
+
+    /// - Returns: The `Path` objects bookmarkData.
+    public var bookmarkData: Data? {
+        return try? url.bookmarkData(
+            options: .suitableForBookmarkFile,
+            includingResourceValuesForKeys: nil,
+            relativeTo: nil)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - Ubiquity Container
+
+    /// Create a path  for the iCloud container associated with the specified identifier and establishes access to that container.
+    public init?(ubiquityContainerIdentifier containerIdentifier: String) {
+        guard let url = FileManager.default.url(forUbiquityContainerIdentifier: containerIdentifier) else {
+            return nil
+        }
+        self.init(url: url)
+    }
+
+    /// - Returns: a Boolean indicating whether the item is targeted for storage in iCloud.
+    public var isUbiquitousItem: Bool {
+        return _fmWraper.fileManager.isUbiquitousItem(at: self.url)
+    }
+
+    /// Removes the local copy of the specified item that’s stored in iCloud.
+    public func evictUbiquitousItem() throws {
+        do {
+            return try _fmWraper.fileManager.evictUbiquitousItem(at: self.url)
+        } catch {
+            throw FileKitError.deleteFileFail(path: self, error: error)
+        }
+    }
+
+    /// Returns a URL that can be emailed to users to allow them to download a copy of a flat file item from iCloud.
+    /// It return also the expiration date.
+    func publicUbiquitousURL() throws -> (URL, Date?) {
+        var expiration: NSDate?
+        let url = try _fmWraper.fileManager.url(forPublishingUbiquitousItemAt: self.url, expiration: &expiration)
+        guard let date = expiration else {
+            return (url, nil)
+        }
+        // TODO need to encapsulate error before exposing it
+        return (url, date as Date)
+    }
+
+    /// Indicates whether the current file should be stored in iCloud.
+    public func setUbiquitous(destination: Path) throws {
+        do {
+            try _fmWraper.fileManager.setUbiquitous(true, itemAt: self.url, destinationURL: destination.url)
+        } catch {
+            throw FileKitError.attributesChangeFail(path: self, error: error)
+        }
+    }
+
+    /// Indicates whether the current file should no more be stored in iCloud.
+    public func unsetUbiquitous() throws {
+        do {
+            try _fmWraper.fileManager.setUbiquitous(false, itemAt: self.url, destinationURL: self.url)
+        } catch {
+            throw FileKitError.attributesChangeFail(path: self, error: error)
+        }
+    }
+
+    /// Starts downloading (if necessary) the specified item to the local system.
+    func startDownloadingUbiquitous() throws {
+        try _fmWraper.fileManager.startDownloadingUbiquitousItem(at: self.url)
+        // TODO need to encapsulate error before exposing it
+    }
+
+}
+
+extension Path {
+
+    // MARK: - SecurityApplicationGroupIdentifier
+
+    /// Returns the container directory associated with the specified security application group ID.
+    ///
+    /// - Parameter groupIdentifier: The group identifier.
+    public init?(groupIdentifier: String) {
+        guard let url = FileManager().containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) else {
+            return nil
+        }
+        self.init(url: url)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - NSFileHandle
+
+    /// Returns a file handle for reading the file at the path, or `nil` if no
+    /// file exists.
+    public var fileHandleForReading: FileHandle? {
+        return FileHandle(forReadingAtPath: absolute._safeRawValue)
+    }
+
+    /// Returns a file handle for writing to the file at the path, or `nil` if
+    /// no file exists.
+    public var fileHandleForWriting: FileHandle? {
+        return FileHandle(forWritingAtPath: absolute._safeRawValue)
+    }
+
+    /// Returns a file handle for reading and writing to the file at the path,
+    /// or `nil` if no file exists.
+    public var fileHandleForUpdating: FileHandle? {
+        return FileHandle(forUpdatingAtPath: absolute._safeRawValue)
+    }
+
+}
+
+extension FileHandle {
+
+    /// Specifies how the operating system should open a file.
+    public enum Mode {
+        case read
+        case write
+        case update
+    }
+}
+
+extension Path {
+
+    /// Returns a file handle for specific mode to the file at the path,
+    /// or `nil` if no file exists.
+    /// - Parameter mode: How the operating system should open the file.
+    public func fileHandle(for mode: FileHandle.Mode) throws -> FileHandle {
+        switch mode {
+        case .read:
+            do {
+                return try FileHandle(forReadingFrom: absolute.url)
+            } catch {
+                throw FileKitError.readFromFileFail(path: self, error: error)
+            }
+        case .write:
+            do {
+                return try FileHandle(forWritingTo: absolute.url)
+            } catch {
+                throw FileKitError.writeToFileFail(path: self, error: error)
+            }
+        case .update:
+            do {
+                return try FileHandle(forUpdating: absolute.url)
+            } catch {
+                throw FileKitError.writeToFileFail(path: self, error: error)
+            }
+        }
+    }
+
+}
+
+extension Path {
+
+    // MARK: - NSStream
+
+    /// Returns an input stream that reads data from the file at the path, or
+    /// `nil` if no file exists.
+    public func inputStream() -> InputStream? {
+        return InputStream(fileAtPath: absolute._safeRawValue)
+    }
+
+    /// Returns an output stream for writing to the file at the path, or `nil`
+    /// if no file exists.
+    ///
+    /// - Parameter shouldAppend: `true` if newly written data should be
+    ///                           appended to any existing file contents,
+    ///                           `false` otherwise. Default value is `false`.
+    ///
+    public func outputStream(append shouldAppend: Bool = false) -> OutputStream? {
+        return OutputStream(toFileAtPath: absolute._safeRawValue, append: shouldAppend)
+    }
+
+}
+
+extension Path: ExpressibleByStringInterpolation {
+
+    // MARK: - StringInterpolationConvertible
+
+    /// Initializes a path from the string interpolation paths.
+    public init(stringInterpolation paths: Path...) {
+        self.init(paths.reduce("", { $0 + $1.rawValue }))
+    }
+
+    /// Initializes a path from the string interpolation segment.
+    public init<T>(stringInterpolationSegment expr: T) {
+        if let path = expr as? Path {
+            self = path
+        } else {
+            self = Path(String(describing: expr))
+        }
+    }
+
+}
+
+extension Path: CustomStringConvertible {
+
+    // MARK: - CustomStringConvertible
+
+    /// A textual representation of `self`.
+    public var description: String {
+        return rawValue
+    }
+
+}
+
+extension Path: CustomDebugStringConvertible {
+
+    // MARK: - CustomDebugStringConvertible
+
+    /// A textual representation of `self`, suitable for debugging.
+    public var debugDescription: String {
+        return "Path(\(rawValue.debugDescription))"
+    }
+
+}
+
+extension Path: Sequence {
+
+    // MARK: - Sequence
+
+    /// - Returns: An *iterator* over the contents of the path.
+    public func makeIterator() -> DirectoryEnumerator {
+        return DirectoryEnumerator(path: self)
+    }
+
+}
+
+extension Path {
+
+    // MARK: - Paths
+
+    /// Returns the path to the user's or application's home directory,
+    /// depending on the platform.
+    public static var userHome: Path {
+        // same as FileManager.default.homeDirectoryForCurrentUser
+        return Path(NSHomeDirectory()).standardized
+    }
+
+    /// Returns the path to the user's temporary directory.
+    public static var userTemporary: Path {
+        // same as FileManager.default.temporaryDirectory
+        return Path(NSTemporaryDirectory()).standardized
+    }
+
+    /// Returns a temporary path for the process.
+    public static var processTemporary: Path {
+        return Path.userTemporary + ProcessInfo.processInfo.globallyUniqueString
+    }
+
+    /// Returns a unique temporary path.
+    public static var uniqueTemporary: Path {
+        return Path.processTemporary + UUID().uuidString
+    }
+
+    /// Returns the path to the user's caches directory.
+    public static var userCaches: Path {
+        return _pathInUserDomain(.cachesDirectory)
+    }
+
+    /// Returns the path to the user's applications directory.
+    public static var userApplications: Path {
+        return _pathInUserDomain(.applicationDirectory)
+    }
+
+    /// Returns the path to the user's application support directory.
+    public static var userApplicationSupport: Path {
+        return _pathInUserDomain(.applicationSupportDirectory)
+    }
+
+    /// Returns the path to the user's desktop directory.
+    public static var userDesktop: Path {
+        return _pathInUserDomain(.desktopDirectory)
+    }
+
+    /// Returns the path to the user's documents directory.
+    public static var userDocuments: Path {
+        return _pathInUserDomain(.documentDirectory)
+    }
+
+    /// Returns the path to the user's autosaved documents directory.
+    public static var userAutosavedInformation: Path {
+        return _pathInUserDomain(.autosavedInformationDirectory)
+    }
+
+    /// Returns the path to the user's downloads directory.
+    public static var userDownloads: Path {
+        return _pathInUserDomain(.downloadsDirectory)
+    }
+
+    /// Returns the path to the user's library directory.
+    public static var userLibrary: Path {
+        return _pathInUserDomain(.libraryDirectory)
+    }
+
+    /// Returns the path to the user's movies directory.
+    public static var userMovies: Path {
+        return _pathInUserDomain(.moviesDirectory)
+    }
+
+    /// Returns the path to the user's music directory.
+    public static var userMusic: Path {
+        return _pathInUserDomain(.musicDirectory)
+    }
+
+    /// Returns the path to the user's pictures directory.
+    public static var userPictures: Path {
+        return _pathInUserDomain(.picturesDirectory)
+    }
+
+    /// Returns the path to the user's Public sharing directory.
+    public static var userSharedPublic: Path {
+        return _pathInUserDomain(.sharedPublicDirectory)
+    }
+
+    #if os(OSX)
+
+    /// Returns the path to the user scripts folder for the calling application
+    public static var userApplicationScripts: Path {
+        return _pathInUserDomain(.applicationScriptsDirectory)
+    }
+
+    /// Returns the path to the user's trash directory
+    public static var userTrash: Path {
+        return _pathInUserDomain(.trashDirectory)
+    }
+
+    #endif
+
+    /// Returns the path to the system's applications directory.
+    public static var systemApplications: Path {
+        return _pathInSystemDomain(.applicationDirectory)
+    }
+
+    /// Returns the path to the system's application support directory.
+    public static var systemApplicationSupport: Path {
+        return _pathInSystemDomain(.applicationSupportDirectory)
+    }
+
+    /// Returns the path to the system's library directory.
+    public static var systemLibrary: Path {
+        return _pathInSystemDomain(.libraryDirectory)
+    }
+
+    /// Returns the path to the system's core services directory.
+    public static var systemCoreServices: Path {
+        return _pathInSystemDomain(.coreServiceDirectory)
+    }
+
+    /// Returns the path to the system's PPDs directory.
+    public static var systemPrinterDescription: Path {
+        return _pathInSystemDomain(.printerDescriptionDirectory)
+    }
+
+    /// Returns the path to the system's PreferencePanes directory.
+    public static var systemPreferencePanes: Path {
+        return _pathInSystemDomain(.preferencePanesDirectory)
+    }
+
+    /// Returns the paths where resources can occur.
+    public static var allLibraries: [Path] {
+        return _pathsInDomains(.allLibrariesDirectory, .allDomainsMask)
+    }
+
+    /// Returns the paths where applications can occur
+    public static var allApplications: [Path] {
+        return _pathsInDomains(.allApplicationsDirectory, .allDomainsMask)
+    }
+
+    fileprivate static func _pathInUserDomain(_ directory: FileManager.SearchPathDirectory) -> Path {
+        return _pathsInDomains(directory, .userDomainMask)[0]
+    }
+
+    fileprivate static func _pathInSystemDomain(_ directory: FileManager.SearchPathDirectory) -> Path {
+        return _pathsInDomains(directory, .systemDomainMask)[0]
+    }
+
+    fileprivate static func _pathsInDomains(_ directory: FileManager.SearchPathDirectory,
+                                            _ domainMask: FileManager.SearchPathDomainMask) -> [Path] {
+        return NSSearchPathForDirectoriesInDomains(directory, domainMask, true)
+            .map({ Path($0).standardized })
+    }
+
+}

+ 40 - 0
Frameworks/FileKit/Sources/Process+FileKit.swift

@@ -0,0 +1,40 @@
+//
+//  Process+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+extension CommandLine {
+
+    /// The working directory for the current process.
+    public static var workingDirectory: Path {
+        get {
+            return Path.current
+        }
+        set {
+            Path.current = newValue
+        }
+    }
+
+}

+ 66 - 0
Frameworks/FileKit/Sources/PropertyListType.swift

@@ -0,0 +1,66 @@
+//
+//  PropertyListFile.swift
+//  FileKit
+//
+//  Created by phimage on 30/09/2017.
+//  Copyright © 2017 Nikolai Vazquez. All rights reserved.
+//
+
+import Foundation
+
+public typealias PropertyListReadableWritable = PropertyListReadable & PropertyListWritable
+
+// MARK: PropertyListReadable
+
+/// A PropertyList readable object is `Decodable`and provide it`s own decoder
+public protocol PropertyListReadable: Readable, Decodable {
+    static var propertyListDecoder: PropertyListDecoder { get }
+}
+extension PropertyListReadable {
+    // default implementation return the shared one
+    public static var propertyListDecoder: PropertyListDecoder {
+        return FileKit.propertyListDecoder
+    }
+}
+// Implement Readable
+extension PropertyListReadable {
+
+    /// Read a Decodable object
+    public static func read(from path: Path) throws -> Self {
+        let data = try DataFile(path: path).read()
+        do {
+            return try propertyListDecoder.decode(self, from: data)
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+}
+
+// MARK: PropertyListWritable
+
+/// A PropertyList readable object is `Decodable`and provide it`s own decoder
+public protocol PropertyListWritable: Writable, Encodable {
+    var propertyListEncoder: PropertyListEncoder { get }
+}
+extension PropertyListWritable {
+    // default implementation return the shared one
+    public var propertyListEncoder: PropertyListEncoder {
+        return FileKit.propertyListEncoder
+    }
+}
+// Implement Writable
+extension PropertyListWritable {
+
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        do {
+            let data = try propertyListEncoder.encode(self)
+            try DataFile(path: path).write(data, atomically: useAuxiliaryFile)
+        } catch let error as FileKitError {
+            throw error
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+}

+ 48 - 0
Frameworks/FileKit/Sources/RelativePathType.swift

@@ -0,0 +1,48 @@
+//
+//  RelativePathType.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// The type attribute for a relative path.
+public enum RelativePathType: String {
+
+    /// path like "dir/path".
+    case normal
+
+    /// path like "." and "".
+    case current
+
+    /// path like "../path".
+    case ancestor
+
+    /// path like "..".
+    case parent
+
+    /// path like "/path".
+    case absolute
+
+}

+ 71 - 0
Frameworks/FileKit/Sources/String+FileKit.swift

@@ -0,0 +1,71 @@
+//
+//  String+FileKit.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+var ReadableWritableStringEncoding = String.Encoding.utf8
+
+/// Allows String to be used as a ReadableWritable.
+extension String: ReadableWritable {
+
+    /// Creates a string from a path.
+    public static func read(from path: Path) throws -> String {
+        do {
+            return try String(contentsOfFile: path._safeRawValue,
+                              encoding: ReadableWritableStringEncoding)
+        } catch {
+            throw FileKitError.readFromFileFail(path: path, error: error)
+        }
+    }
+
+    /// Writes the string to a path atomically.
+    ///
+    /// - Parameter path: The path being written to.
+    ///
+    public func write(to path: Path) throws {
+        try write(to: path, atomically: true)
+    }
+
+    /// Writes the string to a path with `ReadableWritableStringEncoding` encoding.
+    ///
+    /// - Parameter path: The path being written to.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    public func write(to path: Path, atomically useAuxiliaryFile: Bool) throws {
+        do {
+            try self.write(toFile: path._safeRawValue,
+                atomically: useAuxiliaryFile,
+                encoding: ReadableWritableStringEncoding)
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+}

+ 346 - 0
Frameworks/FileKit/Sources/TextFile.swift

@@ -0,0 +1,346 @@
+//
+//  TextFile.swift
+//  FileKit
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A representation of a filesystem text file.
+///
+/// The data type is String.
+open class TextFile: File<String> {
+
+    /// The text file's string encoding.
+    open var encoding: String.Encoding
+
+    /// Initializes a text file from a path.
+    ///
+    /// - Parameter path: The path to be created a text file from.
+    public override init(path: Path) {
+        self.encoding = String.Encoding.utf8
+        super.init(path: path)
+    }
+
+    /// Initializes a text file from a path with an encoding.
+    ///
+    /// - Parameter path: The path to be created a text file from.
+    /// - Parameter encoding: The encoding to be used for the text file.
+    public init(path: Path, encoding: String.Encoding) {
+        self.encoding = encoding
+        super.init(path: path)
+    }
+
+    /// Writes a string to a text file using the file's encoding.
+    ///
+    /// - Parameter data: The string to be written to the text file.
+    /// - Parameter useAuxiliaryFile: If `true`, the data is written to an
+    ///                               auxiliary file that is then renamed to the
+    ///                               file. If `false`, the data is written to
+    ///                               the file directly.
+    ///
+    /// - Throws: `FileKitError.WriteToFileFail`
+    ///
+    open override func write(_ data: String, atomically useAuxiliaryFile: Bool) throws {
+        do {
+            try data.write(toFile: path._safeRawValue, atomically: useAuxiliaryFile, encoding: encoding)
+        } catch {
+            throw FileKitError.writeToFileFail(path: path, error: error)
+        }
+    }
+
+}
+
+// MARK: Line Reader
+
+extension TextFile {
+
+    /// Provide a reader to read line by line.
+    ///
+    /// - Parameter delimiter: the line delimiter (default: \n)
+    /// - Parameter chunkSize: size of buffer (default: 4096)
+    ///
+    /// - Returns: the `TextFileStreamReader`
+    ///
+    /// - Throws:
+    ///     `FileKitError.readFromFileFail`
+    public func streamReader(_ delimiter: String = "\n",
+                             chunkSize: Int = 4096) throws -> TextFileStreamReader {
+            return try TextFileStreamReader(
+                path: self.path,
+                delimiter: delimiter,
+                encoding: encoding,
+                chunkSize: chunkSize
+            )
+    }
+
+    /// Read file and return filtered lines.
+    ///
+    /// - Parameter motif: the motif to compare
+    /// - Parameter include: check if line include motif if true, exclude if not (default: true)
+    /// - Parameter options: optional options  for string comparaison
+    ///
+    /// - Returns: the lines
+    public func grep(_ motif: String, include: Bool = true,
+                     options: String.CompareOptions = []) -> [String] {
+            guard let reader = try? streamReader() else {
+                return []
+            }
+            defer {
+                reader.close()
+            }
+            return reader.filter {($0.range(of: motif, options: options) != nil) == include }
+    }
+
+}
+
+/// A class to read or write `TextFile`.
+open class TextFileStream {
+
+    /// The text encoding.
+    public let encoding: String.Encoding
+
+    let delimData: Data
+    var fileHandle: FileHandle?
+
+    // MARK: - Initialization
+    public init(
+        fileHandle: FileHandle,
+        delimiter: Data,
+        encoding: String.Encoding = .utf8
+        ) throws {
+        self.encoding = encoding
+        self.fileHandle = fileHandle
+        self.delimData = delimiter
+    }
+
+    // MARK: - Deinitialization
+
+    deinit {
+        self.close()
+    }
+
+    // MARK: - public methods
+
+    open var offset: UInt64 {
+        return fileHandle?.offsetInFile ?? 0
+    }
+
+    open func seek(toFileOffset offset: UInt64) {
+        fileHandle?.seek(toFileOffset: offset)
+    }
+
+    /// Close the underlying file. No reading must be done after calling this method.
+    open func close() {
+        fileHandle?.closeFile()
+        fileHandle = nil
+    }
+
+    /// Return true if file handle closed.
+    open var isClosed: Bool {
+        return fileHandle == nil
+    }
+}
+
+/// A class to read `TextFile` line by line.
+open class TextFileStreamReader: TextFileStream {
+
+    /// The chunk size when reading.
+    public let chunkSize: Int
+
+    /// Tells if the position is at the end of file.
+    open var atEOF: Bool = false
+
+    var buffer: Data!
+
+    // MARK: - Initialization
+
+    /// - Parameter path:      the file path
+    /// - Parameter delimiter: the line delimiter (default: \n)
+    /// - Parameter encoding: file encoding (default: .utf8)
+    /// - Parameter chunkSize: size of buffer (default: 4096)
+    public init(
+        path: Path,
+        delimiter: String = "\n",
+        encoding: String.Encoding = .utf8,
+        chunkSize: Int = 4096
+        ) throws {
+        self.chunkSize = chunkSize
+        let fileHandle = try path.fileHandle(for: .read)
+        self.buffer = Data(capacity: chunkSize)
+
+        guard let delimData = delimiter.data(using: encoding) else {
+            throw FileKitError.ReasonError.encoding(encoding, data: delimiter)
+        }
+        try super.init(fileHandle: fileHandle, delimiter: delimData, encoding: encoding)
+    }
+
+    // MARK: - public methods
+
+    /// - Returns: The next line, or nil on EOF.
+    open func nextLine() -> String? {
+        if atEOF {
+            return nil
+        }
+
+        // Read data chunks from file until a line delimiter is found.
+        var range = buffer.range(of: delimData, options: [], in: 0..<buffer.count)
+        while range == nil {
+            guard let tmpData = fileHandle?.readData(ofLength: chunkSize), !tmpData.isEmpty else {
+                // EOF or read error.
+                atEOF = true
+                if !buffer.isEmpty {
+                    // Buffer contains last line in file (not terminated by delimiter).
+                    let line = String(data: buffer, encoding: encoding)
+
+                    buffer.count = 0
+                    return line
+                }
+                // No more lines.
+                return nil
+            }
+            buffer.append(tmpData)
+            range = buffer.range(of: delimData, options: [], in: 0..<buffer.count)
+        }
+
+        // Convert complete line (excluding the delimiter) to a string.
+        let line = String(data: buffer.subdata(in: 0..<range!.lowerBound), encoding: encoding)
+
+        // Remove line (and the delimiter) from the buffer.
+        let cleaningRange: Range<Data.Index> = 0..<range!.upperBound
+        buffer.replaceSubrange(cleaningRange, with: Data())
+
+        return line
+    }
+
+    /// Start reading from the beginning of file.
+    open func rewind() {
+        fileHandle?.seek(toFileOffset: 0)
+        buffer.count = 0
+        atEOF = false
+    }
+
+}
+
+// Implement `SequenceType` for `TextFileStreamReader`
+extension TextFileStreamReader: Sequence {
+    /// - Returns: An iterator to be used for iterating over a `TextFileStreamReader` object.
+    public func makeIterator() -> AnyIterator<String> {
+        return AnyIterator {
+            return self.nextLine()
+        }
+    }
+}
+
+// MARK: Line Writer
+/// A class to write a `TextFile` line by line.
+open class TextFileStreamWriter: TextFileStream {
+
+    public let append: Bool
+
+    // MARK: - Initialization
+
+    /// - Parameter path:      the file path
+    /// - Parameter delimiter: the line delimiter (default: \n)
+    /// - Parameter encoding: file encoding (default: .utf8)
+    /// - Parameter append: if true append at file end (default: false)
+    /// - Parameter createIfNotExist: if true create file if not exixt (default: true)
+    public init(
+        path: Path,
+        delimiter: String = "\n",
+        encoding: String.Encoding = .utf8,
+        append: Bool = false,
+        createIfNotExist: Bool = true
+        ) throws {
+
+        if createIfNotExist && !path.exists {
+            try path.createFile()
+        }
+        self.append = append
+        let fileHandle = try path.fileHandle(for: .write)
+        if append {
+            fileHandle.seekToEndOfFile()
+        }
+        guard let delimData = delimiter.data(using: encoding) else {
+            throw FileKitError.ReasonError.encoding(encoding, data: delimiter)
+        }
+        try super.init(fileHandle: fileHandle, delimiter: delimData, encoding: encoding)
+    }
+
+    /// Write a new line in file
+    /// - Parameter line:      the line
+    /// - Parameter delim:     append the delimiter (default: true)
+    ///
+    /// - Returns: true if successfully.
+    @discardableResult
+    open func write(line: String, delim: Bool = true) -> Bool {
+        if let handle = fileHandle, let data = line.data(using: self.encoding) {
+            handle.write(data)
+            if delim {
+                handle.write(delimData)
+            }
+            return true
+        }
+        return false
+    }
+
+    /// Write a line delimiter.
+    ///
+    /// - Returns: true if successfully.
+    open func writeDelimiter() -> Bool {
+        if let handle = fileHandle {
+            handle.write(delimData)
+            return true
+        }
+        return false
+    }
+
+    /// Causes all in-memory data and attributes of the file represented by the receiver to be written to permanent storage.
+    open func synchronize() {
+        fileHandle?.synchronizeFile()
+    }
+}
+
+extension TextFile {
+
+    /// Provide a writer to write line by line.
+    ///
+    /// - Parameter delimiter: the line delimiter (default: \n)
+    /// - Parameter append: if true append at file end (default: false)
+    ///
+    /// - Returns: the `TextFileStreamWriter`
+    ///
+    /// - Throws:
+    ///     `FileKitError.CreateFileFail`,
+    ///     `FileKitError.writeToFileFail`
+    public func streamWriter(_ delimiter: String = "\n", append: Bool = false) throws -> TextFileStreamWriter {
+        return try TextFileStreamWriter(
+            path: self.path,
+            delimiter: delimiter,
+            encoding: encoding,
+            append: append
+        )
+    }
+
+}

+ 26 - 0
Frameworks/FileKit/Support/Info.plist

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>FMWK</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(CURRENT_RELEASE_VERSION)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(CURRENT_RELEASE_NUMBER)</string>
+	<key>NSPrincipalClass</key>
+	<string></string>
+</dict>
+</plist>

+ 943 - 0
Frameworks/FileKit/Tests/FileKitTests.swift

@@ -0,0 +1,943 @@
+//
+//  FileKitTests.swift
+//  FileKitTests
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2015-2017 Nikolai Vazquez
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+//  swiftlint:disable type_body_length
+//  swiftlint:disable file_length
+//
+
+import XCTest
+import FileKit
+
+class FileKitTests: XCTestCase {
+
+    // MARK: - Path
+
+    class Delegate: NSObject, FileManagerDelegate {
+        var expectedSourcePath: Path = ""
+        var expectedDestinationPath: Path = ""
+        func fileManager(
+            _ fileManager: FileManager,
+            shouldCopyItemAtPath srcPath: String,
+            toPath dstPath: String
+        ) -> Bool {
+            XCTAssertEqual(srcPath, expectedSourcePath.rawValue)
+            XCTAssertEqual(dstPath, expectedDestinationPath.rawValue)
+            return true
+        }
+    }
+
+    func testPathFileManagerDelegate() {
+        do {
+            var sourcePath = .userTemporary + "filekit_test_filemanager_delegate"
+            let destinationPath = Path("\(sourcePath)1")
+            try sourcePath.createFile()
+
+            var delegate: Delegate {
+                let delegate = Delegate()
+                delegate.expectedSourcePath = sourcePath
+                delegate.expectedDestinationPath = destinationPath
+                return delegate
+            }
+
+            let d1 = delegate
+            sourcePath.fileManagerDelegate = d1
+            XCTAssertTrue(d1 === sourcePath.fileManagerDelegate)
+
+            try sourcePath +>! destinationPath
+
+            var secondSourcePath = sourcePath
+            secondSourcePath.fileManagerDelegate = delegate
+            XCTAssertFalse(sourcePath.fileManagerDelegate === secondSourcePath.fileManagerDelegate)
+            try secondSourcePath +>! destinationPath
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+
+    }
+
+    func testFindingPaths() {
+        let homeFolders = Path.userHome.find(searchDepth: 0) { $0.isDirectory }
+        XCTAssertFalse(homeFolders.isEmpty, "Home folder is not empty")
+
+        let rootFiles = Path.root.find(searchDepth: 1) { !$0.isDirectory }
+        XCTAssertFalse(rootFiles.isEmpty)
+    }
+
+    func testPathStringLiteralConvertible() {
+        let a  = "/Users" as Path
+        let b: Path = "/Users"
+        let c = Path("/Users")
+        XCTAssertEqual(a, b)
+        XCTAssertEqual(a, c)
+        XCTAssertEqual(b, c)
+    }
+
+    func testPathStringInterpolationConvertible() {
+        let path: Path = "\(Path.userTemporary)/testfile_\(10)"
+        XCTAssertEqual(path.rawValue, Path.userTemporary.rawValue + "/testfile_10")
+    }
+
+    func testPathEquality() {
+        let a: Path = "~"
+        let b: Path = "~/"
+        let c: Path = "~//"
+        let d: Path = "~/./"
+        XCTAssertEqual(a, b)
+        XCTAssertEqual(a, c)
+        XCTAssertEqual(a, d)
+    }
+
+    func testStandardizingPath() {
+        let a: Path = "~/.."
+        let b: Path = "/Users"
+        XCTAssertEqual(a.standardized, b.standardized)
+    }
+
+    func testPathIsDirectory() {
+        let d = Path.systemApplications
+        XCTAssertTrue(d.isDirectory)
+    }
+
+    func testSequence() {
+        var i = 0
+        let parent = Path.userTemporary
+        for _ in parent {
+            i += 1
+        }
+        print("\(i) files under \(parent)")
+
+        i = 0
+        for (_, _) in Path.userTemporary.enumerated() {
+            i += 1
+        }
+    }
+
+    func testPathExtension() {
+        var path = Path.userTemporary + "file.txt"
+        XCTAssertEqual(path.pathExtension, "txt")
+        path.pathExtension = "pdf"
+        XCTAssertEqual(path.pathExtension, "pdf")
+    }
+
+    func testPathParent() {
+        let a: Path = "/"
+        let b: Path = a + "Users"
+        XCTAssertEqual(a, b.parent)
+    }
+
+    func testPathChildren() {
+        let p: Path = "/Users"
+        XCTAssertNotEqual(p.children(), [])
+    }
+
+    func testPathRecursiveChildren() {
+        let p: Path = Path.userTemporary
+        let children = p.children(recursive: true)
+        XCTAssertNotEqual(children, [])
+    }
+
+    func testRoot() {
+
+        let root = Path.root
+        XCTAssertTrue(root.isRoot)
+
+        XCTAssertEqual(root.standardized, root)
+        XCTAssertEqual(root.parent, root)
+
+        var p: Path = Path.userTemporary
+        XCTAssertFalse(p.isRoot)
+
+        while !p.isRoot { p = p.parent }
+        XCTAssertTrue(p.isRoot)
+
+        let empty = Path("")
+        XCTAssertFalse(empty.isRoot)
+        XCTAssertEqual(empty.standardized, empty)
+
+        XCTAssertTrue(Path("/.").isRoot)
+        XCTAssertTrue(Path("//").isRoot)
+    }
+
+    func testFamily() {
+        let p: Path = Path.userTemporary
+        let children = p.children()
+
+        guard let child = children.first else {
+            XCTFail("No child into \(p)")
+            return
+        }
+        XCTAssertTrue(child.isAncestorOfPath(p))
+        XCTAssertTrue(p.isChildOfPath(child))
+
+        XCTAssertFalse(p.isAncestorOfPath(child))
+        XCTAssertFalse(p.isAncestorOfPath(p))
+        XCTAssertFalse(p.isChildOfPath(p))
+
+        let directories = children.filter { $0.isDirectory }
+
+        if let directory = directories.first, directory.children().isEmpty {
+            try? (directory + "childOfChild").createDirectory()
+        }
+
+        guard let directory = directories.first, let childOfChild = directory.children().first else {
+            XCTFail("No child of child into \(p)")
+            return
+        }
+        XCTAssertTrue(childOfChild.isAncestorOfPath(p))
+        XCTAssertFalse(p.isChildOfPath(childOfChild, recursive: false))
+        XCTAssertTrue(p.isChildOfPath(childOfChild, recursive: true))
+
+
+        // common ancestor
+        XCTAssertTrue(p.commonAncestor(Path.root).isRoot)
+        XCTAssertEqual(.userDownloads <^> .userDocuments, Path.userHome)
+        XCTAssertEqual(("~/Downloads" <^> "~/Documents").rawValue, "~")
+    }
+
+    func testPathAttributes() {
+
+        let a = .userTemporary + "test.txt"
+        let b = .userTemporary + "TestDir"
+        do {
+            try "Hello there, sir" |> TextFile(path: a)
+            try b.createDirectory()
+        } catch {
+            XCTFail(String(describing: error))
+        }
+
+        for p in [a, b] {
+            print(String(describing: p.creationDate))
+            print(String(describing: p.modificationDate))
+            print(String(describing: p.ownerName))
+            print(String(describing: p.ownerID))
+            print(String(describing: p.groupName))
+            print(String(describing: p.groupID))
+            print(String(describing: p.extensionIsHidden))
+            print(String(describing: p.posixPermissions))
+            print(String(describing: p.fileReferenceCount))
+            print(String(describing: p.fileSize))
+            print(String(describing: p.filesystemFileNumber))
+            print(String(describing: p.fileType))
+            print("")
+        }
+    }
+
+    func testPathSubscript() {
+        let path = "~/Library/Preferences" as Path
+
+        let a = path[0]
+        XCTAssertEqual(a, "~")
+
+        let b = path[2]
+        XCTAssertEqual(b, path)
+    }
+
+    func testAddingPaths() {
+        let a: Path = "~/Desktop"
+        let b: Path = "Files"
+        XCTAssertEqual(a + b, "~/Desktop/Files")
+    }
+
+    func testPathPlusEquals() {
+        var a: Path = "~/Desktop"
+        a += "Files"
+        XCTAssertEqual(a, "~/Desktop/Files")
+    }
+
+
+    func testPathSymlinking() {
+        do {
+            let testDir: Path = .userTemporary + "filekit_test_symlinking"
+            if testDir.exists && !testDir.isDirectory {
+                try testDir.deleteFile()
+                XCTAssertFalse(testDir.exists)
+            }
+
+            try testDir.createDirectory()
+            XCTAssertTrue(testDir.exists)
+
+            let testFile = TextFile(path: testDir + "test_file.txt")
+            try "FileKit test" |> testFile
+            XCTAssertTrue(testFile.exists)
+
+            let symDir = testDir + "sym_dir"
+            if symDir.exists && !symDir.isDirectory {
+                try symDir.deleteFile()
+            }
+            try symDir.createDirectory()
+
+            // "/temporary/symDir/test_file.txt"
+            try testFile =>! symDir
+
+            let symPath = symDir + testFile.name
+            XCTAssertTrue(symPath.isSymbolicLink)
+
+            let symPathContents = try String(contentsOfPath: symPath)
+            XCTAssertEqual(symPathContents, "FileKit test")
+
+            let symLink = testDir + "test_file_link.txt"
+            try testFile =>! symLink
+            XCTAssertTrue(symLink.isSymbolicLink)
+
+            let symLinkContents = try String(contentsOfPath: symLink)
+            XCTAssertEqual(symLinkContents, "FileKit test")
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testPathOperators() {
+        let p: Path = "~"
+        let ps = p.standardized
+        XCTAssertEqual(ps, p%)
+        XCTAssertEqual(ps.parent, ps^)
+    }
+
+    func testCurrent() {
+        let oldCurrent: Path = .current
+        let newCurrent: Path = .userTemporary
+
+        XCTAssertNotEqual(oldCurrent, newCurrent) // else there is no test
+
+        Path.current = newCurrent
+        XCTAssertEqual(Path.current, newCurrent)
+
+        Path.current = oldCurrent
+        XCTAssertEqual(Path.current, oldCurrent)
+    }
+
+    func testChangeDirectory() {
+        Path.userTemporary.changeDirectory {
+            XCTAssertEqual(Path.current, Path.userTemporary)
+        }
+
+        Path.userDesktop </> {
+            XCTAssertEqual(Path.current, Path.userDesktop)
+        }
+
+        XCTAssertNotEqual(Path.current, Path.userTemporary)
+    }
+
+    func testVolumes() {
+        var volumes = Path.volumes()
+        XCTAssertFalse(volumes.isEmpty, "No volume")
+
+        for volume in volumes {
+            XCTAssertNotNil("\(volume)")
+        }
+
+        volumes = Path.volumes(.skipHiddenVolumes)
+        XCTAssertFalse(volumes.isEmpty, "No visible volume")
+
+        for volume in volumes {
+            XCTAssertNotNil("\(volume)")
+        }
+    }
+
+    func testURL() {
+        let path: Path = .userTemporary
+        let url = path.url
+        if let pathFromURL = Path(url: url) {
+            XCTAssertEqual(pathFromURL, path)
+
+            let subPath = pathFromURL + "test"
+            XCTAssertEqual(Path(url: url.appendingPathComponent("test")), subPath)
+        } else {
+            XCTFail("Not able to create Path from URL")
+        }
+    }
+
+    func testBookmarkData() {
+        let path: Path = .userTemporary
+        XCTAssertNotNil(path.bookmarkData)
+
+        if let bookmarkData = path.bookmarkData {
+            if let pathFromBookmarkData = Path(bookmarkData: bookmarkData) {
+                XCTAssertEqual(pathFromBookmarkData, path)
+            } else {
+                XCTFail("Not able to create Path from Bookmark Data")
+            }
+        }
+    }
+
+    func testGroupIdentifier() {
+        let path = Path(groupIdentifier: "com.nikolaivazquez.FileKitTests")
+        XCTAssertNotNil(path, "Not able to create Path from group identifier")
+    }
+
+    func testTouch() {
+        let path: Path = .userTemporary + "filekit_test.touch"
+        do {
+            if path.exists { try path.deleteFile() }
+            XCTAssertFalse(path.exists)
+
+            try path.touch()
+            XCTAssertTrue(path.exists)
+
+            guard let modificationDate = path.modificationDate else {
+                XCTFail("Failed to get modification date")
+                return
+            }
+
+            sleep(1)
+
+            try path.touch()
+
+            guard let newModificationDate = path.modificationDate else {
+                XCTFail("Failed to get modification date")
+                return
+            }
+
+            XCTAssertTrue(modificationDate < newModificationDate)
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testCreateDirectory() {
+        let dir: Path = .userTemporary + "filekit_testdir"
+
+        do {
+            if dir.exists { try dir.deleteFile() }
+        } catch {
+            XCTFail(String(describing: error))
+        }
+
+        defer {
+            do {
+                if dir.exists { try dir.deleteFile() }
+            } catch {
+                XCTFail(String(describing: error))
+            }
+        }
+
+        do {
+            XCTAssertFalse(dir.exists)
+            try dir.createDirectory()
+            XCTAssertTrue(dir.exists)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+        do {
+            XCTAssertTrue(dir.exists)
+            try dir.createDirectory(withIntermediateDirectories: false)
+            XCTFail("must throw exception")
+        } catch FileKitError.createDirectoryFail {
+            print("Create directory fail ok")
+        } catch {
+            XCTFail("Unknown error: " + String(describing: error))
+        }
+        do {
+            XCTAssertTrue(dir.exists)
+            try dir.createDirectory(withIntermediateDirectories: true)
+            XCTAssertTrue(dir.exists)
+        } catch {
+            XCTFail("Unexpected error: " + String(describing: error))
+        }
+    }
+
+    func testWellKnownDirectories() {
+        var paths: [Path] = [
+            .userHome, .userTemporary, .userCaches, .userDesktop, .userDocuments,
+            .userAutosavedInformation, .userDownloads, .userLibrary, .userMovies,
+            .userMusic, .userPictures, .userApplicationSupport, .userApplications,
+            .userSharedPublic
+        ]
+        paths += [
+            .systemApplications, .systemApplicationSupport, .systemLibrary,
+            .systemCoreServices, .systemPreferencePanes /* .systemPrinterDescription,*/
+        ]
+        #if os(OSX)
+            paths += [.userTrash] // .userApplicationScripts (not testable)
+        #endif
+
+        for path in paths {
+            XCTAssertTrue(path.exists, path.rawValue)
+        }
+
+        // all
+
+        XCTAssertTrue(Path.allLibraries.contains(.userLibrary))
+        XCTAssertTrue(Path.allLibraries.contains(.systemLibrary))
+        XCTAssertTrue(Path.allApplications.contains(.userApplications))
+        XCTAssertTrue(Path.allApplications.contains(.systemApplications))
+
+        // temporary
+        XCTAssertFalse(Path.processTemporary.exists)
+        XCTAssertFalse(Path.uniqueTemporary.exists)
+        XCTAssertNotEqual(Path.uniqueTemporary, Path.uniqueTemporary)
+    }
+
+    // MARK: - TextFile
+    let testFilePath: Path = .userTemporary + "filekit_test.txt"
+    let textFile = TextFile(path: .userTemporary + "filekit_test.txt")
+    let textFileWithoutExt = TextFile(path: .userTemporary + "filekit_test")
+
+    func testFileName() {
+        XCTAssertEqual(TextFile(path: "/Users/").name, "Users")
+    }
+
+    func testTextFileExtension() {
+        XCTAssertEqual(textFile.pathExtension, "txt")
+    }
+
+    func testTextFileWihoutExtension() {
+        XCTAssertEqual(textFile.nameWithoutExtension, "filekit_test")
+        XCTAssertEqual(textFileWithoutExt.nameWithoutExtension, "filekit_test")
+    }
+
+    func testTextFileExists() {
+        do {
+            try textFile.create()
+            XCTAssertTrue(textFile.exists)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testWriteToTextFile() {
+        do {
+            try textFile.write("This is some test.")
+            try textFile.write("This is another test.", atomically: false)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testTextFileOperators() {
+        do {
+            let text = "FileKit Test"
+
+            try text |> textFile
+            var contents = try textFile.read()
+            XCTAssertTrue(contents.hasSuffix(text))
+
+            try text |>> textFile
+            contents = try textFile.read()
+            XCTAssertTrue(contents.hasSuffix("\(text)\n\(text)"))
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testTextFileStreamReader() {
+        do {
+            let expectedLines = [
+                "Lorem ipsum dolor sit amet",
+                "consectetur adipiscing elit",
+                "Sed non risus"
+            ]
+            let separator = "\n"
+            try expectedLines.joined(separator: separator) |> textFile
+            
+            let reader = try textFile.streamReader() 
+            defer {
+                reader.close()
+            }
+            var lines = [String]()
+            for line in reader {
+                lines.append(line)
+            }
+            XCTAssertEqual(expectedLines, lines)
+            
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testTextFileGrep() {
+        do {
+            let expectedLines = [
+                "Lorem ipsum dolor sit amet",
+                "consectetur adipiscing elit",
+                "Sed non risus"
+            ]
+            let separator = "\n"
+            try expectedLines.joined(separator: separator) |> textFile
+
+            // all
+            var result = textFile | "e"
+            XCTAssertEqual(result, expectedLines)
+
+            // not all
+            result = textFile |- "e"
+            XCTAssertTrue(result.isEmpty)
+
+            // specific line
+            result = textFile | "eli"
+            XCTAssertEqual(result, [expectedLines[1]])
+
+            // the other line
+            result = textFile |- "eli"
+            XCTAssertEqual(result, [expectedLines[0], expectedLines[2]])
+
+            // regex
+            result = textFile |~ "e.*i.*e.*"
+            XCTAssertEqual(result, [expectedLines[0], expectedLines[1]])
+
+            // this not a regex
+            result = textFile | "e.*i.*e.*"
+            XCTAssertTrue(result.isEmpty)
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+    
+    func testTextFileStreamWritter() {
+        if testFilePath.exists {
+            try? testFilePath.deleteFile()
+        }
+        do {
+            let lines = [
+                "Lorem ipsum dolor sit amet",
+                "consectetur adipiscing elit",
+                "Sed non risus"
+            ]
+            let separator = "\n"
+            
+            let writer = try textFile.streamWriter(separator)
+            defer {
+                writer.close()
+            }
+            for line in lines {
+                let delim = line != lines.last
+                writer.write(line: line, delim: delim)
+            }
+            
+            let expected = try textFile.read()
+            let expectedLines = expected.components(separatedBy: separator)
+            XCTAssertEqual(expectedLines, lines)
+            
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - FileType
+
+    func testFileTypeComparable() {
+        let textFile1 = TextFile(path: .userTemporary + "filekit_test_comparable1.txt")
+        let textFile2 = TextFile(path: .userTemporary + "filekit_test_comparable2.txt")
+        do {
+            try "1234567890" |> textFile1
+            try "12345"      |> textFile2
+            XCTAssert(textFile1 > textFile2)
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - FilePermissions
+
+    func testFilePermissions() {
+        let swift: Path = "/usr/bin/swift"
+        if swift.exists {
+            XCTAssertTrue(swift.filePermissions.contains([.read, .execute]))
+        }
+
+        let file: Path = .userTemporary + "filekit_test_filepermissions"
+
+        do {
+            try file.createFile()
+            XCTAssertTrue(file.filePermissions.contains([.read, .write]))
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - DictionaryFile
+
+    let nsDictionaryFile = NSDictionaryFile(path: .userTemporary + "filekit_test_nsdictionary.plist")
+
+    func testWriteToNSDictionaryFile() {
+        do {
+            let dict = NSMutableDictionary()
+            dict["FileKit" as NSString] = true
+            dict["Hello" as NSString] = "World"
+
+            try nsDictionaryFile.write(dict)
+            let contents = try nsDictionaryFile.read()
+            XCTAssertEqual(contents, dict)
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - DictionaryFile
+
+    let dictionaryFile = DictionaryFile<String, Any>(path: .userTemporary + "filekit_test_dictionary.plist")
+
+    func testWriteToDictionaryFile() {
+        do {
+            var dict: [String: Any] = [:]
+            dict["FileKit"] = true
+            dict["Hello"] = "World"
+
+            try dictionaryFile.write(dict)
+            let contents = try dictionaryFile.read()
+
+            XCTAssertEqual(contents.count, dict.count)
+
+            for (kc, vc) in contents {
+                let v = dict[kc]
+
+                if let vb = v as? Bool , let vcb = vc as? Bool {
+                    XCTAssertEqual(vb, vcb)
+                }
+                else if let vb = v as? String , let vcb = vc as? String {
+                    XCTAssertEqual(vb, vcb)
+                }
+                else {
+                    XCTFail("unknow type")
+                }
+            }
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - ArrayFile
+
+    let nsArrayFile = NSArrayFile(path: .userTemporary + "filekit_test_nsarray.plist")
+
+    func testWriteToNSArrayFile() {
+        do {
+            let array: NSArray = ["ABCD", "WXYZ"]
+
+            try nsArrayFile.write(array)
+            let contents = try nsArrayFile.read()
+            XCTAssertEqual(contents, array)
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - ArrayFile
+
+    let arrayFile = ArrayFile<String>(path: .userTemporary + "filekit_test_array.plist")
+
+    func testWriteToArrayFile() {
+        do {
+            let array = ["ABCD", "WXYZ"]
+
+            try arrayFile.write(array)
+            let contents = try arrayFile.read()
+            XCTAssertEqual(contents, array)
+
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - NSDataFile
+
+    let nsDataFile = NSDataFile(path: .userTemporary + "filekit_test_nsdata")
+
+    func testWriteToNSDataFile() {
+        do {
+            let data = ("FileKit test" as NSString).data(using: String.Encoding.utf8.rawValue)! as NSData
+            try nsDataFile.write(data)
+            let contents = try nsDataFile.read()
+            XCTAssertEqual(contents, data)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - DataFile
+
+    let dataFile = DataFile(path: .userTemporary + "filekit_test_data")
+
+    func testWriteToDataFile() {
+        do {
+            let data = "FileKit test".data(using: String.Encoding.utf8)!
+            try dataFile.write(data)
+            let contents = try dataFile.read()
+            XCTAssertEqual(contents, data)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - String+FileKit
+
+    let stringFile = File<String>(path: .userTemporary + "filekit_stringtest.txt")
+
+    func testStringInitializationFromPath() {
+        do {
+            let message = "Testing string init..."
+            try stringFile.write(message)
+            let contents = try String(contentsOfPath: stringFile.path)
+            XCTAssertEqual(contents, message)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testStringWriting() {
+        do {
+            let message = "Testing string writing..."
+            try message.write(to: stringFile.path)
+            let contents = try String(contentsOfPath: stringFile.path)
+            XCTAssertEqual(contents, message)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - Image
+
+    func testImageWriting() {
+        let url = URL(string: "https://raw.githubusercontent.com/nvzqz/FileKit/assets/logo.png")!
+        let img = Image(contentsOf: url) ?? Image()
+        do {
+            let path: Path = .userTemporary + "filekit_imagetest.png"
+            try img.write(to: path)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    // MARK: - Watch
+
+    func testWatch() {
+        let pathToWatch = .userTemporary + "filekit_test_watch"
+        let expectation = "event"
+        let operation = {
+            do {
+                let message = "Testing file system event when writing..."
+                try message.write(to: pathToWatch, atomically: false)
+            } catch {
+                XCTFail(String(describing: error))
+            }
+        }
+
+        // Do watch test
+        let expt = self.expectation(description: expectation)
+        let watcher = pathToWatch.watch { event in
+            print(event)
+            // XXX here could check expected event type according to operation
+            expt.fulfill()
+        }
+        defer {
+            watcher.close()
+        }
+        operation()
+        self.waitForExpectations(timeout: 10, handler: nil)
+    }
+
+    // MARK: - Codable
+
+    func testJSONCodable() {
+        let encodable = TestJSONCodable(string: "test", integer: 9)
+        let path: Path = .userTemporary + "testcodable.json"
+        let codableFile = File<TestJSONCodable>(path: path)
+
+        do {
+            try codableFile.write(encodable)
+            let decodable: TestJSONCodable = try codableFile.read()
+            XCTAssertEqual(decodable, encodable)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+    
+    func testCodable() {
+        let encodable = TestCodable(string: "test", integer: 9)
+        let path: Path = .userTemporary + "testcodable.plist"
+
+        do {
+            try FileKit.write(encodable, to: path)
+            let decodable: TestCodable = try FileKit.read(from: path)
+            XCTAssertEqual(decodable, encodable)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+    func testCodableArray() {
+        let encodable = [TestCodable(string: "test", integer: 9), TestCodable(string: "test3", integer: 8)]
+        let path: Path = .userTemporary + "testcodablearray.plist"
+
+        do {
+            try FileKit.write(encodable, to: path)
+            let decodable: [TestCodable] = try FileKit.read(from: path)
+            XCTAssertEqual(decodable, encodable)
+        } catch {
+            XCTFail(String(describing: error))
+        }
+    }
+
+}
+
+// MARK: Test objects
+
+struct TestCodable: Codable {
+    let string: String
+    let integer: Int
+}
+
+extension TestCodable: Equatable {
+    static func == (l: TestCodable, r: TestCodable) -> Bool {
+        return l.string == r.string && l.integer == r.integer
+    }
+}
+
+struct TestJSONCodable: Codable {
+    let string: String
+    let integer: Int
+}
+
+extension TestJSONCodable: JSONWritable, JSONReadable {}
+
+extension TestJSONCodable: Equatable {
+    static func == (l: TestJSONCodable, r: TestJSONCodable) -> Bool {
+        return l.string == r.string && l.integer == r.integer
+    }
+}
+
+struct TestPropertyListCodable: Codable {
+    let string: String
+    let integer: Int
+}
+
+extension TestPropertyListCodable: PropertyListWritable, PropertyListReadable {}
+
+extension TestPropertyListCodable: Equatable {
+    static func == (l: TestPropertyListCodable, r: TestPropertyListCodable) -> Bool {
+        return l.string == r.string && l.integer == r.integer
+    }
+}
+

+ 24 - 0
Frameworks/FileKit/Tests/Info.plist

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 44 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin.podspec

@@ -0,0 +1,44 @@
+# Start from https://github.com/CocoaPods/pod-template/blob/master/NAME.podspec
+#
+# Be sure to run `pod lib lint ${POD_NAME}.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+  s.name             = 'LiveKitPlugin'
+  s.version          = '1.0.0'
+  s.summary          = 'LiveKitPlugin components'
+
+# This description is used to generate tags and improve search results.
+#   * Think: What does it do? Why did you write it? What is the focus?
+#   * Try to keep it short, snappy and to the point.
+#   * Write the description between the DESC delimiters below.
+#   * Finally, don't worry about the indent, CocoaPods strips it!
+
+  s.description      = <<-DESC
+Contains the decomponents for Design System.
+                       DESC
+
+  s.homepage         = 'https://github.com/Hoxin'
+  s.license          = 'MIT'
+  s.author           = 'MIT'
+  s.source           = { :path => '.' }
+  s.version          = "1.0.1"
+
+  s.ios.deployment_target = '12.0'
+  s.swift_versions = '5.3'
+  
+  #  s.source_files = 'LiveKitPlugin/Classes/**/*'
+  s.subspec "Source" do |sp|
+    sp.public_header_files = 'LiveKitPlugin/Classes/**/*.{h,hpp}'
+    sp.source_files  = 'LiveKitPlugin/Classes/**/*'
+  end
+  s.resources    = 'LiveKitPlugin/Resources/**/*'
+  s.vendored_libraries = 'LiveKitPlugin/Classes/**/*.a'
+  
+  s.dependency 'LiveKitClient', '2.0.5'
+  s.dependency 'LiveKitWebRTC', '114.5735.14'
+end

+ 14 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Common/Macro.swift

@@ -0,0 +1,14 @@
+//
+//  Macro.swift
+//  Alamofire
+//
+//  Created by Bugu on 2024/6/1.
+//
+
+import Foundation
+
+let kTimerUpdateNotification = "kTimerUpdateNotification"
+
+enum FilePath {
+    static let localImagePath = "LiveKit/Images"
+}

+ 96 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Config/AppStorage.swift

@@ -0,0 +1,96 @@
+//
+//  AppStorage.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2023/8/29.
+//
+
+import UIKit
+
+class AppStorage {
+    
+     static let shared = AppStorage()
+    
+    /// 当前用户
+    var user: UserAccount!
+    
+    /// token
+    var token: String?
+    
+    /// Http URL
+    var host: String!
+    
+    /// 用户头像规则 如:http://api.xxx.com/file/user-icon/{uid} ,{uid}为占位符
+    var userAvatarDomain: String?
+    
+    /// 联系人
+    var contacts: [UserAccount] = []
+    
+    /// 群组信息
+    var groupMembers: [UserAccount] = []
+    
+    /// Livekit 服务端地址
+    var liveKitServer: String!
+    
+    /// 主 window
+    var window: UIWindow? {
+        return AppStorage.getWindow()
+    }
+}
+
+extension AppStorage {
+    
+    func getContact(userId: String) -> UserAccount? {
+        guard userId.count > 0 else { return nil }
+        if userId == user?.id { return user }
+        if let userAccount = contacts.first(where: { $0.id == userId })  {
+            return userAccount
+        }
+        
+        if let userAccount = groupMembers.first(where: { $0.id == userId })  {
+            return userAccount
+        }
+        
+        return nil
+    }
+    
+  static func getWindow() -> UIWindow? {
+      guard let window = UIApplication.shared.connectedScenes
+           .filter({ $0.activationState == .foregroundActive })
+           .map({ $0 as? UIWindowScene })
+           .compactMap({ $0 })
+           .last?.windows
+           .filter({ $0.isKeyWindow })
+           .last else {
+               printLog("getWindow \(UIApplication.shared.keyWindow!)")
+               return UIApplication.shared.keyWindow!
+           }
+       printLog("getWindow \(window)")
+       return window
+    }
+    
+    
+}
+
+extension AppStorage {
+    
+    /// 找到传入的在通话中 的用户
+    /// - Parameters:
+    ///   - userIds: 通话中的用户id
+    ///   - members: 所有成员用户
+    /// - Returns:通话中的用户对象
+    static func getAccounts(userIds: [String], members: [UserAccount]) -> [UserAccount] {
+        var selectMembers: [UserAccount] = []
+        userIds.forEach { id in
+            guard let account = getAccount(userId: id, members: members) else { return }
+            selectMembers.append(account)
+        }
+        return selectMembers
+    }
+    
+    static func getAccount(userId: String, members: [UserAccount]) -> UserAccount? {
+        guard userId.count > 0 else { return nil }
+        if userId == AppStorage.shared.user?.id { return AppStorage.shared.user }
+        return members.first(where: { $0.id == userId })
+    }
+}

+ 22 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Adaptive+Extensions.swift

@@ -0,0 +1,22 @@
+//
+//  Adaptive+Extensions.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/30.
+//
+
+import Foundation
+
+extension Int {
+    var adapt: Int {
+        let result = ceil(CGFloat(self) * ScreenHeight / 667.0)
+        return Int(result)
+    }
+}
+
+extension CGFloat {
+    var adapt: CGFloat {
+        let result = ceil(CGFloat(self) * ScreenHeight / 667.0)
+        return CGFloat(result)
+    }
+}

+ 30 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Bundle+HxExt.swift

@@ -0,0 +1,30 @@
+//
+//  File.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/10/28.
+//
+
+import Foundation
+
+
+extension Bundle {
+    
+    static var bundle: Bundle?  {
+        if let bundlePath = Bundle(for: HoxinLiveKitPlugin.self).resourcePath,
+           let bundle = Bundle(path: bundlePath) {
+            return bundle
+        }
+        return nil
+    }
+    
+    static var livekitPluginBundle: Bundle?  {
+        if let bundlePath = Bundle(for: HoxinLiveKitPlugin.self).resourcePath,
+           let bundle = Bundle(path: (bundlePath as NSString).appendingPathComponent("LiveKitPlugin.bundle")) {
+            return bundle
+        }
+        return nil
+       
+    }
+  
+}

+ 75 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Color+Extension.swift

@@ -0,0 +1,75 @@
+//
+//  Color+Extension.swift
+//  superApp
+//
+//  Created by tancheng on 2023/5/24.
+//  Copyright © 2023 neuxnet. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+extension UIColor {
+    convenience init?(color name: String) {
+        guard let bundle = Bundle.bundle else {
+            return nil
+        }
+        self.init(named: name, in: bundle, compatibleWith: nil)
+    }
+}
+
+
+extension UIColor {
+    convenience init(_ hex: Int, alpha: CGFloat = 1) {
+        let redValue = CGFloat((hex & 0xFF0000) >> 16)/255.0
+        let greenValue = CGFloat((hex & 0xFF00) >> 8)/255.0
+        let blueValue = CGFloat(hex & 0xFF)/255.0
+        self.init(red: redValue, green: greenValue, blue: blueValue, alpha: alpha)
+    }
+    
+    convenience init(_ lHex: Int, _ dHex: Int) {
+        self.init(light: UIColor(lHex), dark: UIColor(dHex))
+    }
+    
+    convenience init(light lColor: UIColor, dark dColor: UIColor) {
+        if #available(iOS 13.0, *) {
+            self.init { $0.userInterfaceStyle == .dark ? dColor : lColor }
+        } else {
+            self.init(cgColor: lColor.cgColor)
+        }
+    }
+    ///color:“#123456”、 “0X123456”、 “123456”
+    class func hex(hexString: String, alpha:CGFloat = 1) -> UIColor {
+        if hexString.isEmpty { return .clear }
+        
+        var cString: String = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
+        // String should be 6 or 8 characters
+        if cString.count < 6 { return UIColor.clear}
+        // strip 0X if it appears
+        let index = cString.index(cString.endIndex, offsetBy: -6)
+        let subString = cString[index...]
+        if cString.hasPrefix("0X") { cString = String(subString) }
+        if cString.hasPrefix("#") { cString = String(subString) }
+        
+        if cString.count != 6 { return UIColor.clear }
+        // Separate into r, g, b substrings
+        var range: NSRange = NSMakeRange(0, 2)
+        //r
+        let rString = (cString as NSString).substring(with: range)
+        //g
+        range.location = 2
+        let gString = (cString as NSString).substring(with: range)
+        //b
+        range.location = 4
+        let bString = (cString as NSString).substring(with: range)
+        // Scan values
+        var r: UInt32 = 0x0
+        var g: UInt32 = 0x0
+        var b: UInt32 = 0x0
+    
+        Scanner(string: rString).scanHexInt32(&r)
+        Scanner(string: gString).scanHexInt32(&g)
+        Scanner(string: bString).scanHexInt32(&b)
+        return UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: alpha)
+    }
+}

+ 14 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/Localized+Extensions.swift

@@ -0,0 +1,14 @@
+//
+//  Localized+Extensions.swift
+//  paas-dev
+//
+//  Created by Bugu on 2024/4/24.
+//  Copyright © 2024 neuxnet. All rights reserved.
+//
+
+import Foundation
+
+func Localized(_ key: String) -> String {
+    guard let bundle = Bundle.bundle else { return "" }
+    return NSLocalizedString(key, tableName: "LivekitLocalizable", bundle: bundle, comment: "")
+}

+ 21 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/NSDataAsset+Extensions.swift

@@ -0,0 +1,21 @@
+//
+//  NSDataAsset+Extensions.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/25.
+//
+
+import Foundation
+import UIKit
+
+extension NSDataAsset {
+    
+    convenience init?(data name: String) {
+        guard let bundle = Bundle.bundle else {
+            return nil
+        }
+        self.init(name: name, bundle: bundle)
+    }
+    
+   
+}

+ 42 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/String+Extensions.swift

@@ -0,0 +1,42 @@
+//
+//  String+Extensions.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/6/1.
+//
+
+import Foundation
+
+extension String {
+    /// SwifterSwift: Integer value from string (if applicable).
+    ///
+    ///        "101".int -> 101
+    ///
+    var int: Int? {
+        return Int(self)
+    }
+    
+    func appendingPathComponent(_ str: String) -> String {
+        return (self as NSString).appendingPathComponent(str)
+    }
+}
+
+extension String {
+    var isEmpty: Bool {
+        return startIndex == endIndex
+    }
+    
+    var isNotEmpty: Bool {
+        return !isEmpty
+    }
+}
+
+extension Optional where Wrapped == String {
+    var isEmpty: Bool {
+        return self?.isEmpty ?? true
+    }
+    
+    var isNotEmpty: Bool {
+        return !isEmpty
+    }
+}

+ 15 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIFont+Extension.swift

@@ -0,0 +1,15 @@
+//
+//  UIFont+Extension.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/18.
+//
+
+import Foundation
+import UIKit
+
+extension UIFont {
+    static func font(ofSize fontSize: CGFloat, weight: UIFont.Weight = .regular) -> UIFont {
+        return .systemFont(ofSize: fontSize, weight: weight)
+    }
+}

+ 24 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIImage+HxExt.swift

@@ -0,0 +1,24 @@
+//
+//  UIimage+HxExt.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/8/29.
+//
+
+import UIKit
+
+extension UIImage {
+    convenience init?(image name: String) {
+        guard let bundle = Bundle.bundle else {
+            return nil
+        }
+        self.init(named: name, in: bundle, compatibleWith: nil)
+    }
+    
+    var iconImage: UIImage {
+        let iconColor = UIColor(light: UIColor(0x4F4F4F), dark: UIColor(0xC3C3C3))
+        return self.withTintColor(iconColor)
+    }
+    
+    
+}

+ 8 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIImageView+Extensions.swift

@@ -0,0 +1,8 @@
+//
+//  UIImageView+Extensions.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/25.
+//
+
+import Foundation

+ 67 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Extensions/UIView+Extensions.swift

@@ -0,0 +1,67 @@
+//
+//  UIView+Extensions.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/20.
+//
+
+import Foundation
+import UIKit
+
+extension UIView {
+
+    // MARK:  切圆角
+    // Parameters:
+    //   corners: 需要实现为圆角的角,可传入多个
+    //   radii: 圆角半径
+    
+//    setNeedsLayout:告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用layoutSubviews。
+//    layoutIfNeeded:告知页面布局立刻更新。所以一般都会和setNeedsLayout一起使用。如果希望立刻生成新的frame需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。
+//    layoutSubviews:系统重写布局
+//    setNeedsUpdateConstraints:告知需要更新约束,但是不会立刻开始
+//    updateConstraintsIfNeeded:告知立刻更新约束
+//    updateConstraints:系统更新约束
+    
+    func setCornerRadius(_ radius: CGFloat, _ corners: UIRectCorner?) {
+        if (corners != nil) {
+            DispatchQueue.main.async {
+                self.layoutIfNeeded()
+                let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners!, cornerRadii: CGSize(width: radius, height: radius))
+                let shapeLayer = CAShapeLayer()
+                shapeLayer.frame = self.bounds
+                shapeLayer.path = maskPath.cgPath
+                self.layer.mask = shapeLayer
+            }
+        }else{
+            self.layer.cornerRadius = radius
+            self.layer.masksToBounds = true
+        }
+    }
+    
+    func cleanMask() {
+        self.layer.mask = nil
+    }
+    
+    func setPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
+        self.setContentHuggingPriority(priority, for: axis)
+        self.setContentCompressionResistancePriority(priority, for: axis)
+    }
+    
+    func startRotating(duration: CGFloat = 1.5) {
+        // 创建旋转动画
+        let rotation = CABasicAnimation(keyPath: "transform.rotation")
+        rotation.fromValue = 0.0
+        rotation.toValue = -Double.pi * 2 // 旋转360度
+        rotation.duration = duration // 旋转一圈所需的时间
+        rotation.repeatCount = .infinity // 无限次旋转
+        
+        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
+            // 添加动画到图层
+            self.layer.add(rotation, forKey: "rotationAnimation")
+        }
+    }
+    
+    func stopRotating() {
+        self.layer.removeAnimation(forKey: "rotationAnimation")
+    }
+}

+ 298 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Http/MeetingHttpManager.swift

@@ -0,0 +1,298 @@
+//
+//  MeetingManager.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+
+private let provider = APIProvider<MeetingAPI>()
+
+struct MeetingManager {
+    
+}
+
+
+// MARK: - room
+extension MeetingManager {
+    
+    /// 创建房间
+    /// - Parameters:
+    ///   - title: 标题
+    ///   - description: 会议内容
+    ///   - dueTime: 会议预约时间
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func create(title: String?, description: String?, dueTime: String?, success: @escaping (_ meetingId: String) -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.create(title: title, description: description, dueTime: dueTime)) { result in
+            switch result {
+            case .success(let respone):
+                let dic = respone.mapDictionaryObject()
+                guard let meetingId = dic?["data"] as? String else {
+                    failed(-1, "miss meetingId")
+                    return
+                }
+                success(meetingId)
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 结束会议
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func endMeeting(meetingId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.finish(meetingId: meetingId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 获取房间token
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func getMeetingToken(meetingId: String, success: @escaping (_ token: String) -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.getToken(meetingId: meetingId)) { result in
+            switch result {
+            case .success(let respone):
+                let dic = respone.mapDictionaryObject()
+                guard let token = dic?["data"] as? String else {
+                    failed(-1, "miss token")
+                    return
+                }
+                success(token)
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 获取房间信息
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func getMeetingInfo(meetingId: String, success: @escaping (_ meetingInfo: MeetingInfoModel) -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.fetchMeetingInfo(meetingId: meetingId)) { result in
+            switch result {
+            case .success(let respone):
+                
+                guard let dic = respone.mapDictionaryObject(), let data = dic["data"] as? [ String : Any] else {
+                    failed(-1, "miss meeting info")
+                    return
+                }
+                guard let meetingInfo = MeetingInfoModel.dictionaryToModel(dic: data) else {
+                    failed(-1, "miss meeting info")
+                    return
+                }
+                success(meetingInfo)
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    
+    /// 拒绝加入房间
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func reject(meetingId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.reject(meetingId: meetingId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 启用全员禁言
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func muteAll(meetingId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.muteAll(meetingId: meetingId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 解除全员禁言
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func cancelMuteAll(meetingId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.cancelMute(meetingId: meetingId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+}
+
+// MARK: - member
+extension MeetingManager {
+    
+    /// 呼叫多人会议房间成员,Livekit SDK 加入成功后调用
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - members: 成员信息
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func ringMeetingMember(meetingId: String, members: [UserAccount], success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.ring(meetingId: meetingId, members: members)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 邀请成员
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - members: 成员信息
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func inviteMember(meetingId: String, members: [UserAccount], success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.invite(meetingId: meetingId, members: members)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 踢出成员
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - userId: 成员id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func kickoutMember(meetingId: String, userId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.kickoutMember(meetingId: meetingId, userId: userId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 开启成员麦克风
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - userId: 成员id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func openMemberAudio(meetingId: String, userId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.openMemberAudio(meetingId: meetingId, userId: userId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 关闭成员麦克风
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - userId: 成员id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func closeMemberAudio(meetingId: String, userId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.closeMemberAudio(meetingId: meetingId, userId: userId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 开启成员摄像头
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - userId: 成员id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func openMemberVideo(meetingId: String, userId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.openMemberCamera(meetingId: meetingId, userId: userId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 关闭成员摄像头
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - userId: 成员id
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func closeMemberVideo(meetingId: String, userId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.closeMemberCamera(meetingId: meetingId, userId: userId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+}
+
+
+// MARK: - message
+extension MeetingManager {
+    
+    /// 会议发送消息
+    /// - Parameters:
+    ///   - meetingId: 会议id
+    ///   - content: 消息体
+    ///   - success: 成功
+    ///   - failed: 失败
+    static func sendMessage(meetingId: String, content: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.sendMessage(meetingId: meetingId, content: content)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.hxErrorCode, error.hxErrorMessage ?? "")
+            }
+        }
+    }
+}

+ 169 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Http/P2PHttpManager.swift

@@ -0,0 +1,169 @@
+//
+//  File.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/8/31.
+//
+
+import Foundation
+//import Moya
+
+private let provider = APIProvider<P2PAPI>()
+
+// MARK: - 单人音视频处理
+struct P2PHttpManager {
+    
+    /// 发起单人语音通话
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    ///   - audio: 自己音频是否能用
+    static func callAudio(targetId: Int, success: @escaping (_ roomId: String, _ token: String) -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.callAudio(targetId: targetId)) { result in
+            switch result {
+            case .success(let response):
+                let dic = response.mapDictionaryObject()
+                guard let data = dic?["data"] as? [String: Any] else {
+                    failed(-1, "error data")
+                    return
+                }
+                
+                guard let token = data["token"] as? String else {
+                    failed(-1, "miss token")
+                    return
+                }
+                
+                guard let roomId = data["tag"] as? String else {
+                    failed(-1, "miss roomId")
+                    return
+                }
+                
+                success(roomId, token)
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 发起单人视频通话
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    ///   - audio: 自己音频是否能用
+    ///   - video: 自己视频是否能用
+    static func callVideo(targetId: Int, success: @escaping (_ roomId: String, _ token: String) -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.callVideo(targetId: targetId)) { result in
+            switch result {
+            case .success(let response):
+                let dic = response.mapDictionaryObject()
+                guard let data = dic?["data"] as? [String: Any] else {
+                    failed(-1, "error data")
+                    return
+                }
+                
+                guard let token = data["token"] as? String else {
+                    failed(-1, "miss token")
+                    return
+                }
+                
+                guard let roomId = data["tag"] as? String else {
+                    failed(-1, "miss roomId")
+                    return
+                }
+                
+                success(roomId, token)
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 接收通话请求
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    ///   - audio: 自己音频是否能用
+    ///   - video: 自己视频是否能用
+    static func accept(targetId: Int, roomId: String, success: @escaping (_ token: String) -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.accept(targetId: targetId, roomId: roomId)) { result in
+            switch result {
+            case .success(let response):
+                let dic = response.mapDictionaryObject()
+                guard let token = dic?["data"] as? String else {
+                    failed(-1, "miss token")
+                    return
+                }
+                success(token)
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 反馈正忙
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    static func busy(targetId: Int, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.busy(targetId: targetId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 取消呼叫
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    static func cancel(targetId: Int, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.cancel(targetId: targetId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 挂断电话
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    static func hangup(targetId: Int, roomId: String, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.hangup(targetId: targetId, roomId: roomId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 拒绝通话
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    static func reject(targetId: Int, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.reject(targetId: targetId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+    
+    /// 告知对方我已经响铃
+    /// - Parameters:
+    ///   - targetId: 对方 userId
+    static func ring(targetId: Int, success: @escaping () -> Void, failed: @escaping (_ code: Int, _ des: String) -> Void) {
+        provider.request(.ring(targetId: targetId)) { result in
+            switch result {
+            case .success:
+                success()
+            case .failure(let error):
+                failed(error.httpErrorCode, error.httpErrorMessage ?? "")
+            }
+        }
+    }
+}

+ 121 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/ListenerManager.swift

@@ -0,0 +1,121 @@
+//
+//  ListenerManager.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+
+private var listenerMap: NSMapTable<NSString, AnyObject> = NSMapTable(keyOptions: .strongMemory, valueOptions: .weakMemory)
+
+class ListenerManager {
+    
+    //-------------------单例对象----------------
+    /// 聊天单例对象
+    static let shared = ListenerManager()
+    
+    /// 单人聊天 事件回调
+    var p2pMessageListeners: [P2PMessageListener] {
+        return getP2PMessageListeners()
+    }
+    
+    /// 会议消息事件回调
+    var roomMessageListeners: [MeetingMessageListener] {
+        return getMeetingMessageListeners()
+    }
+    
+    /// livekit 房间事件多代理
+    var livekitListeners: [MeetingRoomListener] {
+        return getMeetingRoomListeners()
+    }
+    
+    /// livekit 房间事件单代理
+    weak var livekitSampleListener: MeetingRoomSampleListener?
+}
+
+// MARK: - 消息 监听器
+extension ListenerManager {
+    
+    /// 添加消息监听器
+    /// - Parameter meetingMessageListener: 对象
+    func addMeetingMessageListener(_ meetingMessageListener: MeetingMessageListener) {
+        listenerMap.setObject(meetingMessageListener, forKey: meetingMessageListener.uniqueID as NSString)
+    }
+    
+    /// 移除 消息监听器
+    /// - Parameter meetingMessageListener: 对象
+    func removeMeetingMessageListener(_ meetingMessageListener: MeetingMessageListener) {
+        listenerMap.removeObject(forKey: meetingMessageListener.uniqueID as NSString)
+    }
+    
+    /// 做一个弱引用
+    func getMeetingMessageListeners() -> [MeetingMessageListener] {
+        var listeners: [MeetingMessageListener] = []
+        
+        listenerMap.objectEnumerator()?.forEach({ value in
+            if let meetingListener = value as? MeetingMessageListener {
+                listeners.append(meetingListener)
+            }
+        })
+        return listeners
+    }
+}
+
+// MARK: - 会议房间 监听器
+extension ListenerManager {
+    
+    /// 添加房间监听器
+    /// - Parameter meetingRoomListener: 对象
+    func addMeetingRoomListener(_ meetingRoomListener: MeetingRoomListener) {
+        listenerMap.setObject(meetingRoomListener, forKey: meetingRoomListener.uniqueID as NSString)
+    }
+    
+    /// 移除房间监听器
+    /// - Parameter meetingRoomListener: 对象
+    func removeMeetingRoomListener(_ meetingRoomListener: MeetingRoomListener) {
+        listenerMap.removeObject(forKey: meetingRoomListener.uniqueID as NSString)
+    }
+    
+    
+    /// 做一个弱引用
+    func getMeetingRoomListeners() -> [MeetingRoomListener] {
+        var listeners: [MeetingRoomListener] = []
+        
+        listenerMap.objectEnumerator()?.forEach({ value in
+            if let livekitListener = value as? MeetingRoomListener {
+                listeners.append(livekitListener)
+            }
+        })
+        return listeners
+    }
+}
+
+// MARK: - RTC监听器
+extension ListenerManager {
+    
+    /// 添加 RTC 监听器 [可以添加多个]
+    /// - Parameter webRTCListener: 对象
+    func addP2PMessageListener(_ p2pListener: P2PMessageListener) {
+        listenerMap.setObject(p2pListener, forKey: p2pListener.uniqueID as NSString)
+    }
+    
+    /// 移除 RTC 监听器
+    /// - Parameter webRTCListener: 对象
+    func removeP2PMessageListener(_ p2pListener: P2PMessageListener) {
+        listenerMap.removeObject(forKey: p2pListener.uniqueID as NSString)
+    }
+    
+    func getP2PMessageListeners() -> [P2PMessageListener] {
+        var listeners: [P2PMessageListener] = []
+        
+        listenerMap.objectEnumerator()?.forEach({ value in
+            if let p2pListener = value as? P2PMessageListener {
+                listeners.append(p2pListener)
+            }
+        })
+        
+        return listeners
+    }
+}
+

+ 26 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/Base/IMBaseListener.swift

@@ -0,0 +1,26 @@
+//
+//  IMBaseListener.swift
+//  bugu
+//
+//  Created by Bugu on 2023/7/14.
+//  Copyright © 2023 Bugu. All rights reserved.
+//
+
+import Foundation
+
+protocol IMBaseListener: NSObject {
+    
+    /// 类唯一 id
+    var uniqueID: String { get }
+    
+    /// 比较是否相等
+    func isEquaTo(_ value: IMBaseListener) -> Bool
+}
+
+extension IMBaseListener {
+    
+    /// 比较是否相等
+    func isEquaTo(_ value: IMBaseListener) -> Bool {
+       return self.uniqueID == value.uniqueID
+    }
+}

+ 51 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/MeetingMessageListener.swift

@@ -0,0 +1,51 @@
+//
+//  MeetingMessageListener.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2023/9/1.
+//
+
+import Foundation
+
+protocol MeetingMessageListener: IMBaseListener {
+    
+    ///  收到会议邀请请求
+    /// - Parameters:
+    ///   - meetingInfo: 会议信息
+    func receiveMeetingInvite(meetingInfo: MeetingInfoModel)
+    
+    /// 收到预约会议请求
+    /// - Parameters:
+    ///   - meetingInfo: 会议信息
+    func receiveMeetingReservation(meetingInfo: MeetingInfoModel)
+    
+    /// 会议已经在其他设备接听
+    /// - Parameter meetingId: 会议Id
+    func receiveOthersDeviceAccept(meetingId: String)
+    
+    /// 会议已经在其他设备拒接
+    /// - Parameter meetingId: 会议Id
+    func receiveOthersDeviceReject(meetingId: String)
+}
+
+extension MeetingMessageListener {
+    
+    ///  收到会议邀请请求
+    /// - Parameters:
+    ///   - meetingInfo: 会议信息
+    func receiveMeetingInvite(meetingInfo: MeetingInfoModel) { }
+    
+    /// 收到预约会议请求
+    /// - Parameters:
+    ///   - meetingInfo: 会议信息
+    func receiveMeetingReservation(meetingInfo: MeetingInfoModel) { }
+    
+    /// 会议已经在其他设备接听
+    /// - Parameter meetingId: 会议Id
+    func receiveOthersDeviceAccept(meetingId: String) { }
+    
+    /// 会议已经在其他设备拒接
+    /// - Parameter meetingId: 会议Id
+    func receiveOthersDeviceReject(meetingId: String) { }
+    
+}

+ 150 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/MeetingRoomListener.swift

@@ -0,0 +1,150 @@
+//
+//  MeetingRoomListener.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/21.
+//
+
+import Foundation
+
+protocol MeetingRoomSampleListener: IMBaseListener {
+    
+    /// 房间禁言状态变化
+    /// - Parameter mute: 禁言状态
+    /// - Returns: 是否执行禁言
+    func executeRoomChange(mute: Bool) -> Bool
+    
+    /// 房主请求打开我的音频
+    func hostRequestMyAudio()
+    
+    /// 房主请求打开我的摄像头
+    func hostRequestMyVideo()
+    
+}
+
+/// 不把 livekit 的对象返回是为了,回调和 livekit 解耦,通过 identifier 到 livekitManager 获取
+protocol MeetingRoomListener: IMBaseListener {
+
+    // MARK: - room
+    /// 正在连接房间中
+    func roomConnecting()
+    
+    /// 房间连接成功
+    func roomConnected()
+    
+    /// 和房间断开连接
+    func roomDisconnected()
+    
+    /// 房主结束了会议
+    func roomDissolve()
+    
+    /// 房间禁言状态变化
+    func roomMuteChange(mute: Bool)
+    
+    // MARK: - broadcast
+    
+    /// 开始了屏幕共享
+    func screenShareStarted(userId: String)
+    
+    /// 结束了屏幕共享
+    func screenShareStoped(userId: String)
+     
+    // MARK: - member
+    /// 成员加入房间
+    func memberJoinRoom(userId: String)
+    
+    /// 成员离开房间
+    func memberLeaveRoom(userId: String)
+    
+    /// 获取到房间成员
+    func memberFetched()
+    
+    /// 有成员被邀请
+    func membersBeInvited(memebers: [UserAccount])
+    
+    /// 被邀请成员拒绝加入
+    func memberRejectJoin(userId: String)
+    
+    /// 成员正在说话
+    func memberSpeak(userId: String)
+    
+    // MARK: - mine
+    /// 我被踢了
+    func kickoutMe()
+    
+    /// 我自己的媒体源操作回调
+    func mediaSourceChange(source: LiveKitSource, mute: Bool)
+    
+    /// 收到会议聊天消息
+    func receiveChat(message: WebSocketMessage)
+}
+
+
+extension MeetingRoomListener {
+    
+    // MARK: - room
+    /// 正在连接房间中
+    func roomConnecting() { }
+    
+    /// 房间连接成功
+    func roomConnected() { }
+    
+    /// 和房间断开连接
+    func roomDisconnected() { }
+    
+    /// 房主结束了会议
+    func roomDissolve() { }
+    
+    /// 房间禁言状态变化
+    func roomMuteChange(mute: Bool) { }
+    
+    // MARK: - broadcast
+    
+    /// 开始了屏幕共享
+    func screenShareStarted(userId: String) { }
+    
+    /// 结束了屏幕共享
+    func screenShareStoped(userId: String) { }
+    
+    // MARK: - member
+    /// 成员加入房间
+    func memberJoinRoom(userId: String) { }
+    
+    /// 成员离开房间
+    func memberLeaveRoom(userId: String) { }
+    
+    /// 获取房间成员成功
+    func memberFetched() { }
+    
+    /// 有成员被邀请
+    func membersBeInvited(memebers: [UserAccount]) { }
+    
+    /// 被邀请成员拒绝加入
+    func memberRejectJoin(userId: String) { }
+    
+    /// 成员正在说话
+    func memberSpeak(userId: String) { }
+    
+    /// 我被踢了
+    func kickoutMe() { }
+
+    /// 我的媒体源被操作了
+    func mediaSourceChange(source: LiveKitSource, mute: Bool) { }
+    
+    /// 收到会议聊天消息
+    func receiveChat(message: WebSocketMessage) { }
+
+}
+
+
+extension MeetingRoomSampleListener {
+    
+    /// 房间禁言状态变化
+    func executeRoomChange(mute: Bool) -> Bool { return true }
+    
+    /// 房主请求打开我的音频
+    func hostRequestMyAudio() { }
+    
+    /// 房主请求打开我的摄像头
+    func hostRequestMyVideo() { }
+}

+ 126 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Listener/Protocol/P2PMessageListener.swift

@@ -0,0 +1,126 @@
+//
+//  WebRTCListener.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/8/31.
+//
+
+import Foundation
+
+protocol P2PMessageListener: IMBaseListener {
+    
+    /// 收到语音通话请求
+    /// - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveAudioCallRequest(callerId: Int, calleeId: Int, roomId: String)
+    
+    /// 收到视频通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveVideoCallRequest(callerId: Int, calleeId: Int, roomId: String)
+    
+    /// 收到接受通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveAcceptCall(callerId: Int, calleeId: Int)
+    
+    /// 收到拒绝通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveRejectCall(callerId: Int, calleeId: Int)
+    
+    /// 收到对方设备正忙
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveBusyDevice(callerId: Int, calleeId: Int)
+    
+    
+    /// 收到对方挂断电话
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveHangupCall(callerId: Int, calleeId: Int)
+    
+    ///  取消通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveCancelCall(callerId: Int, calleeId: Int)
+    
+    /// 该单人通话请求已经在其他设备接听
+    /// - Parameter userId: 好友UID
+    func receiveOthersDeviceAccept(userId: Int)
+    
+    /// 该单人通话请求已经在其他设备拒接
+    /// - Parameter userId: 好友UID
+    func receiveOthersDeviceReject(userId: Int)
+
+    
+    /// 该单人通话请求对方已经响铃
+    /// - Parameter userId: 好友UID
+    func receiveTargetRing(userId: Int)
+    
+}
+
+extension P2PMessageListener {
+    
+    /// 收到语音通话请求
+    /// - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveAudioCallRequest(callerId: Int, calleeId: Int, roomId: String) { }
+    
+    /// 收到视频通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveVideoCallRequest(callerId: Int, calleeId: Int, roomId: String) { }
+    
+    /// 收到接受通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveAcceptCall(callerId: Int, calleeId: Int) { }
+    
+    /// 收到拒绝通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveRejectCall(callerId: Int, calleeId: Int) { }
+    
+    /// 收到对方挂断电话
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveHangupCall(callerId: Int, calleeId: Int) { }
+    
+    /// 收到对方设备正忙
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveBusyDevice(callerId: Int, calleeId: Int) { }
+    
+    ///  取消通话请求
+    ///  - Parameters:
+    ///   - callerId: 主叫方 userId
+    ///   - calleeId: 被叫方 userId
+    func receiveCancelCall(callerId: Int, calleeId: Int) { }
+    
+    /// 该单人通话请求已经在其他设备接听
+    /// - Parameter userId: 好友UID
+    func receiveOthersDeviceAccept(userId: Int) { }
+    
+    /// 该单人通话请求已经在其他设备拒接
+    /// - Parameter userId: 好友UID
+    func receiveOthersDeviceReject(userId: Int) { }
+
+    /// 该单人通话请求对方已经响铃
+    /// - Parameter userId: 好友UID
+    func receiveTargetRing(userId: Int) { }
+    
+}

+ 14 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitConfiguration.swift

@@ -0,0 +1,14 @@
+//
+//  LiveKitConfiguration.swift
+//  Pods
+//
+//  Created by Bugu on 2024/12/7.
+//
+
+import LiveKitClient
+
+struct LiveKitConfiguration {
+    
+    /// 视频分辨率
+    static let dimensions: Dimensions = .h1080_169
+}

+ 44 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitDarwinNotificationCenter.swift

@@ -0,0 +1,44 @@
+//
+//  LiveKitDarwinNotificationCenter.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/6/1.
+//
+
+import Foundation
+
+enum LiveKitDarwinNotification: String {
+    case broadcastStarted = "iOS_BroadcastStarted"
+    case broadcastStopped = "iOS_BroadcastStopped"
+    case broadcastFinish  = "iOS_BroadcastFinished"
+}
+
+class LiveKitDarwinNotificationCenter {
+    static let shared = LiveKitDarwinNotificationCenter()
+    
+    private let notificationCenter: CFNotificationCenter
+    
+    init() {
+        notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
+    }
+    
+    private let callback: CFNotificationCallback = { (center: CFNotificationCenter?, observer: UnsafeMutableRawPointer?, name: CFNotificationName?, object: UnsafeRawPointer?, userInfo: CFDictionary?) in
+        DispatchQueue.main.async {
+            if let identifier = name?.rawValue as String? {
+                NotificationCenter.default.post(name: Notification.Name(identifier), object: identifier)
+            }
+        }
+    }
+    
+    func postNotification(_ name: LiveKitDarwinNotification) {
+        CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true)
+    }
+    
+    func addObserver(_ observer: AnyObject, selector: Selector, notificationName: LiveKitDarwinNotification, object: Any?) {
+        DispatchQueue.main.async {
+            let pointerObserver = Unmanaged.passUnretained(observer).toOpaque()
+            CFNotificationCenterAddObserver(self.notificationCenter, pointerObserver, self.callback, notificationName.rawValue as CFString, nil, .deliverImmediately)
+            NotificationCenter.default.addObserver(observer, selector: selector, name: NSNotification.Name(rawValue: notificationName.rawValue), object: object)
+        }
+    }
+}

+ 47 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitEnumerator.swift

@@ -0,0 +1,47 @@
+//
+//  LiveKitEnumerator.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/25.
+//
+
+import Foundation
+
+enum LiveKitAction: String {
+    case invite = "INVITE"
+    case reject = "REJECT"
+    case remove = "REMOVE"
+    case finish = "FINISH"
+    case audioOff = "AUDIO_OFF"
+    case audioOn = "AUDIO_ON"
+    case videoOff = "CAMERA_OFF"
+    case videoOn = "CAMERA_ON"
+    case message = "MESSAGE"
+    case leave   = "LEAVE"
+}
+
+
+enum LiveKitAudioOutput: String {
+    case speaker =  "Speaker"
+    case earpiece = "Earpiece"
+}
+
+
+enum LiveKitLeaveReason {
+    case kickout
+    case dissolve
+}
+
+enum LiveKitSource {
+    case camera
+    case microphone
+    case screenShareVideo
+    case screenShareAudio
+}
+
+enum LiveKitConnectionState: Int {
+    case disconnected
+    case connecting
+    case reconnecting
+    case connected
+}

+ 567 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/LiveKit/LiveKitManager.swift

@@ -0,0 +1,567 @@
+//
+//  LiveKitManager.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+import LiveKitClient
+import AVFoundation
+import ReplayKit
+
+let kRTCScreenSharingExtension = "RTCScreenSharingExtension"
+
+enum RoomTypeEnum {
+    case p2p
+    case meeting
+}
+
+class LiveKitManager {
+    
+    static let shared = LiveKitManager()
+    
+    /// Livekit 对象
+    private var room: Room? = nil
+    private var roomOptions: RoomOptions? = nil
+    private var muteAll: Bool = false
+    private var userId: String = ""
+    
+    private var currtRoomType: RoomTypeEnum = .meeting
+    
+    private var screenShareStarted: Bool = false
+    
+    private var remoteIdentityIds: [String] = []
+    
+    private var position: AVCaptureDevice.Position = .front
+    
+    private var remoteParticipants = [RemoteParticipant]()
+    private var camearPublication: LocalTrackPublication? = nil
+    private var sharePublication: LocalTrackPublication? = nil
+    
+    init() {
+        initializeNotification()
+    }
+}
+
+
+extension LiveKitManager {
+    
+    /// 开始连接LiveKit
+    @discardableResult
+    public func connectLivekit(token: String, userId: String, roomType: RoomTypeEnum) -> Bool {
+        
+        guard let server = AppStorage.shared.liveKitServer else { return false }
+     
+        let useBroadcastExtension = (Bundle.main.infoDictionary?[kRTCScreenSharingExtension] as? String) != nil
+        
+        let roomOptions = RoomOptions(
+            defaultCameraCaptureOptions: CameraCaptureOptions(
+                dimensions: LiveKitConfiguration.dimensions
+            ),
+            defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
+                dimensions: LiveKitConfiguration.dimensions,
+                useBroadcastExtension: useBroadcastExtension
+            ),
+            defaultVideoPublishOptions: VideoPublishOptions(
+                simulcast: true
+            ),
+            adaptiveStream: true,
+            dynacast: true,
+            reportRemoteTrackStatistics: true
+        )
+        
+        let room = Room(delegate: self, roomOptions: roomOptions)
+        self.room = room
+        self.roomOptions = roomOptions
+        self.userId = userId
+        self.currtRoomType = roomType
+        Task {
+            try await room.connect(url: server, token: token)
+        }
+        return true
+    }
+    
+    public func inRoom() -> Bool {
+        return room != nil
+    }
+    
+    public func roomType() -> RoomTypeEnum {
+        return currtRoomType
+    }
+    
+    public func connectedStatus() -> LiveKitConnectionState {
+        guard let status = room?.connectionState.rawValue else { return .disconnected }
+        return LiveKitConnectionState(rawValue: status) ?? .disconnected
+    }
+    
+    public func disconnectLivekit(completed: (() -> Void)? = nil ) {
+        Task {
+            await self.room?.disconnect()
+            cleanRoom()
+            await MainActor.run {
+                completed?()
+            }
+        }
+    }
+    
+    public func getRemoteParticipants() -> [RemoteParticipant] {
+        return remoteParticipants
+    }
+    
+    public func getLocalParticipant() -> LocalParticipant? {
+        return room?.localParticipant
+    }
+    
+    public func participant(for userId: String) -> Participant? {
+        if self.userId == userId {
+            return getLocalParticipant()
+        }
+        return remoteParticipants.first(where: { $0.identity?.stringValue == userId })
+    }
+    
+    public func setScreenShare(enabled: Bool) {
+        Task {
+            if let roomOptions = roomOptions, !roomOptions.defaultScreenShareCaptureOptions.useBroadcastExtension {
+                if enabled {
+                    sharePublication = try await room?.localParticipant.setScreenShare(enabled: true)
+                    broadcastStarted()
+                } else {
+                    closeScreenShare()
+                    broadcastStopped()
+                }
+            } else {
+                if enabled {
+                    sharePublication = try await room?.localParticipant.setScreenShare(enabled: true)
+                } else {
+                    let screenShareExtensionId = Bundle.main.infoDictionary?["RTCScreenSharingExtension"] as? String
+                    await RPSystemBroadcastPickerView.show(for: screenShareExtensionId, showsMicrophoneButton: false)
+                }
+            }
+            
+        }
+    }
+    
+    public func setCamera(enabled: Bool) {
+        Task {
+            position = .front
+            camearPublication = try await room?.localParticipant.setCamera(enabled: enabled)
+            
+            /// 移除源,否则摄像头是已上次的前后配置打开
+            if !enabled, let publication = camearPublication {
+                try await room?.localParticipant.unpublish(publication: publication)
+                camearPublication = nil
+            }
+        }
+    }
+    
+    public func setFrontCamera() {
+        Task {
+            guard let room = room,
+                  let videoTrack = room.localParticipant.localVideoTracks.first?.track as? LocalVideoTrack,
+                  let cameraCapturer = videoTrack.capturer as? CameraCapturer
+            else {
+                return
+            }
+            
+            position = .front
+            try await cameraCapturer.set(cameraPosition: .front)
+        }
+    }
+    
+    public func setBackCamera() {
+        Task {
+            guard let room = room,
+                  let videoTrack = room.localParticipant.localVideoTracks.first?.track as? LocalVideoTrack,
+                  let cameraCapturer = videoTrack.capturer as? CameraCapturer
+            else {
+                return
+            }
+            
+            position = .back
+            try await cameraCapturer.set(cameraPosition: .back)
+        }
+    }
+    
+    public func setMicrophone(enabled: Bool, host: Bool = false) {
+        Task {
+            if enabled {
+                if muteAll, !host { return }
+                try await room?.localParticipant.setMicrophone(enabled: enabled)
+            } else {
+                try await room?.localParticipant.setMicrophone(enabled: enabled)
+            }
+        }
+    }
+    
+    public func cameraPosition() -> AVCaptureDevice.Position {
+        return position
+    }
+    
+    public func microphoneEnable(for userId: String) -> Bool {
+        guard userId != self.userId else {
+            return localMicrophoneEnable()
+        }
+        guard let participant = remoteParticipants(for: userId) else { return false }
+        return participant.isMicrophoneEnabled()
+    }
+    
+    public func cameraEnable(for userId: String) -> Bool {
+        guard userId != self.userId else {
+            return localCameraEnable()
+        }
+        guard let participant = remoteParticipants(for: userId) else { return false }
+        return participant.isCameraEnabled()
+    }
+    
+    public func screenShareEnable(for userId: String) -> Bool {
+        guard userId != self.userId else {
+            return localScreenShareEnable()
+        }
+        
+        guard let enble = room?.localParticipant.isScreenShareEnabled() else { return false }
+        return enble
+    }
+    
+    public func screenShareStartedEnable() -> Bool {
+        return screenShareStarted
+    }
+  
+    public func roomMuteAll() -> Bool {
+        return muteAll
+    }
+    
+    public func audioOutput() -> LiveKitAudioOutput {
+        return AudioManager.shared.isSpeakerOutputPreferred ? .speaker : .earpiece
+    }
+    
+    /// 切换到扬声器
+    public func setAudioSpeaker() {
+        AudioManager.shared.isSpeakerOutputPreferred = true
+    }
+
+    /// 切换到听筒
+    public func setAudioEarpiece() {
+        AudioManager.shared.isSpeakerOutputPreferred = false
+    }
+}
+
+// MARK: - broadcast
+extension LiveKitManager {
+    
+    private func initializeNotification() {
+        LiveKitDarwinNotificationCenter.shared.addObserver(self, selector: #selector(broadcastStarted), notificationName: .broadcastStarted, object: nil)
+        LiveKitDarwinNotificationCenter.shared.addObserver(self, selector: #selector(broadcastStopped), notificationName: .broadcastStopped, object: nil)
+    }
+    
+    @objc
+    private func broadcastStarted() {
+        Task { @MainActor in
+            screenShareStarted = true
+            ListenerManager.shared.livekitListeners.forEach { $0.screenShareStarted(userId: userId) }
+            ListenerManager.shared.livekitListeners.forEach{ $0.mediaSourceChange(source: .screenShareVideo, mute: false) }
+        }
+    }
+    
+    @objc
+    private func broadcastStopped() {
+        closeScreenShare()
+        Task { @MainActor in
+            screenShareStarted = false
+            ListenerManager.shared.livekitListeners.forEach { $0.screenShareStoped(userId: userId) }
+            ListenerManager.shared.livekitListeners.forEach{ $0.mediaSourceChange(source: .screenShareVideo, mute: true) }
+        }
+    }
+}
+
+// MARK: - private method
+extension LiveKitManager {
+    
+    @MainActor
+    private func setParticipants(callListener: Bool = false) async {
+        guard let room = room else { return }
+        remoteParticipants = Array(room.remoteParticipants.values)
+        remoteIdentityIds = remoteParticipants.map { $0.identity?.stringValue }.compactMap{ $0 }
+        
+        if callListener {
+            /// 第一次获取需要,回调一下
+            ListenerManager.shared.livekitListeners.forEach { $0.memberFetched() }
+        }
+    }
+    
+    private func cleanRoom() {
+        closeScreenShare()
+        room?.remove(delegate: self)
+        room = nil
+        roomOptions = nil
+        cleanRemoteParticipants()
+        cleanLocalParticipants()
+        remoteIdentityIds = []
+        screenShareStarted = false
+    }
+    
+    private func cleanRemoteParticipants() {
+        remoteParticipants = []
+    }
+    
+    private func cleanLocalParticipants() {
+//        localParticipant = nil
+    }
+    
+    private func remoteParticipants(for userId: String) -> RemoteParticipant? {
+        return remoteParticipants.first(where: { $0.identity?.stringValue == userId })
+    }
+    
+    private func localCameraEnable() -> Bool {
+        guard let enble = room?.localParticipant.isCameraEnabled() else { return false }
+        return enble
+    }
+   
+    private func localMicrophoneEnable() -> Bool {
+        guard let enble = room?.localParticipant.isMicrophoneEnabled() else { return false }
+        return enble
+    }
+    
+    private func localScreenShareEnable() -> Bool {
+        guard let enble = room?.localParticipant.isScreenShareEnabled() else { return false }
+        return enble
+    }
+    
+    private func closeScreenShare() {
+        Task {
+            try await room?.localParticipant.setScreenShare(enabled: false)
+            if let publication = sharePublication, let roomOptions = roomOptions, roomOptions.defaultScreenShareCaptureOptions.useBroadcastExtension {
+                try await room?.localParticipant.unpublish(publication: publication)
+                sharePublication = nil
+            }
+        }
+    }
+    
+    private func screenShareChange(participant: Participant, publication: TrackPublication, isMuted: Bool) {
+        guard let userId = participant.identity?.stringValue else { return }
+        guard publication.source == .screenShareVideo else { return }
+        if isMuted {
+            ListenerManager.shared.livekitListeners.forEach { $0.screenShareStoped(userId: userId) }
+        } else {
+            ListenerManager.shared.livekitListeners.forEach { $0.screenShareStarted(userId: userId) }
+        }
+    }
+    
+    private func parseRoomMuteAll(metaData: String?) {
+        guard let content = metaData else { return }
+        guard let data = content.data(using: .utf8) else { return }
+        
+        do {
+            guard let memberDic = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Bool] else {
+                return
+            }
+            
+            guard let muted = memberDic["muted"] else { return }
+            muteAll = muted
+            if let executeMute = ListenerManager.shared.livekitSampleListener?.executeRoomChange(mute: muted), executeMute {
+                setMicrophone(enabled: false)
+            }
+            ListenerManager.shared.livekitListeners.forEach { $0.roomMuteChange(mute: muted) }
+        } catch {
+            return
+        }
+    }
+    
+    private func trackMuted(_ trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool) {
+        switch trackPublication.source {
+        case .unknown:
+            break
+        case .camera:
+            ListenerManager.shared.livekitListeners.forEach{ $0.mediaSourceChange(source: .camera, mute: isMuted) }
+        case .microphone:
+            ListenerManager.shared.livekitListeners.forEach{ $0.mediaSourceChange(source: .microphone, mute: isMuted) }
+        case .screenShareVideo:
+            break
+        case .screenShareAudio:
+            ListenerManager.shared.livekitListeners.forEach{ $0.mediaSourceChange(source: .screenShareAudio, mute: isMuted) }
+        }
+    }
+    
+    
+}
+
+extension LiveKitManager: RoomDelegate {
+    
+    // MARK: - Connection Events
+    /// ``Room/connectionState`` has updated.
+    func room(_ room: Room, didUpdateConnectionState connectionState: ConnectionState, from oldConnectionState: ConnectionState) {
+        Task { @MainActor in
+
+            switch connectionState {
+            case .disconnected:
+                ListenerManager.shared.livekitListeners.forEach { $0.roomDisconnected() }
+            case .connecting:
+                printLog("livekit didUpdateConnectionState = connecting")
+                ListenerManager.shared.livekitListeners.forEach { $0.roomConnecting() }
+            case .reconnecting:
+                printLog("livekit didUpdateConnectionState = reconnecting")
+                ListenerManager.shared.livekitListeners.forEach { $0.roomConnecting() }
+            case .connected:
+                printLog("livekit didUpdateConnectionState = connected")
+                ListenerManager.shared.livekitListeners.forEach { $0.roomConnected() }
+                parseRoomMuteAll(metaData: room.metadata)
+                await setParticipants(callListener: true)
+            }
+        }
+    }
+    
+    func roomDidConnect(_ room: Room) {
+        /// 首次链接成功设置为听筒
+        setAudioEarpiece()
+    }
+   
+    /// Could not connect to the room. Only triggered when the initial connect attempt fails.
+    func room(_ room: Room, didFailToConnectWithError error: LiveKitError?) {
+        printLog("livekit didFailToConnectWithError error = \(String(describing: error))")
+    }
+    
+    /// Client disconnected from the room unexpectedly after a successful connection.
+    func room(_ room: Room, didDisconnectWithError error: LiveKitError?) {
+        printLog("livekit didDisconnectWithError error = \(String(describing: error))")
+    }
+    
+    // MARK: - Participant Management
+    
+    /// A ``RemoteParticipant`` joined the room.
+    func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
+        printLog("participant did join \(String(describing: participant.identity))")
+        Task { @MainActor in
+            await setParticipants()
+            if let userId = participant.identity?.stringValue  {
+                ListenerManager.shared.livekitListeners.forEach { $0.memberJoinRoom(userId: userId) }
+            }
+        }
+    }
+    
+    /// A ``RemoteParticipant`` left the room.
+    func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant) {
+        printLog("participant did leave \(String(describing: participant.identity))")
+        Task { @MainActor in
+            var userIds = remoteIdentityIds
+            await setParticipants()
+            
+            userIds.removeAll { userId  in
+                self.remoteParticipants.contains(where: { $0.identity?.stringValue == userId })
+            }
+            
+            userIds.forEach { userId in
+                ListenerManager.shared.livekitListeners.forEach { $0.memberLeaveRoom(userId: userId) }
+            }
+        }
+    }
+    
+    /// Speakers in the room has updated.
+    func room(_ room: Room, didUpdateSpeakingParticipants participants: [Participant]) {
+        Task { @MainActor in
+            if let userId = participants.first(where: { $0.identity?.stringValue != nil })?.identity?.stringValue  {
+                ListenerManager.shared.livekitListeners.forEach { $0.memberSpeak(userId: userId) }
+            }
+        }
+    }
+    
+    // MARK: - Track Publications
+    
+    /// The ``LocalParticipant`` has published a ``LocalTrack``.
+    func room(_ room: Room, participant: LocalParticipant, didPublishTrack publication: LocalTrackPublication) {
+        printLog("participant LocalParticipant")
+        Task { @MainActor in
+            trackMuted(publication, didUpdateIsMuted: publication.isMuted)
+        }
+    }
+    
+    func room(_ room: Room, participant: RemoteParticipant, didPublishTrack publication: RemoteTrackPublication) {
+        Task { @MainActor in
+            screenShareChange(participant: participant, publication: publication, isMuted: false)
+        }
+    }
+    
+    func room(_ room: Room, participant: RemoteParticipant, didUnpublishTrack publication: RemoteTrackPublication) {
+        Task { @MainActor in
+            screenShareChange(participant: participant, publication: publication, isMuted: true)
+        }
+    }
+    
+    
+    // MARK: - Data and Encryption
+    
+    func room(_ room: Room, didUpdateMetadata metadata: String?) {
+        Task { @MainActor in
+            parseRoomMuteAll(metaData: metadata)
+        }
+    }
+    
+    func room(_ room: Room, participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool) {
+        Task { @MainActor in
+            guard let _ = participant as? LocalParticipant else {
+                screenShareChange(participant: participant, publication: trackPublication, isMuted: isMuted)
+                return
+            }
+            
+            trackMuted(trackPublication, didUpdateIsMuted: isMuted)
+        }
+    }
+    
+    /// Received data from from a user or server. `participant` will be nil if broadcasted from server.
+    func room(_ room: Room, participant: RemoteParticipant?, didReceiveData data: Data, forTopic topic: String) {
+        Task { @MainActor in
+            guard let dict = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String : Any] else {
+                return
+            }
+            
+            guard let action = LiveKitAction(rawValue: (dict["action"] as? String) ?? "") else {
+                return
+            }
+            
+            switch action {
+            case .invite:
+                guard let content = dict["content"] as? String else { return }
+                guard let data = content.data(using: .utf8) else { return }
+                
+                do {
+                    guard let memberDic = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] else {
+                        return
+                    }
+                    var members: [UserAccount] = []
+                    memberDic.forEach { userId, userName in
+                        let account = UserAccount(id: userId, name: userName)
+                        members.append(account)
+                    }
+                    ListenerManager.shared.livekitListeners.forEach { $0.membersBeInvited(memebers: members) }
+                } catch {
+                    return
+                }
+            case .reject:
+                guard let userId = dict["fromUid"] as? Int else { return }
+                ListenerManager.shared.livekitListeners.forEach { $0.memberRejectJoin(userId: "\(userId)") }
+            case .remove:
+                ListenerManager.shared.livekitListeners.forEach { $0.kickoutMe() }
+            case .finish:
+                ListenerManager.shared.livekitListeners.forEach { $0.roomDissolve() }
+            case .audioOff:
+                setMicrophone(enabled: false)
+            case .audioOn:
+                ListenerManager.shared.livekitSampleListener?.hostRequestMyAudio()
+            case .videoOff:
+                setCamera(enabled: false)
+            case .videoOn:
+                ListenerManager.shared.livekitSampleListener?.hostRequestMyVideo()
+            case .message:
+                let content = dict["content"] as? String
+                let sender = dict["fromUid"] as? Int
+                let format = dict["format"] as? Int
+                
+                let message = WebSocketMessage(content: content, sender: "\(sender ?? -1)", format: "\(format ?? -1)")
+                ListenerManager.shared.livekitListeners.forEach{ $0.receiveChat(message: message) }
+                
+            case .leave:
+                guard let userId = dict["content"] as? String else { return }
+                ListenerManager.shared.livekitListeners.forEach { $0.memberLeaveRoom(userId: userId) }
+            }
+        }
+    }
+}

+ 59 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/HXMessageAction.swift

@@ -0,0 +1,59 @@
+//
+//  HXMessageAction.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+// MARK: 消息分类
+enum HXMessageAction {
+    
+    /// webRTC 单人相关信令
+    enum WebRTC: String {
+        
+        /// 收到通话请求
+        case receiveCallRequest = "900"
+        
+        /// 接受通话请求
+        case acceptCallRequest = "902"
+        
+        /// 拒绝通话请求
+        case rejectCallRequest = "903"
+        
+        /// 对方设备正忙
+        case targetDeviceBusy = "904"
+        
+        /// 对方挂断电话
+        case targetHangupCall = "905"
+
+        /// 取消通话请求
+        case targetCancelCall = "906"
+        
+        /// 其他设备接听
+        case othersDeviceAccpet = "928"
+        
+        /// 其他设备接听
+        case othersDeviceReject = "929"
+        
+        /// 对方已经响铃
+        case targetRing = "932"
+    }
+
+    /// 会议相关信令
+    enum Meeting: String {
+        
+        /// 收到会议邀请
+        case receiveMeetingInvite = "600"
+        
+        /// 收到会议预约
+        case receiveMeetingReservation = "601"
+        
+        /// 其他设备接听
+        case othersDeviceAccpet = "603"
+        
+        /// 其他设备拒绝
+        case othersDeviceReject = "604"
+    }
+    
+}

+ 105 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/MessageHandler+Meeting.swift

@@ -0,0 +1,105 @@
+//
+//  MessageHandler+Meeting.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/6/1.
+//
+
+import Foundation
+
+extension MessageHandler {
+    
+    /// 会议相关信令处理
+    /// - Parameters:
+    ///   - message: 消息 info
+    ///   - action: 消息类型
+    ///   - Returns: true 拦截, false 不拦截
+    static func meetingMessageHandler(message: WebSocketMessage, action: HXMessageAction.Meeting) -> Bool {
+        switch action {
+        case .receiveMeetingInvite:
+            receiveMeetingInvite(message: message)
+            return false
+        case .receiveMeetingReservation:
+            receiveMeetingReservation(message: message)
+            return false
+        case .othersDeviceAccpet:
+            othersDeviceAccpet(message: message)
+            return false
+        case .othersDeviceReject:
+            othersDeviceReject(message: message)
+            return false
+        }
+    }
+}
+
+extension MessageHandler {
+    
+    /// 收到会议邀请请求
+    private static func receiveMeetingInvite(message: WebSocketMessage) {
+        guard let creatorId = message.sender, let meetingId = message.content else { return }
+         
+        let meetingInfo = MeetingInfoModel(creatorId: creatorId, creatorName: "", meetingId: meetingId, meetingTitle: message.title)
+        ListenerManager.shared.roomMessageListeners.forEach{ $0.receiveMeetingInvite(meetingInfo: meetingInfo) }
+    }
+    
+    /// 收到预约会议请求
+    private static func receiveMeetingReservation(message: WebSocketMessage) {
+
+        guard let creatorId = message.sender, let meetingId = message.content, let extra = message.extra, let data = extra.data(using: .utf8)  else { return }
+        do {
+            guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
+                return
+            }
+            
+            let members = parseMembers(content: dictionary["member"] as? String)
+            let description = dictionary["description"] as? String
+            let dueTime = dictionary["dueTime"] as? String
+            let creatorName = dictionary["name"] as? String
+            
+            let meetingInfo = MeetingInfoModel(creatorId: creatorId, creatorName: creatorName ?? "", meetingId: meetingId, meetingTitle: message.title, meetingDescription: description, members: members, dueTime: dueTime)
+            ListenerManager.shared.roomMessageListeners.forEach{ $0.receiveMeetingReservation(meetingInfo: meetingInfo) }
+        } catch {
+            return
+        }
+    }
+    
+    /// 其他设备接听
+    private static func othersDeviceAccpet(message: WebSocketMessage) {
+        guard let meetingId = message.content else { return }
+        ListenerManager.shared.roomMessageListeners.forEach{ $0.receiveOthersDeviceAccept(meetingId: meetingId) }
+    }
+    
+    /// 其他设备拒绝
+    private static func othersDeviceReject(message: WebSocketMessage) {
+        guard let meetingId = message.content else { return }
+        ListenerManager.shared.roomMessageListeners.forEach{ $0.receiveOthersDeviceReject(meetingId: meetingId) }
+    }
+}
+
+extension MessageHandler {
+    
+    /// 解析房间成员信息
+    /// - Parameter content: 字符串
+    /// - Returns: 用户信息
+    private static func parseMembers(content: String?) -> [UserAccount] {
+        guard let content = content else { return [] }
+        guard let data = content.data(using: .utf8) else { return [] }
+        
+        do {
+            guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
+                return []
+            }
+            
+            var accounts: [UserAccount] = []
+            dictionary.forEach { key, value in
+                let account = UserAccount(id: key, name: value as? String)
+                accounts.append(account)
+            }
+            return accounts
+        } catch {
+            return []
+        }
+        
+    }
+    
+}

+ 122 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/MessageHandler+P2P.swift

@@ -0,0 +1,122 @@
+//
+//  MessageHandler+WebRTC.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/9/1.
+//
+
+import Foundation
+
+extension MessageHandler {
+    
+    /// 单人音视频相关信令处理
+    /// - Parameters:
+    ///   - message: 消息 info
+    ///   - action: 消息类型
+    /// - Returns: true 表示该消息拦截 false 不拦截
+    static func p2pMessageHandler(message: WebSocketMessage, action: HXMessageAction.WebRTC) -> Bool {
+        switch action {
+        case .receiveCallRequest: /// 收到通话请求
+            receiveCallRequest(message: message)
+        case .acceptCallRequest: /// 接受通话请求
+            acceptCallRequest(message: message)
+        case .rejectCallRequest: /// 拒绝通话请求
+            rejectCallRequest(message: message)
+        case .targetHangupCall: /// 对方挂断电话
+            targetHangupCall(message: message)
+        case .targetDeviceBusy: /// 对方设备正忙
+            targetDeviceBusy(message: message)
+        case .targetCancelCall: /// 取消通话请求
+            targetCancelCall(message: message)
+        case .othersDeviceAccpet:  /// 其他设备接听
+            othersDeviceAccpet(message: message)
+        case .othersDeviceReject: /// 其他设备拒绝
+            othersDeviceReject(message: message)
+        case .targetRing: /// 对方已经响铃
+            targetRing(message: message)
+        }
+        
+        return false
+    }
+}
+
+extension MessageHandler {
+    
+    /// 收到通话请求
+    private static func receiveCallRequest(message: WebSocketMessage) {
+        let callerId = message.sender?.int ?? 0
+        let calleeId = message.receiver?.int ?? 0
+        let type = parse(content: message.content)["type"] as? String
+        let roomId = parse(content: message.content)["roomTag"] as? String
+        
+        guard let type = type, let roomId = roomId else { return }
+        if type == "audio" {
+            ListenerManager.shared.p2pMessageListeners.forEach { $0.receiveAudioCallRequest(callerId: callerId, calleeId: calleeId, roomId: roomId)}
+        } else if type == "video" {
+            ListenerManager.shared.p2pMessageListeners.forEach { $0.receiveVideoCallRequest(callerId: callerId, calleeId: calleeId, roomId: roomId)}
+        }
+    }
+    
+    /// 接受通话请求
+    private static func acceptCallRequest(message: WebSocketMessage) {
+        let callerId = message.receiver?.int ?? 0
+        let calleeId = message.sender?.int ?? 0
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveAcceptCall(callerId: callerId, calleeId: calleeId) }
+    }
+    
+    /// 拒绝通话请求
+    private static func rejectCallRequest(message: WebSocketMessage) {
+        let callerId = message.receiver?.int ?? 0
+        let calleeId = message.sender?.int ?? 0
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveRejectCall(callerId: callerId, calleeId: calleeId) }
+    }
+    
+    /// 对方挂断电话
+    private static func targetHangupCall(message: WebSocketMessage) {
+        let callerId = message.sender?.int ?? 0
+        let calleeId = message.receiver?.int ?? 0
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveHangupCall(callerId: callerId, calleeId: calleeId) }
+    }
+    
+    /// 对方设备正忙
+    private static func targetDeviceBusy(message: WebSocketMessage) {
+        let callerId = message.receiver?.int ?? 0
+        let calleeId = message.sender?.int ?? 0
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveBusyDevice(callerId: callerId, calleeId: calleeId) }
+    }
+    
+    /// 取消呼叫
+    private static func targetCancelCall(message: WebSocketMessage) {
+        let callerId = message.sender?.int ?? 0
+        let calleeId = message.receiver?.int ?? 0
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveCancelCall(callerId: callerId, calleeId: calleeId) }
+    }
+    
+    /// 其他设备接听
+    private static func othersDeviceAccpet(message: WebSocketMessage) {
+        guard let userId = message.content?.int else { return }
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveOthersDeviceAccept(userId: userId) }
+    }
+    
+    /// 其他设备拒绝
+    private static func othersDeviceReject(message: WebSocketMessage) {
+        guard let userId = message.content?.int else { return }
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveOthersDeviceReject(userId: userId) }
+    }
+    
+    private static func targetRing(message: WebSocketMessage) {
+        guard let userId = message.content?.int else { return }
+        
+        ListenerManager.shared.p2pMessageListeners.forEach{ $0.receiveTargetRing(userId: userId) }
+    }
+}
+
+
+

+ 52 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Message/MessageHandler.swift

@@ -0,0 +1,52 @@
+//
+//  MessageHandler.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+
+struct MessageHandler {
+    
+    /// 消息分流
+    /// - Parameter message: 消息 info
+    /// - Returns: intercepted 是否拦截消息
+    static func messageHandle(webMessage: WebSocketMessage) -> Bool {
+        return messageHandle(webMessage)
+    }
+
+}
+
+extension MessageHandler {
+    
+    /// 消息分流 私有方法
+    private static func messageHandle(_ webMessage: WebSocketMessage) -> Bool {
+        
+        /// 单人RTC
+        if let messageAction = HXMessageAction.WebRTC(rawValue: webMessage.action ?? "") {
+           return p2pMessageHandler(message: webMessage, action: messageAction)
+        }
+        
+        if let messageAction = HXMessageAction.Meeting(rawValue: webMessage.action ?? "") {
+           return meetingMessageHandler(message: webMessage, action: messageAction)
+        }
+        return true
+    }
+}
+
+extension MessageHandler {
+    static func parse(content: String?) -> [String : Any] {
+        guard let content = content, content.count > 0 else {
+            return [:]
+        }
+        
+        guard let data = content.data(using: .utf8) else {
+            return [:]
+        }
+        guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
+            return [:]
+        }
+        return dictionary
+    }
+}

+ 226 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Music/RTCCallMusicManager.swift

@@ -0,0 +1,226 @@
+//
+//  RTCCallMusicManager.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/9/22.
+//
+
+import Foundation
+import AVFAudio
+import UIKit
+
+enum RTCCallMusic: String {
+    case calling = "calling"
+    case callend = "callend"
+    case meetingInvited = "meeting_invite_notification"
+    case meetingOtherJoined = "meeting_other_joined"
+    case meetingOtherLeft = "meeting_other_left"
+    case meetingMeJoin = "meeting_you_joined"
+    case meetingMeLeft = "meeting_you_left"
+}
+
+class RTCCallMusicManager: NSObject {
+    
+    static let shared = RTCCallMusicManager()
+    
+    var rtcMusic: Bool {
+        return playMusic
+    }
+    
+    private var playMusic: Bool = false
+    
+    private var audioPlayer: AVAudioPlayer?
+    private var vibrateTimer: Timer?
+    private var detectTimer: Timer?
+    private var mute: Bool?
+    private var isPlaying: Bool = false
+    private var isPlayVibrate: Bool = false
+    private let queue = DispatchQueue(label: "com.livekitplugin.callmusic.queue")
+    
+    override init() {
+        super.init()
+    }
+    
+    func playCallMusic(action: RTCCallMusic, isRepeat: Bool = false, isComing: Bool, checkMute: Bool = false) {
+        play(resource: action.rawValue, isRepeat: isRepeat, isComing: isComing, checkMute: checkMute)
+    }
+    
+    /// 播放来电获取去电声音
+    func playCallComingMusic(checkMute: Bool = false) {
+        play(resource: "calling", isRepeat: true, isComing: true, checkMute: checkMute)
+    }
+    
+    /// 播放挂断声音
+    func playCallEndMusic(checkMute: Bool = true) {
+        play(resource: "callend", isRepeat: false, isComing: false, checkMute: checkMute)
+    }
+    
+    func playVibrate() {
+        if isPlayVibrate { return }
+        
+        isPlayVibrate = true
+        
+        if vibrateTimer != nil {
+            vibrateTimer?.invalidate()
+            vibrateTimer = nil
+        }
+        vibrateTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(vibrate), userInfo: nil, repeats: true)
+    }
+    
+    func stopVibrate() {
+        isPlayVibrate = false
+        vibrateTimer?.invalidate()
+        vibrateTimer = nil
+    }
+    
+    /// 播放链接成功声音
+    func playCallConnected() {
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            stopPlayCallMusic()
+            
+            let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
+            feedbackGenerator.impactOccurred()
+        }
+    }
+    
+    /// 停止播放声音
+    func stopPlayCallMusic() {
+        isPlaying = false
+        playMusic = false
+        mute = nil
+        
+        stopVibrate()
+        
+        detectTimer?.invalidate()
+        detectTimer = nil
+        RTCClientMuteChecker.shared.abort()
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            audioPlayer?.stop()
+            audioPlayer = nil
+            
+            do {
+                let audioSession = AVAudioSession.sharedInstance()
+                try audioSession.overrideOutputAudioPort(.none)
+                try audioSession.setActive(true)
+            } catch {
+                
+            }
+        }
+    }
+}
+
+extension RTCCallMusicManager {
+    
+    
+    /// 播放音频文件
+    /// - Parameter repeatPlay: 是否重复
+    private func play(resource: String, isRepeat: Bool, isComing: Bool, checkMute: Bool) {
+        stopPlayCallMusic()
+        
+        guard checkMute else {
+            self._play(resource: resource, isRepeat: isRepeat)
+            return
+        }
+        
+        detectTimer = Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { timer in
+            self.detectMuteStatus(resource: resource, isRepeat: isRepeat, isComing: isComing)
+        }
+        
+    }
+    
+    func detectMuteStatus(resource: String, isRepeat: Bool, isComing: Bool) {
+        RTCClientMuteChecker.shared.detectMuteStatus { mute in
+            if mute == self.mute {
+                return
+            }
+            self.mute = mute
+            if mute {
+                self.isPlaying = false
+                self.audioPlayer?.pause()
+                
+                if isComing {
+                    self.playVibrate()
+                } else {
+                    self.vibrate()
+                    self.stopPlayCallMusic()
+                }
+            } else {
+                self._play(resource: resource, isRepeat: isRepeat)
+            }
+        }
+    }
+    
+    private func _play(resource: String, isRepeat: Bool) {
+        playMusic = true
+        stopVibrate()
+        
+        if resource == "calling" {
+            printLog("123")
+        }
+        
+        if isPlaying {
+            return
+        }
+        
+        // 你的音频文件的URL
+        guard let bundle = Bundle.livekitPluginBundle else {
+            printLog("找不到音频文件")
+            isPlaying = false
+            return
+        }
+        
+        guard let audioFileURL = bundle.url(forResource: resource, withExtension: "mp3") else {
+            printLog("找不到音频文件")
+            isPlaying = false
+            return
+        }
+        
+        
+        isPlaying = true
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            do {
+                
+                let audioSession = AVAudioSession.sharedInstance()
+                let lastCategory = audioSession.category
+                if lastCategory != .playAndRecord {
+                    try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth, .mixWithOthers])
+                }
+                
+                try audioSession.setActive(true)
+                
+                // 创建AVAudioPlayer实例
+                audioPlayer = try AVAudioPlayer(contentsOf: audioFileURL)
+                audioPlayer?.delegate = self
+                
+                if isRepeat {
+                    // 设置循环播放
+                    audioPlayer?.numberOfLoops = -1 // -1表示无限循环
+                }
+                
+                // 准备并播放音频
+                audioPlayer?.prepareToPlay()
+                audioPlayer?.play()
+            } catch {
+                // 处理错误
+                print("无法创建AVAudioPlayer:\(error.localizedDescription)")
+                self.isPlaying = false
+                self.playMusic = false
+            }
+        }
+    }
+    
+    @objc private func vibrate() {
+        AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate, nil)
+    }
+}
+
+extension RTCCallMusicManager: AVAudioPlayerDelegate {
+    
+    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        stopPlayCallMusic()
+    }
+}
+

+ 73 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Music/RTCClientMuteChecker.swift

@@ -0,0 +1,73 @@
+//
+//  RTCClientMuteChecker.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/10/30.
+//
+
+import Foundation
+import AudioToolbox
+
+class RTCClientMuteChecker {
+    
+    static let shared = RTCClientMuteChecker()
+    
+    private var soundDuration: Float = 0.0
+    private var playbackTimer: Timer?
+    private var muteCheckerCallBack: ((Bool) -> Void)?
+    
+    func abort() {
+        self.muteCheckerCallBack = nil
+        playbackTimer?.invalidate()
+        playbackTimer = nil
+    }
+
+    func detectMuteStatus(_ muteCheckerCallBack: ((Bool) -> Void)?) {
+        self.muteCheckerCallBack = muteCheckerCallBack
+        
+        // iOS 5+ doesn't allow mute switch detection using state length detection
+        // So we need to play a blank 100ms file and detect the playback length
+        soundDuration = 0.0
+        var soundFileObject: SystemSoundID = 0
+        
+        // Get the main bundle for the app
+        let bundle = Bundle.livekitPluginBundle!
+        let cfBundle = CFBundleCreate(kCFAllocatorDefault, bundle.bundleURL as CFURL)
+        
+        // Get the URL to the sound file to play
+        guard let soundFileURLRef = CFBundleCopyResourceURL(cfBundle, "detection" as CFString, "aiff" as CFString, nil) else {
+            return
+        }
+        
+        // Create a system sound object representing the sound file
+        let error = AudioServicesCreateSystemSoundID(soundFileURLRef, &soundFileObject)
+        if error != 0 && self.muteCheckerCallBack != nil {
+            self.muteCheckerCallBack!(false)
+            return
+        }
+        
+        AudioServicesAddSystemSoundCompletion(soundFileObject, nil, nil, { mySSID, mySelf in
+            guard let mySelf = mySelf else { return }
+            Unmanaged<RTCClientMuteChecker>.fromOpaque(mySelf).takeUnretainedValue().playbackComplete()
+        }, Unmanaged.passUnretained(self).toOpaque())
+        
+        // Start the playback timer
+        playbackTimer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(incrementTimer), userInfo: nil, repeats: true)
+        
+        // Play the sound
+        AudioServicesPlaySystemSound(soundFileObject)
+    }
+    
+    @objc func incrementTimer() {
+        soundDuration += 0.001
+    }
+    
+    func playbackComplete() {
+        if let callBack = self.muteCheckerCallBack {
+            callBack(soundDuration < 0.030)
+        }
+        playbackTimer?.invalidate()
+        playbackTimer = nil
+    }
+}
+

+ 115 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Notification/RTCNotificationManager.swift

@@ -0,0 +1,115 @@
+//
+//  RTCNotificationManager.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/9/18.
+//
+
+import Foundation
+
+enum RTCNotificationState: Int {
+    
+    /// 通话结束
+    case hangupCall = 0
+    
+    /// 取消通话
+    case cancelCall = 1
+    
+    /// 拒绝通话
+    case rejectCall = 2
+    
+    /// 忙线
+    case busyCall = 3
+    
+    /// 超时
+    case timeoutCall = 4
+    
+    /// 忽略来电
+    case ignoreCall = 5
+}
+
+
+class RTCNotificationManager {
+    
+    static var pluginActionNotification: String {
+        return "LiveKitPluginStateChange"
+    }
+    
+    static func singleCallBegin() {
+        let parameters: [String : Any] = [
+            "key"  : "ACTION_START_CALLING",
+        ]
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: RTCNotificationManager.pluginActionNotification), object: nil, userInfo: parameters)
+    }
+    
+    /// 单人通话完结通知
+    /// - Parameters:
+    ///   - state: 结束状态
+    ///   - duration: 通话时长(毫秒)
+    ///   - role: 通话角色
+    ///   - type: 通话类型
+    ///   - targetId: 对方用户UID
+    static func singleCallFinished(state: RTCNotificationState, duration: Int, role: P2PCallIdentity, type: P2PCallType, targetId: Int) {
+        let parameters: [String : Any] = [
+            "key"  : "ACTION_CALL_FINISHED",
+            "data" : [
+                "state"    : state.rawValue,
+                "duration" : duration,
+                "role"     : role == .initiator ? 0 : 1,
+                "type"     : type == .audio  ? "audio" : "video",
+                "uid"      : targetId
+            ]
+        ]
+        
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: RTCNotificationManager.pluginActionNotification), object: nil, userInfo: parameters)
+    }
+    
+    /// 被他人拉入会议,显示了接听界面,插件通知保存会议记录
+    static func meetingInvited(meetingId: String, meetingTitle: String, meetingDescription: String, invitedTime: Int, dueTime: String, creatorId: String, creatorName: String) {
+        let parameters: [String : Any] = [
+            "key"  : "ACTION_MEETING_RING",
+            "data" : [
+                "tag"         : meetingId,
+                "title"       : meetingTitle,
+                "description" : meetingDescription,
+                "createAt"    : invitedTime,
+                "dueTime"     : dueTime,
+                "uid"         : Int(creatorId) ?? 0,
+                "name"        : creatorName,
+            ] as [String : Any]
+        ]
+        
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: RTCNotificationManager.pluginActionNotification), object: nil, userInfo: parameters)
+    }
+    
+    /// 房间完结通知
+    /// - Parameters:
+    ///   - meetingId: 房间号
+    ///   - duration: 参会时长 毫秒
+    ///   - creatorId: 会议创建者UID
+    ///   - creatorName: 会议创建者名称
+    ///   - members: 通话人信息
+    static func meetingFinished(meetingId: String, duration: Int, creatorId: String, creatorName: String, members:[UserAccount] ) {
+        var membersMap: [String : String] = [:]
+        members.forEach { account in
+            guard let id = account.id else {
+                return
+            }
+            membersMap["\(id)"] = account.name ?? ""
+        }
+   
+        let parameters: [String : Any] = [
+            "key"  : "ACTION_MEETING_FINISHED",
+            "data" : [
+                "tag"       : meetingId,
+                "duration"  : duration,
+                "uid"       : Int(creatorId) ?? 0,
+                "name"      : creatorName,
+                "members"   : membersMap
+            ] as [String : Any]
+        ]
+        
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: RTCNotificationManager.pluginActionNotification), object: nil, userInfo: parameters)
+    }
+    
+}

+ 104 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Others/PermissionManager.swift

@@ -0,0 +1,104 @@
+//
+//  PermissionManager.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/9/22.
+//
+
+import AVFoundation
+import UIKit
+
+class PermissionManager {
+    static let shared = PermissionManager()
+
+    enum PermissionType {
+        case microphone
+        case camera
+        case microphoneAndCamera
+    }
+
+    private init() {}
+
+    func requestPermission(type: PermissionType, completion: @escaping (Bool) -> Void) {
+        switch type {
+        case .microphone:
+            let status = AVCaptureDevice.authorizationStatus(for: .audio)
+            switch status {
+            case .notDetermined, .restricted, .denied:
+                AVCaptureDevice.requestAccess(for: .audio) { granted in
+                    if granted {
+                        DispatchQueue.main.async {
+                            completion(true)
+                        }
+                    } else {
+                        DispatchQueue.main.async {
+                            self.showPermissionAlert(permissionType: .microphone)
+                            completion(false)
+                        }
+                    }
+                }
+            case .authorized:
+                completion(true)
+            @unknown default:
+                break
+            }
+            
+            
+        case .camera:
+            AVCaptureDevice.requestAccess(for: .video) { granted in
+                if granted {
+                    DispatchQueue.main.async {
+                        completion(true)
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        self.showPermissionAlert(permissionType: .camera)
+                        completion(false)
+                    }
+                }
+            }
+        case .microphoneAndCamera:
+            AVCaptureDevice.requestAccess(for: .audio) { microphoneGranted in
+                if microphoneGranted {
+                    AVCaptureDevice.requestAccess(for: .video) { cameraGranted in
+                        if cameraGranted {
+                            DispatchQueue.main.async {
+                                completion(true)
+                            }
+                        } else {
+                            DispatchQueue.main.async {
+                                self.showPermissionAlert(permissionType: .camera)
+                                completion(false)
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        self.showPermissionAlert(permissionType: .microphone)
+                        completion(false)
+                    }
+                }
+            }
+        }
+    }
+
+    private func showPermissionAlert(permissionType: PermissionType) {
+        let alertController = UIAlertController(
+            title: L10n.Common.permissionDenied,
+            message: permissionType == .microphone ? L10n.Common.requestMicrophone : L10n.Common.requestCamera,
+            preferredStyle: .alert
+        )
+
+        let settingsAction = UIAlertAction(title: L10n.Common.goSet, style: .default) { _ in
+            if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
+                UIApplication.shared.open(settingsURL)
+            }
+        }
+        
+        let cancelAction = UIAlertAction(title: L10n.Common.cancel, style: .cancel, handler: nil)
+        alertController.addAction(settingsAction)
+        alertController.addAction(cancelAction)
+        
+        SkipManager.present(alertController, animated: true)
+    }
+}

+ 37 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Others/SkipManager.swift

@@ -0,0 +1,37 @@
+//
+//  SkipManager.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/8/29.
+//
+
+import Foundation
+import UIKit
+
+struct SkipManager {
+    
+    static func present(_ viewControllerToPresent: UIViewController, modalPresentationStyle: UIModalPresentationStyle = .fullScreen, animated flag: Bool, completion: (() -> Void)? = nil) {
+        if modalPresentationStyle == .custom {
+            viewControllerToPresent.modalPresentationStyle = .custom
+            viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = true
+            viewControllerToPresent.transitioningDelegate = viewControllerToPresent as? any UIViewControllerTransitioningDelegate
+        } else {
+            viewControllerToPresent.modalPresentationStyle = modalPresentationStyle
+        }
+        AppStorage.shared.window?.rootViewController?.present(viewControllerToPresent, animated: flag, completion: completion)
+    }
+    
+    static func present(from: UIViewController, to: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
+        to.modalPresentationStyle = .fullScreen
+        from.present(to, animated: flag, completion: completion)
+    }
+    
+    static func push(_ viewController: UIViewController, animated flag: Bool) {
+        AppStorage.shared.window?.rootViewController?.navigationController?.pushViewController(viewController, animated: flag)
+    }
+    
+    static func addView(_ view: UIView) {
+        AppStorage.shared.window?.addSubview(view)
+    }
+    
+}

+ 189 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Share/BroadcastServerSocketConnection.swift

@@ -0,0 +1,189 @@
+//
+//  BroadcastServerSocketConnection.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/6/2.
+//
+
+import Foundation
+import Logging
+
+let logger = Logger(label: "LiveKitSDK")
+
+class BroadcastServerSocketConnection: NSObject {
+    private let streamDelegate: StreamDelegate
+
+    private let filePath: String
+    private var socketHandle: Int32 = -1
+    private var address: sockaddr_un?
+
+    private var inputStream: InputStream?
+    private var outputStream: OutputStream?
+
+    private var listeningSource: DispatchSourceRead?
+    private var networkQueue: DispatchQueue?
+    private var shouldKeepRunning = false
+
+    init?(filePath path: String, streamDelegate: StreamDelegate) {
+        self.streamDelegate = streamDelegate
+        filePath = path
+        socketHandle = socket(AF_UNIX, SOCK_STREAM, 0)
+
+        guard socketHandle >= 0 else {
+            logger.log(level: .debug, "failure: create socket")
+            return nil
+        }
+    }
+
+    func open() -> Bool {
+        logger.log(level: .debug, "open socket connection")
+
+        guard setupAddress() == true else {
+            logger.log(level: .debug, "failed setting up address")
+
+            return false
+        }
+
+        guard bindSocket() == true else {
+            return false
+        }
+
+        guard FileManager.default.fileExists(atPath: filePath) else {
+            logger.log(level: .debug, "failure: socket file missing")
+            return false
+        }
+        guard Darwin.listen(socketHandle, 10) >= 0 else {
+            logger.log(level: .debug, "failure: socket failed listening connection")
+            return false
+        }
+
+        let listeningSource = DispatchSource.makeReadSource(fileDescriptor: socketHandle)
+        listeningSource.setEventHandler {
+            let clientSocket = Darwin.accept(self.socketHandle, nil, nil)
+
+            guard clientSocket >= 0 else {
+                logger.log(level: .debug, "failure: socket failed accepting connection")
+                return
+            }
+
+            self.setupStreams(clientSocket: clientSocket)
+
+            self.inputStream?.open()
+            self.outputStream?.open()
+
+            logger.log(level: .debug, "streams open")
+        }
+
+        self.listeningSource = listeningSource
+        listeningSource.resume()
+        return true
+    }
+
+    func close() {
+        unscheduleStreams()
+
+        inputStream?.delegate = nil
+        outputStream?.delegate = nil
+
+        inputStream?.close()
+        outputStream?.close()
+
+        inputStream = nil
+        outputStream = nil
+
+        logger.log(level: .debug, "closing server socket")
+        listeningSource?.cancel()
+        Darwin.close(socketHandle)
+    }
+
+    func writeToStream(buffer: UnsafePointer<UInt8>, maxLength length: Int) -> Int {
+        outputStream?.write(buffer, maxLength: length) ?? 0
+    }
+
+    private func setupAddress() -> Bool {
+        var addr = sockaddr_un()
+        addr.sun_family = sa_family_t(AF_UNIX)
+        guard filePath.count < MemoryLayout.size(ofValue: addr.sun_path) else {
+            logger.log(level: .debug, "failure: fd path is too long")
+            return false
+        }
+
+        _ = filePath.withCString {
+            unlink($0)
+        }
+
+        _ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in
+            filePath.withCString {
+                strncpy(ptr, $0, filePath.count)
+            }
+        }
+
+        address = addr
+        return true
+    }
+
+    private func bindSocket() -> Bool {
+        guard var addr = address else {
+            logger.log(level: .debug, "failure: no address?")
+            return false
+        }
+
+        let status = withUnsafePointer(to: &addr) { ptr in
+            ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) {
+                Darwin.bind(socketHandle, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
+            }
+        }
+
+        guard status == noErr else {
+            logger.log(level: .debug, "failure: \(status)")
+            return false
+        }
+
+        return true
+    }
+
+    private func setupStreams(clientSocket: Int32) {
+        var readStream: Unmanaged<CFReadStream>?
+        var writeStream: Unmanaged<CFWriteStream>?
+
+        CFStreamCreatePairWithSocket(kCFAllocatorDefault, clientSocket, &readStream, &writeStream)
+
+        inputStream = readStream?.takeRetainedValue()
+        inputStream?.delegate = streamDelegate
+        inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
+
+        outputStream = writeStream?.takeRetainedValue()
+        outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
+
+        scheduleStreams()
+    }
+
+    private func scheduleStreams() {
+        shouldKeepRunning = true
+
+        networkQueue = DispatchQueue.global(qos: .userInitiated)
+        networkQueue?.async { [weak self] in
+            self?.inputStream?.schedule(in: .current, forMode: .default)
+            self?.outputStream?.schedule(in: .current, forMode: .default)
+
+            logger.log(level: .debug, "streams scheduled")
+            var isRunning = false
+
+            repeat {
+                isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture)
+            } while isRunning
+
+            logger.log(level: .debug, "streams stopped")
+        }
+    }
+
+    private func unscheduleStreams() {
+        logger.log(level: .debug, "unscheduleStreams")
+        networkQueue?.sync { [weak self] in
+            self?.inputStream?.remove(from: .current, forMode: .common)
+            self?.outputStream?.remove(from: .current, forMode: .common)
+        }
+
+        shouldKeepRunning = false
+    }
+}

+ 142 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Share/ShareScreenManager.swift

@@ -0,0 +1,142 @@
+//
+//  ShareScreenManager.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/6/1.
+//
+
+import Foundation
+import ReplayKit
+import LiveKitClient
+import LiveKitWebRTC
+
+class ShareScreenManager: NSObject {
+    
+    static let shared = ShareScreenManager()
+    
+    public var isShareScreen: Bool = false
+    
+    public var startShare: (() -> Void)?
+    public var endShare: (() -> Void)?
+    public var pushShare: ((LKRTCVideoFrame) -> Void)?
+    
+    private var frameReader: SocketConnectionFrameReader? = nil
+    private var shareScreenCompleted: ((_ begin: Bool) -> Void)?
+
+    
+    private lazy var broadcastPickerView: RPSystemBroadcastPickerView = {
+        let broadcastPickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
+        broadcastPickerView.showsMicrophoneButton = false
+        
+        let screenShareExtensionId = Bundle.main.infoDictionary?["RTCScreenSharingExtension"] as? String
+        broadcastPickerView.preferredExtension =  screenShareExtensionId
+        return broadcastPickerView
+    }()
+
+    
+    override init() {
+        super.init()
+        initializeNotification()
+    }
+}
+
+extension ShareScreenManager {
+    
+    /// 屏幕分享
+    func shareScreen(completed:((_ begin: Bool) -> Void)? = nil) {
+        var btn = UIButton()
+        for item in broadcastPickerView.subviews {
+            if item.isKind(of: UIButton.self) {
+                btn = item as! UIButton
+            }
+        }
+        if #available(iOS 13, *) {
+            btn.sendActions(for: UIControl.Event.touchUpInside)
+        } else {
+            btn.sendActions(for: UIControl.Event.touchDown)
+        }
+        shareScreenCompleted = completed
+    }
+}
+
+extension ShareScreenManager {
+    
+    private func startCapture() {
+        guard let identifier = lookUpAppGroupIdentifier(),
+              let filePath = filePathForIdentifier(identifier) else { return }
+
+        let frameReader = SocketConnectionFrameReader()
+        guard let socketConnection = BroadcastServerSocketConnection(filePath: filePath, streamDelegate: frameReader) else { return  }
+        frameReader.didCapture = { pixelBuffer, rotation in
+            self.capture(pixelBuffer, rotation: rotation)
+        }
+        frameReader.startCapture(with: socketConnection)
+        self.frameReader = frameReader
+    }
+
+    private func stopCapture() {
+        frameReader?.stopCapture()
+        frameReader = nil
+    }
+
+    private func lookUpAppGroupIdentifier() -> String? {
+        Bundle.main.infoDictionary?["RTCAppGroupIdentifier"] as? String
+    }
+
+    private func filePathForIdentifier(_ identifier: String) -> String? {
+        guard let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier)
+        else { return nil }
+
+        let filePath = sharedContainer.appendingPathComponent("rtc_SSFD").path
+        return filePath
+    }
+}
+
+// MARK: - private methods
+extension ShareScreenManager {
+    
+    private func sendShareScreenData(sampleBuffer: CMSampleBuffer) {
+        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
+            return
+        }
+        let rtcPixelBuffer = LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer)
+        let timeStampNs = Int64(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000000000)
+        let rtcVideoFrame = LKRTCVideoFrame(buffer: rtcPixelBuffer, rotation: RTCVideoRotation._0, timeStampNs: timeStampNs)
+        self.pushShare?(rtcVideoFrame)
+    }
+    
+    private func capture(_ pixelBuffer: CVPixelBuffer,
+                         timeStampNs: Int64 = VideoCapturer.createTimeStampNs(),
+                         rotation: RTCVideoRotation = ._0) {
+        let rtcPixelBuffer = LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer)
+        let rtcVideoFrame = LKRTCVideoFrame(buffer: rtcPixelBuffer, rotation: RTCVideoRotation._0, timeStampNs: timeStampNs)
+        self.pushShare?(rtcVideoFrame)
+    }
+    
+    // MARK: - Notification
+    func initializeNotification() {
+        LiveKitDarwinNotificationCenter.shared.addObserver(self, selector: #selector(startScreenShareBySystemUINotification), notificationName: .broadcastStarted, object: nil)
+        LiveKitDarwinNotificationCenter.shared.addObserver(self, selector: #selector(endScreenShareBySystemUINotification), notificationName: .broadcastStopped, object: nil)
+    }
+
+    @objc func startScreenShareBySystemUINotification() {
+        isShareScreen = true
+        startShare?()
+        shareScreenCompleted?(true)
+        
+        startCapture()
+//        HXSampleHandlerClientSocketManager.shared.connect()
+//        HXSampleHandlerClientSocketManager.shared.bufferReceiveCallBack = { sampleBuffer in
+//            self.sendShareScreenData(sampleBuffer: sampleBuffer)
+//        }
+    }
+    
+    
+    @objc func endScreenShareBySystemUINotification() {
+        isShareScreen = false
+        endShare?()
+        shareScreenCompleted?(false)
+        stopCapture()
+//        HXSampleHandlerClientSocketManager.shared.discontent()
+    }
+}

+ 222 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Manager/Share/SocketConnectionFrameReader.swift

@@ -0,0 +1,222 @@
+//
+//  File.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/6/2.
+//
+
+import CoreImage
+import CoreVideo
+import Foundation
+import LiveKitWebRTC
+
+private class Message {
+    // Initializing a CIContext object is costly, so we use a singleton instead
+    static let imageContextVar: CIContext? = {
+        var imageContext = CIContext(options: nil)
+        return imageContext
+    }()
+
+    var imageBuffer: CVImageBuffer?
+    var didComplete: ((_ success: Bool, _ message: Message) -> Void)?
+    var imageOrientation: CGImagePropertyOrientation = .up
+    private var framedMessage: CFHTTPMessage?
+
+    init() {}
+
+    func appendBytes(buffer: [UInt8], length: Int) -> Int {
+        if framedMessage == nil {
+            framedMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, false).takeRetainedValue()
+        }
+
+        guard let framedMessage else {
+            return -1
+        }
+
+        CFHTTPMessageAppendBytes(framedMessage, buffer, length)
+        if !CFHTTPMessageIsHeaderComplete(framedMessage) {
+            return -1
+        }
+
+        guard let contentLengthStr = CFHTTPMessageCopyHeaderFieldValue(framedMessage, "Content-Length" as CFString)?.takeRetainedValue(),
+              let body = CFHTTPMessageCopyBody(framedMessage)?.takeRetainedValue()
+        else {
+            return -1
+        }
+
+        let contentLength = Int(CFStringGetIntValue(contentLengthStr))
+        let bodyLength = CFDataGetLength(body)
+
+        let missingBytesCount = contentLength - bodyLength
+        if missingBytesCount == 0 {
+            let success = unwrapMessage(framedMessage)
+            didComplete?(success, self)
+
+            self.framedMessage = nil
+        }
+
+        return missingBytesCount
+    }
+
+    private func imageContext() -> CIContext? {
+        Message.imageContextVar
+    }
+
+    private func unwrapMessage(_ framedMessage: CFHTTPMessage) -> Bool {
+        guard let widthStr = CFHTTPMessageCopyHeaderFieldValue(framedMessage, "Buffer-Width" as CFString)?.takeRetainedValue(),
+              let heightStr = CFHTTPMessageCopyHeaderFieldValue(framedMessage, "Buffer-Height" as CFString)?.takeRetainedValue(),
+              let imageOrientationStr = CFHTTPMessageCopyHeaderFieldValue(framedMessage, "Buffer-Orientation" as CFString)?.takeRetainedValue(),
+              let messageData = CFHTTPMessageCopyBody(framedMessage)?.takeRetainedValue()
+        else {
+            return false
+        }
+
+        let width = Int(CFStringGetIntValue(widthStr))
+        let height = Int(CFStringGetIntValue(heightStr))
+        imageOrientation = CGImagePropertyOrientation(rawValue: UInt32(CFStringGetIntValue(imageOrientationStr))) ?? .up
+
+        // Copy the pixel buffer
+        let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, nil, &imageBuffer)
+        if status != kCVReturnSuccess {
+            logger.log(level: .warning, "CVPixelBufferCreate failed")
+            return false
+        }
+
+        copyImageData(messageData as Data, to: imageBuffer)
+
+        return true
+    }
+
+    private func copyImageData(_ data: Data?, to pixelBuffer: CVPixelBuffer?) {
+        if let pixelBuffer {
+            CVPixelBufferLockBaseAddress(pixelBuffer, [])
+        }
+
+        var image: CIImage?
+        if let data {
+            image = CIImage(data: data)
+        }
+        if let image, let pixelBuffer {
+            imageContext()?.render(image, to: pixelBuffer)
+        }
+
+        if let pixelBuffer {
+            CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
+        }
+    }
+}
+
+class SocketConnectionFrameReader: NSObject {
+    private static let kMaxReadLength = 10 * 1024
+    private var readLength = 0
+
+    private var _connection: BroadcastServerSocketConnection?
+    private var connection: BroadcastServerSocketConnection? {
+        get { _connection }
+        set {
+            if _connection != newValue {
+                _connection?.close()
+                _connection = newValue
+            }
+        }
+    }
+
+    private var message: Message?
+    var didCapture: ((CVPixelBuffer, RTCVideoRotation) -> Void)?
+
+    override init() {}
+
+    func startCapture(with connection: BroadcastServerSocketConnection) {
+        self.connection = connection
+        message = nil
+
+        if !connection.open() {
+            stopCapture()
+        }
+    }
+
+    func stopCapture() {
+        connection?.close()
+        connection = nil
+    }
+
+    // MARK: Private Methods
+
+    func readBytes(from stream: InputStream) {
+        if !(stream.hasBytesAvailable) {
+            return
+        }
+
+        if message == nil {
+            message = Message()
+            readLength = SocketConnectionFrameReader.kMaxReadLength
+
+            weak var weakSelf = self
+            message?.didComplete = { success, message in
+                if success {
+                    weakSelf?.didCaptureVideoFrame(message.imageBuffer, with: message.imageOrientation)
+                }
+
+                weakSelf?.message = nil
+            }
+        }
+
+        guard let msg = message
+        else {
+            return
+        }
+
+        var buffer = [UInt8](repeating: 0, count: readLength)
+        let numberOfBytesRead = stream.read(&buffer, maxLength: readLength)
+        if numberOfBytesRead < 0 {
+            logger.log(level: .debug, "error reading bytes from stream")
+            return
+        }
+
+        readLength = msg.appendBytes(buffer: buffer, length: numberOfBytesRead)
+        if readLength == -1 || readLength > SocketConnectionFrameReader.kMaxReadLength {
+            readLength = SocketConnectionFrameReader.kMaxReadLength
+        }
+    }
+
+    func didCaptureVideoFrame(
+        _ pixelBuffer: CVPixelBuffer?,
+        with orientation: CGImagePropertyOrientation
+    ) {
+        guard let pixelBuffer else {
+            return
+        }
+
+        var rotation: RTCVideoRotation
+        switch orientation {
+        case .left:
+            rotation = ._90
+        case .down:
+            rotation = ._180
+        case .right:
+            rotation = ._270
+        default:
+            rotation = ._0
+        }
+
+        didCapture?(pixelBuffer, rotation)
+    }
+}
+
+extension SocketConnectionFrameReader: StreamDelegate {
+    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
+        switch eventCode {
+        case .openCompleted:
+            logger.log(level: .debug, "server stream open completed")
+        case .hasBytesAvailable:
+            readBytes(from: aStream as! InputStream)
+        case .endEncountered:
+            logger.log(level: .debug, "server stream end encountered")
+            stopCapture()
+        case .errorOccurred:
+            logger.log(level: .debug, "server stream error encountered: \(aStream.streamError?.localizedDescription ?? "")")
+        default:
+            break
+        }
+    }
+}

+ 51 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/AppConfigure.swift

@@ -0,0 +1,51 @@
+//
+//  AppConfigure.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+
+@objcMembers
+public class LiveKitConfigure: NSObject {
+    
+    /// 用户id
+    let userId: String
+    
+    /// 用户昵称
+    let userName: String
+    
+    /// 用户头像
+    let userAvatarURL: String?
+    
+    /// 用户头像规则 如:http://api.xxx.com/file/user-icon/{uid} ,{uid}为占位符
+    let userAvatarDomain: String?
+    
+    /// http url
+    let host: String
+    
+    /// http token
+    let token: String?
+    
+    /// Livekit 服务端地址
+    let liveKitServer: String
+    
+    public init(userId: String,
+                userName: String,
+                userAvatarURL: String?,
+                userAvatarDomain: String?,
+                host: String,
+                token: String?,
+                liveKitServer: String)
+    {
+        self.userId = userId
+        self.userName = userName
+        self.userAvatarURL = userAvatarURL
+        self.userAvatarDomain = userAvatarDomain
+        self.host = host
+        self.token = token
+        self.liveKitServer = liveKitServer
+    }
+    
+}

+ 57 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/CreateMeetingModel.swift

@@ -0,0 +1,57 @@
+//
+//  MeetingInfoModel.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+
+@objcMembers
+public class CreateMeetingModel: NSObject  {
+    
+    /// 会议创建人id (目前发起人才能邀请)
+    var creatorId: String
+    
+    /// 会议创建人名称
+    var creatorName: String
+    
+    /// 会议id
+    var meetingId: String
+    
+    /// 会议标题
+    var meetingTitle: String?
+
+    /// 会议描述
+    var meetingDescription: String?
+    
+    /// 会议成员
+    var members: [UserAccount] = []
+    
+    /// 预约时间
+    var dueTime: String?
+    
+    /// 自己的麦克风状态
+    var microphoneOn: Bool = false
+    
+    /// 自己的摄像头状态
+    var cameraOn: Bool = false
+    
+    /// 房间是否全部禁言
+    var muted: Bool = false
+    
+    public init(creatorId: String, creatorName: String, meetingId: String, meetingTitle: String? = nil, meetingDescription: String? = nil, members: [UserAccount] = [], dueTime: String? = nil, microphoneOn: Bool = false, cameraOn: Bool = false, muted: Bool = false) {
+        self.creatorId = creatorId
+        self.creatorName = creatorName
+        self.meetingId = meetingId
+        self.meetingTitle = meetingTitle
+        self.meetingDescription = meetingDescription
+        self.members = members
+        self.dueTime = dueTime
+        self.microphoneOn = microphoneOn
+        self.cameraOn = cameraOn
+        self.muted = muted
+    }
+     
+}
+

+ 25 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/ICEServer.swift

@@ -0,0 +1,25 @@
+//
+//  ICEServer.swift
+//  WebRTCPlugin
+//
+//  Created by Bugu on 2023/8/29.
+//
+
+import Foundation
+
+
+@objcMembers
+public class ICEServer: NSObject {
+    
+    let uri: String
+    
+    let username: String?
+    
+    let password: String?
+    
+    public init(uri: String, username: String? = nil, password: String? = nil) {
+        self.uri = uri
+        self.username = username
+        self.password = password
+    }
+}

+ 93 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/ImageMessageElem.swift

@@ -0,0 +1,93 @@
+//
+//  MessageImageElem.swift
+//  Pods
+//
+//  Created by Tan Cheng on 2025/4/3.
+//
+
+// MARK: - 图片消息内容
+public class ImageMessageElem: Codable {
+    
+    /// 文件存储目录
+    var bucket: String?
+    
+    /// 原图文件key
+    var image: String?
+    
+    /// 缩略图文件key
+    var thumb: String?
+    
+    /// 原图高度px
+    var oh: Int?
+    
+    /// 原图宽度px
+    var ow: Int?
+    
+    /// 缩略图高度px
+    var th: Int?
+    
+    /// 缩略图宽度px
+    var tw: Int?
+    
+    /// 图片本地路径,发送方和已经下载图片后的接收方可以获取到
+    var path: String?
+    
+    /// 缩略图图片本地路径,发送方和已经下载图片后的接收方可以获取到
+    var thumbPath: String?
+  
+    init(bucket: String?, image: String?, thumb: String?, oh: Int?, ow: Int?, th: Int?, tw: Int?, path: String?, thumbPath: String?) {
+        self.bucket = bucket
+        self.image = image
+        self.thumb = thumb
+        self.oh = oh
+        self.ow = ow
+        self.th = th
+        self.tw = tw
+        self.path = path
+        self.thumbPath = thumbPath
+    }
+    
+}
+
+extension ImageMessageElem: Identifiable {
+    
+    var url: String? {
+        guard let bucket = bucket, let image = image, bucket.isNotEmpty, image.isNotEmpty else {
+            return nil
+        }
+        return "\(HttpService.baseURL)/file/\(bucket)/\(image)"
+    }
+    
+    var thumburl: String? {
+        guard let bucket = bucket, let image = image, let thumb = thumb, bucket.isNotEmpty, image.isNotEmpty else {
+            return nil
+        }
+        return "\(HttpService.baseURL)/file/\(bucket)/\(thumb)"
+    }
+    
+    var localPath: String? {
+        if let path = path, path.isNotEmpty {
+            return path
+        }
+        
+        guard let image = image, image.isNotEmpty else {
+            return nil
+        }
+        
+        let path = FileKit.path(for: FilePath.localImagePath).path.appendingPathComponent(image)
+        return path
+    }
+    
+    var thumbLocalPath: String? {
+        if let thumbPath = thumbPath, thumbPath.isNotEmpty {
+            return thumbPath
+        }
+        
+        guard let thumb = thumb, image.isNotEmpty else {
+            return nil
+        }
+ 
+        let path = FileKit.path(for: FilePath.localImagePath).path.appendingPathComponent(thumb)
+        return path
+    }
+}

+ 67 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/MeetingInfoModel.swift

@@ -0,0 +1,67 @@
+//
+//  MeetingInfoModel.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/21.
+//
+
+import Foundation
+
+class MeetingInfoModel: CreateMeetingModel {
+    
+    override init(creatorId: String, creatorName: String, meetingId: String, meetingTitle: String? = nil, meetingDescription: String? = nil, members: [UserAccount] = [], dueTime: String? = nil, microphoneOn: Bool = false, cameraOn: Bool = false, muted: Bool = false) {
+        super.init(creatorId: creatorId, creatorName: creatorName, meetingId: meetingId, meetingTitle: meetingTitle, meetingDescription: meetingDescription, members: members, dueTime: dueTime, microphoneOn: microphoneOn, cameraOn: cameraOn, muted: muted)
+    }
+    
+    init(createInfo: CreateMeetingModel) {
+        super.init(creatorId: createInfo.creatorId, creatorName: createInfo.creatorName, meetingId: createInfo.meetingId, meetingTitle: createInfo.meetingTitle, meetingDescription: createInfo.meetingDescription, members: createInfo.members, dueTime: createInfo.dueTime, microphoneOn: createInfo.microphoneOn, cameraOn: createInfo.cameraOn, muted: createInfo.muted)
+    }
+    
+    public var isHost: Bool {
+        return creatorId == AppStorage.shared.user?.userId
+    }
+    
+}
+
+extension MeetingInfoModel {
+    
+    func removeMember(userId: String) {
+        guard let index = members.firstIndex(where: { $0.userId == userId }) else { return }
+        members.remove(at: index)
+    }
+    
+    func addMember(userInfo: UserAccount) {
+        members.append(userInfo)
+    }
+    
+    func existMember(userId: String) -> Bool {
+        return members.contains(where: { $0.userId == userId })
+    }
+}
+
+
+extension MeetingInfoModel {
+
+    static func dictionaryToModel(dic: [String : Any]?) -> MeetingInfoModel? {
+        guard let dic = dic else { return nil }
+        guard let creatorId = dic["uid"] as? Int,
+              let creatorName = dic["name"] as? String,
+              let meetingId = dic["tag"] as? String else {
+            return nil
+        }
+        
+        let meetingTitle = dic["title"] as? String
+        let meetingDescription = dic["description"] as? String
+        let dueTime = dic["dueTime"] as? String
+        let membersDic = dic["member"] as? [String : String]
+        
+        var members: [UserAccount] = []
+        membersDic?.forEach({ key,value in
+            let account = UserAccount(id: key, name: value)
+            members.append(account)
+        })
+        
+        let meetingInfo = MeetingInfoModel(creatorId: "\(creatorId)", creatorName: creatorName, meetingId: meetingId, meetingTitle: meetingTitle, meetingDescription: meetingDescription, members: members, dueTime: dueTime, microphoneOn: false, cameraOn: false)
+        return meetingInfo
+    }
+}

+ 87 - 0
Frameworks/LiveKitPlugin/LiveKitPlugin/Classes/Core/Model/UserAccount.swift

@@ -0,0 +1,87 @@
+//
+//  UserAccount.swift
+//  LiveKitPlugin
+//
+//  Created by Bugu on 2024/5/15.
+//
+
+import Foundation
+
+@objcMembers
+public class UserAccount: NSObject {
+    
+    /// 用户id
+    var id: String?
+    
+    /// 昵称
+    var name: String?
+    
+    /// logo
+    var logo: String?
+    
+    public init(id: String? = nil, name: String? = nil, logo: String? = nil) {
+        self.id = id
+        self.name = name
+        self.logo = logo
+    }
+}
+
+
+extension UserAccount {
+    
+   var namePinYin: String {
+       guard let name = self.name else { return "#" }
+       let mutableString = NSMutableString(string: name)
+       //把汉字转为拼音
+       CFStringTransform(mutableString, nil, kCFStringTransformToLatin, false)
+       //去掉拼音的音标
+       CFStringTransform(mutableString, nil, kCFStringTransformStripDiacritics, false)
+       let string = String(mutableString)
+       //去掉空格
+       return string.replacingOccurrences(of: " ", with: "")
+   }
+   
+   var namePinYinHead: String {
+       return namePinYin.first?.uppercased() ?? "#"
+   }
+}
+
+extension UserAccount {
+    
+    var userId: String? {
+        return id
+    }
+    
+    var displayName: String? {
+        return name
+    }
+    
+    var avatarURL: URL? {
+        guard let logo = logo, logo.count > 0 else {
+            guard let userId = userId else {
+                return nil
+            }
+            
+            guard var avatarDomain = AppStorage.shared.userAvatarDomain, avatarDomain.count > 0 else {
+                return nil
+            }
+            
+            avatarDomain = avatarDomain.replacingOccurrences(of: "{uid}", with: userId)
+            return URL(string: avatarDomain)
+        }
+        return URL(string: logo)
+    }
+    
+    var organizationName: String? {
+        return nil
+    }
+    
+    var departmentName: String? {
+        return nil
+    }
+    
+    var jobTitle: String? {
+        return nil
+    }
+}
+

Vissa filer visades inte eftersom för många filer har ändrats