FabrizioBrancati/BFKit-Swift

View on GitHub
Sources/BFKit/Linux/Foundation/FileManager+Extensions.swift

Summary

Maintainability
C
7 hrs
Test Coverage
//
//  FileManager+Extensions.swift
//  BFKit-Swift
//
//  The MIT License (MIT)
//
//  Copyright (c) 2015 - 2019 Fabrizio Brancati.
//
//  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: - FileManager extension

/// This extension adds some useful functions to FileManager.
public extension FileManager {
    // MARK: - Variables
    
    /// Path type enum.
    enum PathType: Int {
        /// Main bundle path.
        case mainBundle
        /// Library path.
        case library
        /// Documents path.
        case documents
        /// Cache path.
        case cache
        /// Application Support path.
        case applicationSupport
        /// Temporary path.
        case temporary
    }
    
    // MARK: - Functions
    
    /// Get the path for a PathType.
    ///
    /// - Parameter path: Path type.
    /// - Returns: Returns the path type String.
    func pathFor(_ path: PathType) -> String? {
        var pathString: String?
        
        switch path {
        case .mainBundle:
            pathString = mainBundlePath()

        case .library:
            pathString = libraryPath()

        case .documents:
            pathString = documentsPath()

        case .cache:
            pathString = cachePath()

        case .applicationSupport:
            pathString = applicationSupportPath()

        case .temporary:
            pathString = temporaryPath()
        }
        
        return pathString
    }
    
    /// Save a file with given content.
    ///
    /// - Parameters:
    ///   - file: File to be saved.
    ///   - path: File path.
    ///   - content: Content to be saved.
    /// - Throws: write(toFile:, atomically:, encoding:) errors.
    func save(file: String, in path: PathType, content: String) throws {
        guard let path = FileManager.default.pathFor(path) else {
            return
        }
        
        try content.write(toFile: path.appendingPathComponent(file), atomically: true, encoding: .utf8)
    }
    
    /// Read a file an returns the content as String.
    ///
    /// - Parameters:
    ///   - file: File to be read.
    ///   - path: File path.
    /// - Returns: Returns the content of the file a String.
    /// - Throws: Throws String(contentsOfFile:, encoding:) errors.
    func read(file: String, from path: PathType) throws -> String? {
        guard let path = FileManager.default.pathFor(path) else {
            return nil
        }
        
        return try String(contentsOfFile: path.appendingPathComponent(file), encoding: .utf8)
    }
    
    /// Save an object into a PLIST with given filename.
    ///
    /// - Parameters:
    ///   - object: Object to save into PLIST.
    ///   - path: Path of PLIST.
    ///   - filename: PLIST filename.
    /// - Returns: Returns true if the operation was successful, otherwise false.
    @discardableResult
    func savePlist(object: Any, in path: PathType, filename: String) -> Bool {
        let path = checkPlist(path: path, filename: filename)
        
        guard !path.exist else {
            return NSKeyedArchiver.archiveRootObject(object, toFile: path.path)
        }
        
        return false
    }
    
    /// Load an object from a PLIST with given filename.
    ///
    /// - Parameters:
    ///   - path: Path of PLIST.
    ///   - filename: PLIST filename.
    /// - Returns: Returns the loaded object.
    func readPlist(from path: PathType, filename: String) -> Any? {
        let path = checkPlist(path: path, filename: filename)
        
        guard !path.exist else {
            return NSKeyedUnarchiver.unarchiveObject(withFile: path.path)
        }
        
        return nil
    }
    
    /// Check if plist exist.
    ///
    /// - Parameters:
    ///   - path: Path of plist.
    ///   - filename: Plist filename.
    /// - Returns: Returns if plist exists and path.
    private func checkPlist(path: PathType, filename: String) -> (exist: Bool, path: String) {
        guard let path = FileManager.default.pathFor(path), let finalPath = path.appendingPathComponent(filename).appendingPathExtension("plist") else {
            return (false, "")
        }
        
        return (true, finalPath)
    }
    
    /// Get Main Bundle path for a filename.
    /// If no file is specified, the main bundle path will be returned.
    ///
    /// - Parameter file: Filename.
    /// - Returns: Returns the path as a String.
    func mainBundlePath(file: String = "") -> String? {
        file.isEmpty ? Bundle.main.bundlePath : Bundle.main.path(forResource: file.deletingPathExtension, ofType: file.pathExtension)
    }
    
    /// Get Documents path for a filename.
    ///
    /// - Parameter file: Filename
    /// - Returns: Returns the path as a String.
    func documentsPath(file: String = "") -> String? {
        guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            return nil
        }
        
        return documentsURL.path.appendingPathComponent(file)
    }
    
    /// Get Library path for a filename.
    ///
    /// - Parameter file: Filename
    /// - Returns: Returns the path as a String.
    func libraryPath(file: String = "") -> String? {
        guard let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else {
            return nil
        }
        
        return libraryURL.path.appendingPathComponent(file)
    }
    
    /// Get Cache path for a filename.
    ///
    /// - Parameter file: Filename
    /// - Returns: Returns the path as a String.
    func cachePath(file: String = "") -> String? {
        guard let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
            return nil
        }
        
        return cacheURL.path.appendingPathComponent(file)
    }
    
    /// Get Application Support path for a filename.
    ///
    /// - Parameter file: Filename
    /// - Returns: Returns the path as a String.
    func applicationSupportPath(file: String = "") -> String? {
        guard let applicationSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
            return nil
        }
        
        if !FileManager.default.fileExists(atPath: applicationSupportURL.absoluteString, isDirectory: nil) {
            do {
                try FileManager.default.createDirectory(atPath: applicationSupportURL.path, withIntermediateDirectories: true, attributes: nil)
            } catch {
                return nil
            }
        }
        
        return applicationSupportURL.path.appendingPathComponent(file)
    }
    
    /// Get Temporary path for a filename.
    ///
    /// - Parameter file: Filename.
    /// - Returns: Returns the path as a String.
    func temporaryPath(file: String = "") -> String? {
        NSTemporaryDirectory().appendingPathComponent(file)
    }
    
    /// Returns the file size.
    ///
    /// - Parameters:
    ///   - file: Filename.
    ///   - path: Path of the file.
    /// - Returns: Returns the file size.
    /// - Throws: Throws FileManager.default.attributesOfItem(atPath:) errors.
    func size(file: String, from path: PathType) throws -> Float? {
        if !file.isEmpty {
            guard let path = FileManager.default.pathFor(path) else {
                return nil
            }
            
            let finalPath = path.appendingPathComponent(file)
            
            if FileManager.default.fileExists(atPath: finalPath) {
                let fileAttributes = try FileManager.default.attributesOfItem(atPath: finalPath)
                return fileAttributes[FileAttributeKey.size] as? Float
            }
        }
        
        return nil
    }
    
    /// Delete a file with the given filename.
    ///
    /// - Parameters:
    ///   - file: File to delete.
    ///   - path: Path of the file.
    /// - Throws: Throws FileManager.default.removeItem(atPath:) errors.
    func delete(file: String, from path: PathType) throws {
        if !file.isEmpty {
            guard let path = FileManager.default.pathFor(path) else {
                throw BFKitError.pathNotExist
            }
            
            if FileManager.default.fileExists(atPath: path.appendingPathComponent(file)) {
                try FileManager.default.removeItem(atPath: path.appendingPathComponent(file))
            }
        }
    }
    
    /// Move a file from a path to another.
    ///
    /// - Parameters:
    ///   - file: Filename to move.
    ///   - origin: Origin path of the file.
    ///   - destination:  Destination path of the file.
    /// - Returns: Returns true if the operation was successful, otherwise false.
    /// - Throws: Throws FileManager.default.moveItem(atPath:, toPath:) and BFKitError errors.
    func move(file: String, from origin: PathType, to destination: PathType) throws {
        let paths = try check(file: file, origin: origin, destination: destination)
        
        if paths.fileExist {
            try FileManager.default.moveItem(atPath: paths.origin, toPath: paths.destination)
        }
    }
    
    /// Copy a file into another path.
    ///
    /// - Parameters:
    ///   - file: Filename to copy.
    ///   - origin: Origin path
    ///   - destination: Destination path
    /// - Returns: Returns true if the operation was successful, otherwise false.
    /// - Throws: Throws FileManager.default.copyItem(atPath:, toPath:) and BFKitError errors.
    func copy(file: String, from origin: PathType, to destination: PathType) throws {
        let paths = try check(file: file, origin: origin, destination: destination)
        
        if paths.fileExist {
            try FileManager.default.copyItem(atPath: paths.origin, toPath: paths.destination)
        }
    }
    
    /// Check is orign path, destination path and file exists.
    ///
    /// - Parameters:
    ///   - file: File.
    ///   - origin: Origin path.
    ///   - destination: Destination path.
    /// - Returns: Returns a tuple with origin, destination and if file exist.
    /// - Throws: Throws BFKitError errors.
    private func check(file: String, origin: PathType, destination: PathType) throws -> (origin: String, destination: String, fileExist: Bool) { // swiftlint:disable:this large_tuple
        guard let originPath = FileManager.default.pathFor(origin), let destinationPath = FileManager.default.pathFor(destination) else {
            throw BFKitError.pathNotExist
        }
        
        guard destination != .mainBundle else {
            throw BFKitError.pathNotAllowed
        }
        
        let finalOriginPath = originPath.appendingPathComponent(file)
        let finalDestinationPath = destinationPath.appendingPathComponent(file)
        
        guard !FileManager.default.fileExists(atPath: finalOriginPath) else {
            return (finalOriginPath, finalDestinationPath, true)
        }
        
        return (finalOriginPath, finalDestinationPath, false)
    }
    
    /// Rename a file with another filename.
    ///
    /// - Parameters:
    ///   - file: Filename to rename.
    ///   - origin: Origin path.
    ///   - newName: New filename.
    /// - Returns: Returns true if the operation was successful, otherwise false.
    /// - Throws: Throws FileManager.default.copyItem(atPath:, toPath:), FileManager.default.removeItem(atPath:, toPath:) and BFKitError errors.
    func rename(file: String, in origin: PathType, to newName: String) throws {
        guard let originPath = FileManager.default.pathFor(origin) else {
            throw BFKitError.pathNotExist
        }
        
        let finalOriginPath = originPath.appendingPathComponent(file)
        
        if FileManager.default.fileExists(atPath: finalOriginPath) {
            let destinationPath: String = finalOriginPath.replacingOccurrences(of: file, with: newName)
            try FileManager.default.copyItem(atPath: finalOriginPath, toPath: destinationPath)
            try FileManager.default.removeItem(atPath: finalOriginPath)
        }
    }
    
    /// Set settings for object and key. The file will be saved in the Library path if not exist.
    ///
    /// - Parameters:
    ///   - filename: Settings filename. "-Settings" will be automatically added.
    ///   - object: Object to set.
    ///   - objKey: Object key.
    /// - Returns: Returns true if the operation was successful, otherwise false.
    /// - Throws: Throws BFKitError errors.
    @discardableResult
    func setSettings(filename: String, object: Any, forKey objectKey: String) -> Bool {
        guard var path = FileManager.default.pathFor(.applicationSupport) else {
            return false
        }
        
        path = path.appendingPathComponent("\(filename)-Settings.plist")
        
        var settings: [String: Any]
        
        if let plistData = try? Data(contentsOf: URL(fileURLWithPath: path)), let plistFile = try? PropertyListSerialization.propertyList(from: plistData, format: nil), let plistDictionary = plistFile as? [String: Any] {
            settings = plistDictionary
        } else {
            settings = [:]
        }
        
        settings[objectKey] = object
        
        do {
            let plistData = try PropertyListSerialization.data(fromPropertyList: settings, format: .xml, options: 0)
            try plistData.write(to: URL(fileURLWithPath: path))
            
            return true
        } catch {
            return false
        }
    }
    
    /// Get settings for key.
    ///
    /// - Parameters:
    ///   - filename: Settings filename. "-Settings" will be automatically added.
    ///   - forKey: Object key.
    /// - Returns: Returns the object for the given key.
    func getSettings(filename: String, forKey objectKey: String) -> Any? {
        guard var path = FileManager.default.pathFor(.applicationSupport) else {
            return nil
        }
        
        path = path.appendingPathComponent("\(filename)-Settings.plist")
        
        var settings: [String: Any]
        
        if let plistData = try? Data(contentsOf: URL(fileURLWithPath: path)), let plistFile = try? PropertyListSerialization.propertyList(from: plistData, format: nil), let plistDictionary = plistFile as? [String: Any] {
            settings = plistDictionary
        } else {
            settings = [:]
        }
        
        return settings[objectKey]
    }
}