2022-04-19 12:29:03 +04:00
|
|
|
//
|
|
|
|
// FileUtils.swift
|
|
|
|
// SimpleX (iOS)
|
|
|
|
//
|
|
|
|
// Created by JRoberts on 15.04.2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import SwiftUI
|
2022-05-31 07:55:13 +01:00
|
|
|
import OSLog
|
|
|
|
|
|
|
|
let logger = Logger()
|
2022-04-19 12:29:03 +04:00
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
// maximum image file size to be auto-accepted
|
2022-05-31 07:55:13 +01:00
|
|
|
public let maxImageSize: Int64 = 236700
|
2022-04-25 12:44:24 +04:00
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public let maxFileSize: Int64 = 8000000
|
2022-05-04 09:10:36 +04:00
|
|
|
|
2022-09-17 16:41:20 +04:00
|
|
|
private let CHAT_DB: String = "_chat.db"
|
|
|
|
|
|
|
|
private let AGENT_DB: String = "_agent.db"
|
|
|
|
|
|
|
|
private let CHAT_DB_BAK: String = "_chat.db.bak"
|
|
|
|
|
|
|
|
private let AGENT_DB_BAK: String = "_agent.db.bak"
|
|
|
|
|
2022-06-24 13:52:20 +01:00
|
|
|
public func getDocumentsDirectory() -> URL {
|
|
|
|
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
|
|
}
|
|
|
|
|
|
|
|
func getGroupContainerDirectory() -> URL {
|
2022-06-02 13:16:22 +01:00
|
|
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
|
2022-06-24 13:52:20 +01:00
|
|
|
func getAppDirectory() -> URL {
|
|
|
|
dbContainerGroupDefault.get() == .group
|
|
|
|
? getGroupContainerDirectory()
|
|
|
|
: getDocumentsDirectory()
|
|
|
|
// getDocumentsDirectory()
|
|
|
|
}
|
|
|
|
|
|
|
|
let DB_FILE_PREFIX = "simplex_v1"
|
|
|
|
|
|
|
|
func getLegacyDatabasePath() -> URL {
|
|
|
|
getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false)
|
|
|
|
}
|
|
|
|
|
|
|
|
public func getAppDatabasePath() -> URL {
|
|
|
|
dbContainerGroupDefault.get() == .group
|
|
|
|
? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false)
|
|
|
|
: getLegacyDatabasePath()
|
|
|
|
}
|
|
|
|
|
2022-09-17 16:41:20 +04:00
|
|
|
func fileModificationDate(_ path: String) -> Date? {
|
|
|
|
do {
|
|
|
|
let attr = try FileManager.default.attributesOfItem(atPath: path)
|
|
|
|
return attr[FileAttributeKey.modificationDate] as? Date
|
|
|
|
} catch {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-19 19:05:29 +04:00
|
|
|
public func deleteAppFiles() {
|
|
|
|
let fm = FileManager.default
|
|
|
|
do {
|
|
|
|
let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path)
|
|
|
|
for fileName in fileNames {
|
|
|
|
removeFile(fileName)
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func fileSize(_ url: URL) -> Int? { // in bytes
|
|
|
|
do {
|
|
|
|
let val = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey])
|
|
|
|
return val.totalFileAllocatedSize ?? val.fileAllocatedSize
|
|
|
|
} catch {
|
|
|
|
logger.error("FileUtils fileSize error: \(error.localizedDescription)")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public func directoryFileCountAndSize(_ dir: URL) -> (Int, Int)? { // size in bytes
|
|
|
|
let fm = FileManager.default
|
|
|
|
if let enumerator = fm.enumerator(at: dir, includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey], options: [], errorHandler: { (_, error) -> Bool in
|
|
|
|
logger.error("FileUtils directoryFileCountAndSize error: \(error.localizedDescription)")
|
|
|
|
return false
|
|
|
|
}) {
|
|
|
|
var fileCount = 0
|
|
|
|
var bytes = 0
|
|
|
|
for case let url as URL in enumerator {
|
|
|
|
fileCount += 1
|
|
|
|
bytes += fileSize(url) ?? 0
|
|
|
|
}
|
|
|
|
return (fileCount, bytes)
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-17 16:41:20 +04:00
|
|
|
public func hasBackup(newerThan date: Date) -> Bool {
|
|
|
|
let dbPath = getAppDatabasePath().path
|
|
|
|
return hasBackupFile(dbPath + AGENT_DB_BAK, newerThan: date)
|
|
|
|
&& hasBackupFile(dbPath + CHAT_DB_BAK, newerThan: date)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func hasBackupFile(_ path: String, newerThan date: Date) -> Bool {
|
|
|
|
let fm = FileManager.default
|
|
|
|
return fm.fileExists(atPath: path)
|
|
|
|
&& date <= (fileModificationDate(path) ?? Date.distantPast)
|
|
|
|
}
|
|
|
|
|
|
|
|
public func restoreBackup() throws {
|
|
|
|
let dbPath = getAppDatabasePath().path
|
|
|
|
try restoreBackupFile(fromPath: dbPath + AGENT_DB_BAK, toPath: dbPath + AGENT_DB)
|
|
|
|
try restoreBackupFile(fromPath: dbPath + CHAT_DB_BAK, toPath: dbPath + CHAT_DB)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func restoreBackupFile(fromPath: String, toPath: String) throws {
|
|
|
|
let fm = FileManager.default
|
|
|
|
if fm.fileExists(atPath: toPath) {
|
|
|
|
try fm.removeItem(atPath: toPath)
|
|
|
|
}
|
|
|
|
try fm.copyItem(atPath: fromPath, toPath: toPath)
|
|
|
|
}
|
|
|
|
|
2022-06-24 13:52:20 +01:00
|
|
|
public func hasLegacyDatabase() -> Bool {
|
2022-09-07 12:49:41 +01:00
|
|
|
hasDatabaseAtPath(getLegacyDatabasePath())
|
|
|
|
}
|
|
|
|
|
|
|
|
public func hasDatabase() -> Bool {
|
|
|
|
hasDatabaseAtPath(getAppDatabasePath())
|
|
|
|
}
|
|
|
|
|
|
|
|
func hasDatabaseAtPath(_ dbPath: URL) -> Bool {
|
2022-06-24 13:52:20 +01:00
|
|
|
let fm = FileManager.default
|
2022-09-17 16:41:20 +04:00
|
|
|
return fm.isReadableFile(atPath: dbPath.path + AGENT_DB) &&
|
|
|
|
fm.isReadableFile(atPath: dbPath.path + CHAT_DB)
|
2022-06-24 13:52:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public func removeLegacyDatabaseAndFiles() -> Bool {
|
|
|
|
let dbPath = getLegacyDatabasePath()
|
|
|
|
let appFiles = getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
|
|
|
|
let fm = FileManager.default
|
2022-09-17 16:41:20 +04:00
|
|
|
let r1 = nil != (try? fm.removeItem(atPath: dbPath.path + AGENT_DB))
|
|
|
|
let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + CHAT_DB))
|
|
|
|
try? fm.removeItem(atPath: dbPath.path + AGENT_DB_BAK)
|
|
|
|
try? fm.removeItem(atPath: dbPath.path + CHAT_DB_BAK)
|
2022-06-24 13:52:20 +01:00
|
|
|
try? fm.removeItem(at: appFiles)
|
|
|
|
return r1 && r2
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func getAppFilesDirectory() -> URL {
|
2022-06-24 13:52:20 +01:00
|
|
|
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
|
2022-05-06 21:10:32 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
func getAppFilePath(_ fileName: String) -> URL {
|
|
|
|
getAppFilesDirectory().appendingPathComponent(fileName)
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func getLoadedFilePath(_ file: CIFile?) -> String? {
|
2022-04-19 12:29:03 +04:00
|
|
|
if let file = file,
|
2022-05-07 16:25:04 +04:00
|
|
|
file.loaded,
|
2022-04-19 12:29:03 +04:00
|
|
|
let savedFile = file.filePath {
|
2022-05-06 21:10:32 +04:00
|
|
|
return getAppFilePath(savedFile).path
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
2022-05-07 16:25:04 +04:00
|
|
|
if let filePath = getLoadedFilePath(file) {
|
2022-04-19 12:29:03 +04:00
|
|
|
return UIImage(contentsOfFile: filePath)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-05-04 16:08:40 +04:00
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func saveFileFromURL(_ url: URL) -> String? {
|
2022-05-06 21:10:32 +04:00
|
|
|
let savedFile: String?
|
|
|
|
if url.startAccessingSecurityScopedResource() {
|
|
|
|
do {
|
|
|
|
let fileData = try Data(contentsOf: url)
|
|
|
|
let fileName = uniqueCombine(url.lastPathComponent)
|
|
|
|
savedFile = saveFile(fileData, fileName)
|
|
|
|
} catch {
|
|
|
|
logger.error("FileUtils.saveFileFromURL error: \(error.localizedDescription)")
|
|
|
|
savedFile = nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.error("FileUtils.saveFileFromURL startAccessingSecurityScopedResource returned false")
|
|
|
|
savedFile = nil
|
|
|
|
}
|
|
|
|
url.stopAccessingSecurityScopedResource()
|
|
|
|
return savedFile
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func saveImage(_ uiImage: UIImage) -> String? {
|
2022-05-06 21:10:32 +04:00
|
|
|
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: maxImageSize) {
|
2022-05-07 16:25:04 +04:00
|
|
|
let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss")
|
|
|
|
let fileName = uniqueCombine("IMG_\(timestamp).jpg")
|
2022-05-06 21:10:32 +04:00
|
|
|
return saveFile(imageDataResized, fileName)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-05-07 16:25:04 +04:00
|
|
|
extension Date {
|
|
|
|
func getFormattedDate(_ format: String) -> String {
|
|
|
|
let df = DateFormatter()
|
|
|
|
df.dateFormat = format
|
|
|
|
df.locale = Locale(identifier: "US")
|
|
|
|
return df.string(from: self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-06 21:10:32 +04:00
|
|
|
private func saveFile(_ data: Data, _ fileName: String) -> String? {
|
|
|
|
let filePath = getAppFilePath(fileName)
|
|
|
|
do {
|
|
|
|
try data.write(to: filePath)
|
|
|
|
return fileName
|
|
|
|
} catch {
|
|
|
|
logger.error("FileUtils.saveFile error: \(error.localizedDescription)")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func uniqueCombine(_ fileName: String) -> String {
|
|
|
|
func tryCombine(_ fileName: String, _ n: Int) -> String {
|
2022-06-24 13:52:20 +01:00
|
|
|
let ns = fileName as NSString
|
|
|
|
let name = ns.deletingPathExtension
|
|
|
|
let ext = ns.pathExtension
|
2022-05-06 21:10:32 +04:00
|
|
|
let suffix = (n == 0) ? "" : "_\(n)"
|
|
|
|
let f = "\(name)\(suffix).\(ext)"
|
|
|
|
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
|
|
|
|
}
|
|
|
|
return tryCombine(fileName, 0)
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func removeFile(_ fileName: String) {
|
2022-05-06 21:10:32 +04:00
|
|
|
do {
|
|
|
|
try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path)
|
|
|
|
} catch {
|
|
|
|
logger.error("FileUtils.removeFile error: \(error.localizedDescription)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-04 16:08:40 +04:00
|
|
|
// image utils
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func dropImagePrefix(_ s: String) -> String {
|
2022-05-04 16:08:40 +04:00
|
|
|
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func dropPrefix(_ s: String, _ prefix: String) -> String {
|
|
|
|
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func cropToSquare(_ image: UIImage) -> UIImage {
|
2022-05-04 16:08:40 +04:00
|
|
|
let size = image.size
|
|
|
|
let side = min(size.width, size.height)
|
|
|
|
let newSize = CGSize(width: side, height: side)
|
|
|
|
var origin = CGPoint.zero
|
|
|
|
if size.width > side {
|
|
|
|
origin.x -= (size.width - side) / 2
|
|
|
|
} else if size.height > side {
|
|
|
|
origin.y -= (size.height - side) / 2
|
|
|
|
}
|
|
|
|
return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size))
|
|
|
|
}
|
|
|
|
|
2022-05-06 21:10:32 +04:00
|
|
|
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
|
2022-05-04 16:08:40 +04:00
|
|
|
var img = image
|
|
|
|
var data = img.jpegData(compressionQuality: 0.85)
|
|
|
|
var dataSize = data?.count ?? 0
|
|
|
|
while dataSize != 0 && dataSize > maxDataSize {
|
|
|
|
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
|
|
|
let clippedRatio = min(ratio, 2.0)
|
|
|
|
img = reduceSize(img, ratio: clippedRatio)
|
|
|
|
data = img.jpegData(compressionQuality: 0.85)
|
|
|
|
dataSize = data?.count ?? 0
|
|
|
|
}
|
|
|
|
logger.debug("resizeImageToDataSize final \(dataSize)")
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
2022-05-04 16:08:40 +04:00
|
|
|
var img = image
|
|
|
|
var str = compressImageStr(img)
|
|
|
|
var dataSize = str?.count ?? 0
|
|
|
|
while dataSize != 0 && dataSize > maxDataSize {
|
|
|
|
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
|
|
|
let clippedRatio = min(ratio, 2.0)
|
|
|
|
img = reduceSize(img, ratio: clippedRatio)
|
|
|
|
str = compressImageStr(img)
|
|
|
|
dataSize = str?.count ?? 0
|
|
|
|
}
|
|
|
|
logger.debug("resizeImageToStrSize final \(dataSize)")
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85) -> String? {
|
|
|
|
if let data = image.jpegData(compressionQuality: compressionQuality) {
|
|
|
|
return "data:image/jpg;base64,\(data.base64EncodedString())"
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func reduceSize(_ image: UIImage, ratio: CGFloat) -> UIImage {
|
|
|
|
let newSize = CGSize(width: floor(image.size.width / ratio), height: floor(image.size.height / ratio))
|
|
|
|
let bounds = CGRect(origin: .zero, size: newSize)
|
|
|
|
return resizeImage(image, newBounds: bounds, drawIn: bounds)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect) -> UIImage {
|
|
|
|
let format = UIGraphicsImageRendererFormat()
|
|
|
|
format.scale = 1.0
|
|
|
|
format.opaque = true
|
|
|
|
return UIGraphicsImageRenderer(bounds: newBounds, format: format).image { _ in
|
|
|
|
image.draw(in: drawIn)
|
|
|
|
}
|
|
|
|
}
|