mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
(mobile): ios - calculate hash using CryptoKit (#5976)
* ios: calculate hash using CryptoKit * chore: remove unused crypto dep --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
ac0cb4a96e
commit
d3af2b1f69
5 changed files with 58 additions and 34 deletions
|
@ -8,6 +8,7 @@
|
||||||
import Flutter
|
import Flutter
|
||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
class BackgroundServicePlugin: NSObject, FlutterPlugin {
|
class BackgroundServicePlugin: NSObject, FlutterPlugin {
|
||||||
|
|
||||||
|
@ -102,12 +103,62 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin {
|
||||||
case "backgroundAppRefreshEnabled":
|
case "backgroundAppRefreshEnabled":
|
||||||
handleBackgroundRefreshStatus(call: call, result: result)
|
handleBackgroundRefreshStatus(call: call, result: result)
|
||||||
break
|
break
|
||||||
|
case "digestFiles":
|
||||||
|
handleDigestFiles(call: call, result: result)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculates the SHA-1 hash of each file from the list of paths provided
|
||||||
|
func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
|
||||||
|
let bufsize = 2 * 1024 * 1024
|
||||||
|
// Private error to throw if file cannot be read
|
||||||
|
enum DigestError: String, LocalizedError {
|
||||||
|
case NoFileHandle = "Cannot Open File Handle"
|
||||||
|
|
||||||
|
public var errorDescription: String? { self.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the arguments or else fail
|
||||||
|
guard let args = call.arguments as? Array<String> else {
|
||||||
|
print("Cannot parse args as array: \(String(describing: call.arguments))")
|
||||||
|
result(FlutterError(code: "Malformed",
|
||||||
|
message: "Received args is not an Array<String>",
|
||||||
|
details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute hash in background thread
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count)
|
||||||
|
for i in (0 ..< args.count) {
|
||||||
|
do {
|
||||||
|
guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle }
|
||||||
|
var hasher = Insecure.SHA1.init();
|
||||||
|
while autoreleasepool(invoking: {
|
||||||
|
let chunk = file.readData(ofLength: bufsize)
|
||||||
|
guard !chunk.isEmpty else { return false } // EOF
|
||||||
|
hasher.update(data: chunk)
|
||||||
|
return true // continue
|
||||||
|
}) { }
|
||||||
|
let digest = hasher.finalize()
|
||||||
|
hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator())))
|
||||||
|
} catch {
|
||||||
|
print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result in main thread
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
result(Array(hashes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Called by the flutter code when enabled so that we can turn on the backround services
|
// Called by the flutter code when enabled so that we can turn on the backround services
|
||||||
// and save the callback information to communicate on this method channel
|
// and save the callback information to communicate on this method channel
|
||||||
public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
|
public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
|
|
|
@ -132,6 +132,7 @@ class BackgroundService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yet to be implemented
|
||||||
Future<Uint8List?> digestFile(String path) {
|
Future<Uint8List?> digestFile(String path) {
|
||||||
return _foregroundChannel.invokeMethod<Uint8List>("digestFile", [path]);
|
return _foregroundChannel.invokeMethod<Uint8List>("digestFile", [path]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
|
@ -119,37 +117,12 @@ class HashService {
|
||||||
/// Hashes the given files and returns a list of the same length
|
/// Hashes the given files and returns a list of the same length
|
||||||
/// files that could not be hashed have a `null` value
|
/// files that could not be hashed have a `null` value
|
||||||
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
||||||
if (Platform.isAndroid) {
|
final List<Uint8List?>? hashes =
|
||||||
final List<Uint8List?>? hashes =
|
await _backgroundService.digestFiles(paths);
|
||||||
await _backgroundService.digestFiles(paths);
|
if (hashes == null) {
|
||||||
if (hashes == null) {
|
throw Exception("Hashing ${paths.length} files failed");
|
||||||
throw Exception("Hashing ${paths.length} files failed");
|
|
||||||
}
|
|
||||||
return hashes;
|
|
||||||
} else if (Platform.isIOS) {
|
|
||||||
final List<Uint8List?> result = List.filled(paths.length, null);
|
|
||||||
for (int i = 0; i < paths.length; i++) {
|
|
||||||
result[i] = await _hashAssetDart(File(paths[i]));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
throw Exception("_hashFiles implementation missing");
|
|
||||||
}
|
}
|
||||||
}
|
return hashes;
|
||||||
|
|
||||||
/// Hashes a single file using Dart's crypto package
|
|
||||||
Future<Uint8List?> _hashAssetDart(File f) async {
|
|
||||||
late Digest output;
|
|
||||||
final sink = sha1.startChunkedConversion(
|
|
||||||
ChunkedConversionSink<Digest>.withCallback((accumulated) {
|
|
||||||
output = accumulated.first;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await for (final chunk in f.openRead()) {
|
|
||||||
sink.add(chunk);
|
|
||||||
}
|
|
||||||
sink.close();
|
|
||||||
return Uint8List.fromList(output.bytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts [AssetEntity]s that were successfully hashed to [Asset]s
|
/// Converts [AssetEntity]s that were successfully hashed to [Asset]s
|
||||||
|
|
|
@ -282,7 +282,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3+4"
|
version: "0.3.3+4"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
|
|
|
@ -54,7 +54,6 @@ dependencies:
|
||||||
permission_handler: ^10.2.0
|
permission_handler: ^10.2.0
|
||||||
device_info_plus: ^8.1.0
|
device_info_plus: ^8.1.0
|
||||||
connectivity_plus: ^4.0.1
|
connectivity_plus: ^4.0.1
|
||||||
crypto: ^3.0.3 # TODO remove once native crypto is used on iOS
|
|
||||||
wakelock_plus: ^1.1.1
|
wakelock_plus: ^1.1.1
|
||||||
flutter_local_notifications: ^15.1.0+1
|
flutter_local_notifications: ^15.1.0+1
|
||||||
timezone: ^0.9.2
|
timezone: ^0.9.2
|
||||||
|
|
Loading…
Reference in a new issue