diff --git a/Podfile b/Podfile index d7743a7..1478401 100644 --- a/Podfile +++ b/Podfile @@ -7,7 +7,6 @@ use_frameworks! target 'uPic' do pod "libminipng" - pod 'WCDB.swift' end post_install do |installer| diff --git a/Podfile.lock b/Podfile.lock index ba3355f..eab64c6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,27 +1,16 @@ PODS: - libminipng (0.5.6) - - WCDB.swift (2.0.2.5): - - WCDB.swift/no-arc (= 2.0.2.5) - - WCDBOptimizedSQLCipher (~> 1.4.1) - - WCDB.swift/no-arc (2.0.2.5): - - WCDBOptimizedSQLCipher (~> 1.4.1) - - WCDBOptimizedSQLCipher (1.4.1) DEPENDENCIES: - libminipng - - WCDB.swift SPEC REPOS: trunk: - libminipng - - WCDB.swift - - WCDBOptimizedSQLCipher SPEC CHECKSUMS: libminipng: a44c35d06b9d54d6640acdf97f4500c034748abb - WCDB.swift: 41d30d0e113cb2032947e34998f9eedc5c8cbd71 - WCDBOptimizedSQLCipher: 880ec8aa6cda6b27e4c72b30bf0cc01771deda6f -PODFILE CHECKSUM: 578e68ce0a8889942b52298dac45365f59c42985 +PODFILE CHECKSUM: 2f154ab6bb78169cbea229fc0e9e6d544635745d COCOAPODS: 1.14.2 diff --git a/uPic.xcodeproj/project.pbxproj b/uPic.xcodeproj/project.pbxproj index d37e1da..4deef1b 100644 --- a/uPic.xcodeproj/project.pbxproj +++ b/uPic.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 03714B932C19907C00BB4459 /* WCDBSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03714B922C19907C00BB4459 /* WCDBSwift */; }; + 03F383CA2C15A6BC00795FD3 /* URL+sha1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F383C92C15A6BC00795FD3 /* URL+sha1.swift */; }; 1602ED9722ADEFB200AA8638 /* BaseUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1602ED9622ADEFB200AA8638 /* BaseUploader.swift */; }; 1602ED9922ADF43800AA8638 /* SmmsUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1602ED9822ADF43800AA8638 /* SmmsUploader.swift */; }; 16068C7522AEC1D1004D39B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16068C7422AEC1D1004D39B7 /* PreferencesViewController.swift */; }; @@ -205,6 +207,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 03F383C92C15A6BC00795FD3 /* URL+sha1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+sha1.swift"; sourceTree = ""; }; 1602ED9622ADEFB200AA8638 /* BaseUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUploader.swift; sourceTree = ""; }; 1602ED9822ADF43800AA8638 /* SmmsUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmmsUploader.swift; sourceTree = ""; }; 1605DDD8246D993C00262C89 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; @@ -395,6 +398,7 @@ 1649393F285E2620007278DA /* CocoaLumberjackSwift in Frameworks */, 964198B62729A6BA0001AB97 /* SwiftyJSON in Frameworks */, 964198C62729A7F00001AB97 /* Kingfisher in Frameworks */, + 03714B932C19907C00BB4459 /* WCDBSwift in Frameworks */, 964198C42729A7EC0001AB97 /* SwiftyXMLParser in Frameworks */, 964198C02729A7E30001AB97 /* Alamofire in Frameworks */, 16539DCF278AD8700035B9DF /* SotoS3 in Frameworks */, @@ -613,6 +617,7 @@ 4B6C62F923613AEF008E3735 /* NSImage+Extension.swift */, 4B2B3355236C3B6400C9F2CA /* NSView+Extension.swift */, 4B2B3353236BC5AB00C9F2CA /* NSButton+Extension.swift */, + 03F383C92C15A6BC00795FD3 /* URL+sha1.swift */, ); path = Extensions; sourceTree = ""; @@ -924,6 +929,7 @@ 1649393C285E2620007278DA /* CocoaLumberjack */, 1649393E285E2620007278DA /* CocoaLumberjackSwift */, 1649394C285F1C45007278DA /* Zip */, + 03714B922C19907C00BB4459 /* WCDBSwift */, ); productName = uPic; productReference = 16EC9A0222ABBBB4001B6C89 /* uPic.app */; @@ -1025,6 +1031,7 @@ 16539DCD278AD8700035B9DF /* XCRemoteSwiftPackageReference "soto" */, 1649393B285E2620007278DA /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, 1649394B285F1C44007278DA /* XCRemoteSwiftPackageReference "Zip" */, + 03714B912C19907C00BB4459 /* XCRemoteSwiftPackageReference "wcdb" */, ); productRefGroup = 16A6DC4B22AA375700813706; projectDirPath = ""; @@ -1264,6 +1271,7 @@ 16AFDB3824E50F450008E5A7 /* S3Region.swift in Sources */, 96F633F625AEC3C7005514EB /* DatabaseViewController.swift in Sources */, 16CD34FC23BC8966005B52F2 /* MimeType.swift in Sources */, + 03F383CA2C15A6BC00795FD3 /* URL+sha1.swift in Sources */, 16F04EE623BCC45F00CCA2FE /* Option.swift in Sources */, 169F074422AF7A3D008E8525 /* StatusMenuController.swift in Sources */, 167DDBEB22C76F5000B03357 /* GithubUploader.swift in Sources */, @@ -1552,9 +1560,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "uPic/Supporting Files/uPicDebug.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = W863J6W8DZ; ENABLE_HARDENED_RUNTIME = YES; @@ -1565,7 +1574,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.4.2; + MARKETING_VERSION = 1.4.3; PRODUCT_BUNDLE_IDENTIFIER = com.svend.uPic.macos; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1585,9 +1594,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "uPic/Supporting Files/uPic.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = W863J6W8DZ; ENABLE_HARDENED_RUNTIME = YES; @@ -1598,7 +1608,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.4.2; + MARKETING_VERSION = 1.4.3; PRODUCT_BUNDLE_IDENTIFIER = com.svend.uPic.macos; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1758,6 +1768,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 03714B912C19907C00BB4459 /* XCRemoteSwiftPackageReference "wcdb" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Tencent/wcdb"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.1.0; + }; + }; 1649393B285E2620007278DA /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CocoaLumberjack/CocoaLumberjack.git"; @@ -1849,6 +1867,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 03714B922C19907C00BB4459 /* WCDBSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 03714B912C19907C00BB4459 /* XCRemoteSwiftPackageReference "wcdb" */; + productName = WCDBSwift; + }; 1649393C285E2620007278DA /* CocoaLumberjack */ = { isa = XCSwiftPackageProductDependency; package = 1649393B285E2620007278DA /* XCRemoteSwiftPackageReference "CocoaLumberjack" */; diff --git a/uPic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/uPic.xcworkspace/xcshareddata/swiftpm/Package.resolved index aed84da..f1d0177 100644 --- a/uPic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/uPic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "4ef337139bd210e7da4124ce672b7e886fd0a73988e4c7957d7fa3f522103a10", "pins" : [ { "identity" : "alamofire", @@ -99,6 +100,15 @@ "version" : "6.5.0" } }, + { + "identity" : "sqlcipher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Tencent/sqlcipher", + "state" : { + "revision" : "f7e94ce2fad6fc55cfc1180592addd654f140058", + "version" : "1.4.4" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -198,6 +208,15 @@ "version" : "5.6.0" } }, + { + "identity" : "wcdb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Tencent/wcdb", + "state" : { + "revision" : "b30137b3002ae7fcf8a31dae61168e406d41d1a3", + "version" : "2.1.5" + } + }, { "identity" : "zip", "kind" : "remoteSourceControl", diff --git a/uPic/Extensions/Data+Extension.swift b/uPic/Extensions/Data+Extension.swift index fc05ab1..cf34012 100644 --- a/uPic/Extensions/Data+Extension.swift +++ b/uPic/Extensions/Data+Extension.swift @@ -10,47 +10,67 @@ import Cocoa import CryptoSwift extension Data { - + func toBytes() -> Array { return self.bytes } - + func toMd5() -> String { return self.md5().toHexString() } - + func toBase64() -> String { return self.bytes.toBase64() } - + func toSha1() -> String { return self.sha1().toHexString() } - + func toSha224() -> String { return self.sha224().toHexString() } - + func toSha256() -> String { return self.sha256().toHexString() } - + func toSha384() -> String { return self.sha384().toHexString() } - + func toSha512() -> String { return self.sha512().toHexString() } - + func toString() -> String { return String(data: self, encoding: .utf8)! } - + // 转换图片格式 func convertImageData(_ fileType: NSBitmapImageRep.FileType = .png) -> Data? { let bitmap = NSBitmapImageRep(data: self) let data = bitmap?.representation(using: fileType, properties: [:]) return data } + + /// Get github file sha. + func githubSHA() -> String { + let header = "blob \(self.count)\0".data(using: .utf8)! + var store = Data() + store.append(header) + store.append(self) + let sha1 = store.sha1().toHexString() + return sha1 + } + + /// Get github file sha. + func githubSHAAsync() async -> String { + return await withCheckedContinuation { continuation in + DispatchQueue.global().async { + let sha1 = self.githubSHA() + continuation.resume(returning: sha1) + } + } + } } diff --git a/uPic/Extensions/URL+sha1.swift b/uPic/Extensions/URL+sha1.swift new file mode 100644 index 0000000..a811aa3 --- /dev/null +++ b/uPic/Extensions/URL+sha1.swift @@ -0,0 +1,28 @@ +// +// URL+sha1.swift +// uPic +// +// Created by tisfeng on 2024/6/9. +// Copyright © 2024 Svend Jin. All rights reserved. +// + +import Foundation +import CryptoSwift + +extension URL { + /// Get github file sha. + func githubSHA() -> String? { + do { + let fileData = try Data(contentsOf: self) + let header = "blob \(fileData.count)\0".data(using: .utf8)! + var store = Data() + store.append(header) + store.append(fileData) + let sha1 = store.sha1().toHexString() + return sha1 + } catch { + print("Error reading file: \(error)") + return nil + } + } +} diff --git a/uPic/Managers/DBManager.swift b/uPic/Managers/DBManager.swift index 1bdffe5..18653a4 100644 --- a/uPic/Managers/DBManager.swift +++ b/uPic/Managers/DBManager.swift @@ -17,10 +17,10 @@ public class DBManager { private var database: Database! init() { - Database.globalTrace(ofError: { (error) in + Database.globalTraceError { error in assert(error.level != .Fatal) print(error) - }) + } debugPrintOnly("数据库地址:\(Constants.CachePath.databasePath)") database = Database(at: Constants.CachePath.databasePath) createHistoryTable() diff --git a/uPic/Models/Github/GithubUploader.swift b/uPic/Models/Github/GithubUploader.swift index 2677c27..27f6f87 100644 --- a/uPic/Models/Github/GithubUploader.swift +++ b/uPic/Models/Github/GithubUploader.swift @@ -13,25 +13,25 @@ import SwiftyJSON class GithubUploader: BaseUploader { static let shared = GithubUploader() static let fileExtensions: [String] = [] - + func _upload(_ fileUrl: URL?, fileData: Data?, host: Host) { guard let data = host.data else { super.faild(errorMsg: "There is a problem with the map bed configuration, please check!".localized) return } - + super.start() - + let config = data as! GithubHostConfig - + let owner = config.owner let repo = config.repo let branch = config.branch let token = config.token let domain = config.domain - + let saveKeyPath = config.saveKeyPath - + guard let configuration = BaseUploaderUtil.getSaveConfigurationWithB64(fileUrl, fileData, saveKeyPath) else { super.faild(errorMsg: "Invalid file") return @@ -40,49 +40,98 @@ class GithubUploader: BaseUploader { let fileBase64 = configuration["fileBase64"] as! String let fileName = configuration["fileName"] as! String let saveKey = configuration["saveKey"] as! String - - + let url = GithubUtil.getUrl(owner: owner, repo: repo, filePath: saveKey) let parameters = GithubUtil.getRequestParameters(branch: branch, filePath: saveKey, b64Content: fileBase64) - + var headers = HTTPHeaders() headers.add(HTTPHeader.authorization("token \(token)")) headers.add(HTTPHeader.contentType("application/json")) headers.add(HTTPHeader.defaultUserAgent) - + AF.request(url, method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers).validate().uploadProgress { progress in super.progress(percent: progress.fractionCompleted) - }.responseData(completionHandler: { response -> Void in - switch response.result { - case .success(let value): - let json = JSON(value) - if let errorMessage = json["message"].string { - super.faild(responseData: response.data, errorMsg: errorMessage) + }.responseData(completionHandler: { response -> Void in + switch response.result { + case .success(let value): + let json = JSON(value) + if let errorMessage = json["message"].string { + super.faild(responseData: response.data, errorMsg: errorMessage) + return + } + if domain.isEmpty { + super.completed(url: json["content"]["download_url"].stringValue.urlDecoded(), retData, fileUrl, fileName) + } else { + super.completed(url: "\(domain)/\(saveKey)", retData, fileUrl, fileName) + } + case .failure(let error): + var errorMsg = error.localizedDescription + + if let data = response.data { + let json = JSON(data) + errorMsg = json["message"].stringValue + Logger.shared.error("upload image error: \(errorMsg)") + + // If image name is duplicated, we will receive a 422 error and "sha\" wasn't supplied msg. + if !self.isImageNameDuplicatedError(json) { + super.faild(responseData: data, errorMsg: errorMsg) return } - if domain.isEmpty { - super.completed(url: json["content"]["download_url"].stringValue.urlDecoded(), retData, fileUrl, fileName) - } else { - super.completed(url: "\(domain)/\(saveKey)", retData, fileUrl, fileName) - } - case .failure(let error): - var errorMsg = error.localizedDescription - if let data = response.data { - let json = JSON(data) - errorMsg = json["message"].stringValue + + let errMsg = errorMsg + + Task { + do { + async let fileInfo = self.getGitHubFileInfo(url: url, token: token) + async let sha = retData?.githubSHAAsync() ?? "" + let (fileInfoJSON, shaValue) = await (try fileInfo, sha) + + let fileSHA = fileInfoJSON["sha"].string + if shaValue == fileSHA { + Logger.shared.info("image has been uploaded, return download_url") + let url = domain.isEmpty ? fileInfoJSON["download_url"].stringValue : "\(domain)/\(saveKey)" + super.completed(url: url, retData, fileUrl, fileName) + return + } + + Logger.shared.info("image name has been existed, re-load image with random name") + + // If uploading image name has been existed, but sha hash is different, means they are different images, we need to re-upload the image. Use random file name. + self._upload(nil, fileData: retData, host: host) + } catch { + return super.faild(responseData: data, errorMsg: errMsg) + } } - super.faild(responseData: response.data, errorMsg: errorMsg) } - }) - + } + }) } - + func upload(_ fileUrl: URL, host: Host) { self._upload(fileUrl, fileData: nil, host: host) } - + func upload(_ fileData: Data, host: Host) { self._upload(nil, fileData: fileData, host: host) } + + /// Check if error status is 422 and message is "Invalid request.\n\n\"sha\" wasn't supplied.", that means the uploaded image name has been existed. + func isImageNameDuplicatedError(_ json: JSON) -> Bool { + if json["status"].intValue == 422, json["message"].stringValue == "Invalid request.\n\n\"sha\" wasn't supplied." { + return true + } + return false + } + + /// Use AF await to get file JSON by github url GET method. + func getGitHubFileInfo(url: String, token: String) async throws -> JSON { + var headers = HTTPHeaders() + headers.add(HTTPHeader.authorization("token \(token)")) + headers.add(HTTPHeader.contentType("application/json")) + headers.add(HTTPHeader.defaultUserAgent) + + let dataTask = AF.request(url, method: .get, headers: headers).serializingDecodable(JSON.self) + return try await dataTask.value + } } diff --git a/uPic/Models/Github/GithubUtil.swift b/uPic/Models/Github/GithubUtil.swift index 5f70478..5de5a70 100644 --- a/uPic/Models/Github/GithubUtil.swift +++ b/uPic/Models/Github/GithubUtil.swift @@ -13,7 +13,7 @@ class GithubUtil { static func getUrl(owner: String, repo: String, filePath: String) -> String { return "https://api.github.com/repos/\(owner)/\(repo)/contents/\(filePath)".urlEncoded() } - + static func getRequestParameters(branch: String, filePath: String, b64Content: String) -> Parameters { var parameters = Parameters() parameters["branch"] = branch