@bota.dev/react-native-app-sdk 2.0.0-beta.0 → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -2
- package/android/build.gradle +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDiagnostics.kt +53 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +16 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +144 -14
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +150 -29
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +27 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +68 -3
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +33 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +135 -6
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +236 -10
- package/generated/codegen-contract.json +495 -8
- package/ios/BotaDeviceSDK.mm +102 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +126 -4
- package/ios/BotaDeviceSDKAppleDiagnostics.swift +40 -0
- package/ios/BotaDeviceSDKAppleLogs.swift +18 -0
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +138 -6
- package/ios/BotaDeviceSDKAppleRecordings.swift +113 -5
- package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +40 -0
- package/lib/commonjs/BotaClient.js +13 -2
- package/lib/commonjs/BotaClient.js.map +1 -1
- package/lib/commonjs/ble/constants.js +5 -1
- package/lib/commonjs/ble/constants.js.map +1 -1
- package/lib/commonjs/ble/deviceDiagnostics.js +240 -0
- package/lib/commonjs/ble/deviceDiagnostics.js.map +1 -0
- package/lib/commonjs/client.js +163 -28
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +8 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/commonjs/managers/NativeUploadQueue.js +386 -0
- package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +83 -102
- package/lib/commonjs/managers/RecordingManager.js.map +1 -1
- package/lib/commonjs/managers/uploadRecoveryMetadata.js +83 -0
- package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/module/BotaClient.js +13 -2
- package/lib/module/BotaClient.js.map +1 -1
- package/lib/module/ble/constants.js +4 -0
- package/lib/module/ble/constants.js.map +1 -1
- package/lib/module/ble/deviceDiagnostics.js +234 -0
- package/lib/module/ble/deviceDiagnostics.js.map +1 -0
- package/lib/module/client.js +163 -28
- package/lib/module/client.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +8 -0
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/module/managers/NativeUploadQueue.js +380 -0
- package/lib/module/managers/NativeUploadQueue.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +83 -102
- package/lib/module/managers/RecordingManager.js.map +1 -1
- package/lib/module/managers/uploadRecoveryMetadata.js +77 -0
- package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/typescript/BotaClient.d.ts.map +1 -1
- package/lib/typescript/ble/constants.d.ts +4 -0
- package/lib/typescript/ble/constants.d.ts.map +1 -1
- package/lib/typescript/ble/deviceDiagnostics.d.ts +10 -0
- package/lib/typescript/ble/deviceDiagnostics.d.ts.map +1 -0
- package/lib/typescript/client.d.ts +18 -2
- package/lib/typescript/client.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +6 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/managers/DeviceManager.d.ts +3 -1
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/managers/NativeUploadQueue.d.ts +41 -0
- package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -0
- package/lib/typescript/managers/RecordingManager.d.ts +3 -8
- package/lib/typescript/managers/RecordingManager.d.ts.map +1 -1
- package/lib/typescript/managers/types.d.ts +6 -1
- package/lib/typescript/managers/types.d.ts.map +1 -1
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts +6 -0
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -0
- package/lib/typescript/models/Device.d.ts +42 -0
- package/lib/typescript/models/Device.d.ts.map +1 -1
- package/lib/typescript/models/Recording.d.ts +47 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -1
- package/lib/typescript/models/Status.d.ts +5 -1
- package/lib/typescript/models/Status.d.ts.map +1 -1
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +61 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/bota_device_sdk_spm_workaround.rb +26 -15
- package/src/BotaClient.ts +13 -3
- package/src/ble/constants.ts +5 -0
- package/src/ble/deviceDiagnostics.ts +238 -0
- package/src/client.ts +166 -18
- package/src/index.ts +12 -0
- package/src/managers/DeviceManager.ts +11 -0
- package/src/managers/NativeUploadQueue.ts +330 -0
- package/src/managers/RecordingManager.ts +83 -116
- package/src/managers/types.ts +7 -1
- package/src/managers/uploadRecoveryMetadata.ts +59 -0
- package/src/models/Device.ts +43 -0
- package/src/models/Recording.ts +52 -0
- package/src/models/Status.ts +5 -1
- package/src/specs/NativeBotaDeviceSDK.ts +56 -0
package/ios/BotaDeviceSDK.mm
CHANGED
|
@@ -569,6 +569,46 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
|
|
|
569
569
|
}];
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
- (void)listPendingRecordings:(JS::NativeBotaDeviceSDK::NativeConnectedDevice &)device
|
|
573
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
574
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
575
|
+
{
|
|
576
|
+
[[BotaDeviceSDKAppleBridge shared]
|
|
577
|
+
listPendingRecordingsWithID:device.id_()
|
|
578
|
+
serialNumber:device.serialNumber()
|
|
579
|
+
deviceType:device.deviceType()
|
|
580
|
+
firmwareVersion:device.firmwareVersion()
|
|
581
|
+
hardwareRevision:device.hardwareRevision()
|
|
582
|
+
isProvisioned:device.isProvisioned()
|
|
583
|
+
connectionState:device.connectionState()
|
|
584
|
+
mtu:device.mtu()
|
|
585
|
+
completion:^(NSArray<NSDictionary *> *_Nullable values, NSError *_Nullable error) {
|
|
586
|
+
if (error != nil) { BotaRejectEncryptedUploadV2Error(reject); return; }
|
|
587
|
+
resolve(values);
|
|
588
|
+
}];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
- (void)cancelEncryptedRecordingV2:(NSString *)operationId
|
|
592
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
593
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
594
|
+
{
|
|
595
|
+
[[BotaDeviceSDKAppleBridge shared]
|
|
596
|
+
cancelEncryptedRecordingV2WithOperationID:operationId
|
|
597
|
+
completion:^(NSError *_Nullable error) {
|
|
598
|
+
if (error != nil) { BotaRejectEncryptedUploadV2Error(reject); return; }
|
|
599
|
+
resolve(nil);
|
|
600
|
+
}];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
- (void)releaseEncryptedUploadV2Material:(NSString *)materialRegistrationId
|
|
604
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
605
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
606
|
+
{
|
|
607
|
+
[[BotaDeviceSDKAppleBridge shared]
|
|
608
|
+
releaseEncryptedUploadV2MaterialWithRegistrationID:materialRegistrationId
|
|
609
|
+
completion:^{ resolve(nil); }];
|
|
610
|
+
}
|
|
611
|
+
|
|
572
612
|
- (void)syncRecording:(JS::NativeBotaDeviceSDK::NativeConnectedDevice &)device
|
|
573
613
|
recording:(JS::NativeBotaDeviceSDK::NativeDeviceRecording &)recording
|
|
574
614
|
sinkId:(NSString *)sinkId
|
|
@@ -626,6 +666,10 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
|
|
|
626
666
|
generation:recording.generation()
|
|
627
667
|
ciphertextLength:recording.ciphertextLength()
|
|
628
668
|
ciphertextSHA256:recording.ciphertextSha256()
|
|
669
|
+
startedAtMs:recording.startedAtMs()
|
|
670
|
+
durationMs:recording.durationMs()
|
|
671
|
+
plaintextLength:recording.plaintextLength()
|
|
672
|
+
storageFormat:recording.storageFormat().has_value() ? @(recording.storageFormat().value()) : nil
|
|
629
673
|
operationID:operationId
|
|
630
674
|
onProfileRequest:^(NSDictionary *request) {
|
|
631
675
|
[weakSelf emitOnEncryptedUploadV2ProfileRequested:request];
|
|
@@ -807,6 +851,7 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
|
|
|
807
851
|
uploadRecordingFileWithTaskID:request.taskId()
|
|
808
852
|
recordingID:request.recordingId()
|
|
809
853
|
localPath:request.localPath()
|
|
854
|
+
fileSizeBytes:request.fileSizeBytes().has_value() ? @(request.fileSizeBytes().value()) : nil
|
|
810
855
|
uploadURL:request.uploadUrl()
|
|
811
856
|
uploadToken:request.uploadToken()
|
|
812
857
|
completeURL:request.completeUrl()
|
|
@@ -826,6 +871,23 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
|
|
|
826
871
|
}];
|
|
827
872
|
}
|
|
828
873
|
|
|
874
|
+
- (void)releaseRecordingFile:(NSString *)taskId
|
|
875
|
+
localPath:(NSString *)localPath
|
|
876
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
877
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
878
|
+
{
|
|
879
|
+
[[BotaDeviceSDKAppleBridge shared]
|
|
880
|
+
releaseRecordingFileWithTaskID:taskId
|
|
881
|
+
localPath:localPath
|
|
882
|
+
completion:^(NSError *error) {
|
|
883
|
+
if (error != nil) {
|
|
884
|
+
BotaRejectAppleError(error, reject);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
resolve(nil);
|
|
888
|
+
}];
|
|
889
|
+
}
|
|
890
|
+
|
|
829
891
|
- (void)cancelRecordingUpload:(NSString *)taskId
|
|
830
892
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
831
893
|
reject:(__unused RCTPromiseRejectBlock)reject
|
|
@@ -966,6 +1028,46 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
|
|
|
966
1028
|
}];
|
|
967
1029
|
}
|
|
968
1030
|
|
|
1031
|
+
- (void)readDiagnosticEvents:(JS::NativeBotaDeviceSDK::NativeConnectedDevice &)device
|
|
1032
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1033
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
1034
|
+
{
|
|
1035
|
+
[[BotaDeviceSDKAppleBridge shared]
|
|
1036
|
+
readDiagnosticEventsWithID:device.id_()
|
|
1037
|
+
serialNumber:device.serialNumber()
|
|
1038
|
+
deviceType:device.deviceType()
|
|
1039
|
+
firmwareVersion:device.firmwareVersion()
|
|
1040
|
+
hardwareRevision:device.hardwareRevision()
|
|
1041
|
+
isProvisioned:device.isProvisioned()
|
|
1042
|
+
connectionState:device.connectionState()
|
|
1043
|
+
mtu:device.mtu()
|
|
1044
|
+
completion:^(NSDictionary *_Nullable batch, NSError *_Nullable error) {
|
|
1045
|
+
if (error != nil) { BotaRejectAppleError(error, reject); return; }
|
|
1046
|
+
resolve(batch);
|
|
1047
|
+
}];
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
- (void)acknowledgeDiagnosticEvents:(JS::NativeBotaDeviceSDK::NativeConnectedDevice &)device
|
|
1051
|
+
acceptedEventIds:(NSArray *)acceptedEventIds
|
|
1052
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1053
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
1054
|
+
{
|
|
1055
|
+
[[BotaDeviceSDKAppleBridge shared]
|
|
1056
|
+
acknowledgeDiagnosticEventsWithID:device.id_()
|
|
1057
|
+
serialNumber:device.serialNumber()
|
|
1058
|
+
deviceType:device.deviceType()
|
|
1059
|
+
firmwareVersion:device.firmwareVersion()
|
|
1060
|
+
hardwareRevision:device.hardwareRevision()
|
|
1061
|
+
isProvisioned:device.isProvisioned()
|
|
1062
|
+
connectionState:device.connectionState()
|
|
1063
|
+
mtu:device.mtu()
|
|
1064
|
+
acceptedEventIds:acceptedEventIds
|
|
1065
|
+
completion:^(NSError *_Nullable error) {
|
|
1066
|
+
if (error != nil) { BotaRejectAppleError(error, reject); return; }
|
|
1067
|
+
resolve(nil);
|
|
1068
|
+
}];
|
|
1069
|
+
}
|
|
1070
|
+
|
|
969
1071
|
- (void)stopDeviceLogs:(RCTPromiseResolveBlock)resolve
|
|
970
1072
|
reject:(__unused RCTPromiseRejectBlock)reject
|
|
971
1073
|
{
|
|
@@ -801,6 +801,48 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
801
801
|
}
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
+
@objc(listPendingRecordingsWithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:completion:)
|
|
805
|
+
public func listPendingRecordings(
|
|
806
|
+
id: String, serialNumber: String, deviceType: String, firmwareVersion: String,
|
|
807
|
+
hardwareRevision: String?, isProvisioned: Bool, connectionState: String, mtu: Double,
|
|
808
|
+
completion: @escaping @Sendable ([[String: Any]]?, NSError?) -> Void
|
|
809
|
+
) {
|
|
810
|
+
Task {
|
|
811
|
+
do {
|
|
812
|
+
let values = try await recordings.listPendingRecordings(Self.connectedDevice(
|
|
813
|
+
id: id, serialNumber: serialNumber, deviceType: deviceType,
|
|
814
|
+
firmwareVersion: firmwareVersion, hardwareRevision: hardwareRevision,
|
|
815
|
+
isProvisioned: isProvisioned, connectionState: connectionState, mtu: mtu
|
|
816
|
+
))
|
|
817
|
+
completion(values.map { value in
|
|
818
|
+
switch value {
|
|
819
|
+
case let .legacy(recording):
|
|
820
|
+
["profile": "legacy", "legacy": Self.recording(recording)]
|
|
821
|
+
case let .encryptedV2(recording):
|
|
822
|
+
["profile": "encrypted_upload_v2", "encrypted": BotaDeviceSDKAppleRecordings.encryptedUploadV2Recording(recording)]
|
|
823
|
+
}
|
|
824
|
+
}, nil)
|
|
825
|
+
} catch { completion(nil, error as NSError) }
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@objc(cancelEncryptedRecordingV2WithOperationID:completion:)
|
|
830
|
+
public func cancelEncryptedRecordingV2(
|
|
831
|
+
operationID: String, completion: @escaping @Sendable (NSError?) -> Void
|
|
832
|
+
) {
|
|
833
|
+
Task {
|
|
834
|
+
do { try await recordings.cancelEncryptedRecordingV2(operationID); completion(nil) }
|
|
835
|
+
catch { completion(error as NSError) }
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
@objc(releaseEncryptedUploadV2MaterialWithRegistrationID:completion:)
|
|
840
|
+
public func releaseEncryptedUploadV2Material(
|
|
841
|
+
registrationID: String, completion: @escaping @Sendable () -> Void
|
|
842
|
+
) {
|
|
843
|
+
Task { await BotaDeviceSDKEncryptedUploadV2Materials.release(registrationID); completion() }
|
|
844
|
+
}
|
|
845
|
+
|
|
804
846
|
@objc(syncRecordingWithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:startedAtMilliseconds:durationMilliseconds:fileSize:codec:isEncrypted:sinkID:onProgress:completion:)
|
|
805
847
|
public func syncRecording(
|
|
806
848
|
id: String,
|
|
@@ -855,6 +897,7 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
855
897
|
var value: [String: Any] = [
|
|
856
898
|
"localPath": result.localPath,
|
|
857
899
|
"e2eEncrypted": result.isE2EEncrypted,
|
|
900
|
+
"fileSizeBytes": result.fileSizeBytes,
|
|
858
901
|
]
|
|
859
902
|
if let contentSHA256Hex = result.contentSHA256Hex {
|
|
860
903
|
value["contentSha256"] = contentSHA256Hex
|
|
@@ -866,7 +909,7 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
866
909
|
}
|
|
867
910
|
}
|
|
868
911
|
|
|
869
|
-
@objc(syncEncryptedRecordingV2WithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:generation:ciphertextLength:ciphertextSHA256:operationID:onProfileRequest:onProgress:completion:)
|
|
912
|
+
@objc(syncEncryptedRecordingV2WithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:generation:ciphertextLength:ciphertextSHA256:startedAtMs:durationMs:plaintextLength:storageFormat:operationID:onProfileRequest:onProgress:completion:)
|
|
870
913
|
public func syncEncryptedRecordingV2(
|
|
871
914
|
id: String,
|
|
872
915
|
serialNumber: String,
|
|
@@ -880,6 +923,10 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
880
923
|
generation: Double,
|
|
881
924
|
ciphertextLength: String,
|
|
882
925
|
ciphertextSHA256: String,
|
|
926
|
+
startedAtMs: String?,
|
|
927
|
+
durationMs: String?,
|
|
928
|
+
plaintextLength: String?,
|
|
929
|
+
storageFormat: NSNumber?,
|
|
883
930
|
operationID: String,
|
|
884
931
|
onProfileRequest: @escaping @Sendable ([String: Any]) -> Void,
|
|
885
932
|
onProgress: @escaping @Sendable ([String: Any]) -> Void,
|
|
@@ -895,6 +942,9 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
895
942
|
throw BotaDeviceSDKAppleBridgeInputError.invalidUnsignedInteger
|
|
896
943
|
}
|
|
897
944
|
let digest = try Self.sha256Data(ciphertextSHA256)
|
|
945
|
+
guard let format = UInt8(exactly: try Self.unsignedInteger(storageFormat?.doubleValue ?? 3)) else {
|
|
946
|
+
throw BotaDeviceSDKAppleBridgeInputError.invalidUnsignedInteger
|
|
947
|
+
}
|
|
898
948
|
try await recordings.syncEncryptedRecordingV2(
|
|
899
949
|
Self.connectedDevice(
|
|
900
950
|
id: id,
|
|
@@ -910,7 +960,11 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
910
960
|
uuid: recordingUUID,
|
|
911
961
|
generation: generationValue,
|
|
912
962
|
ciphertextLength: length,
|
|
913
|
-
ciphertextSHA256: digest
|
|
963
|
+
ciphertextSHA256: digest,
|
|
964
|
+
startedAtMs: try Self.catalogDecimal(startedAtMs),
|
|
965
|
+
durationMs: try Self.catalogDecimal(durationMs),
|
|
966
|
+
plaintextLength: try Self.catalogDecimal(plaintextLength),
|
|
967
|
+
storageFormat: format
|
|
914
968
|
),
|
|
915
969
|
operationID: operationID,
|
|
916
970
|
onProfileRequest: onProfileRequest,
|
|
@@ -923,6 +977,14 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
923
977
|
}
|
|
924
978
|
}
|
|
925
979
|
|
|
980
|
+
private static func catalogDecimal(_ text: String?) throws -> UInt64 {
|
|
981
|
+
guard let text else { return 0 }
|
|
982
|
+
guard let value = UInt64(text), String(value) == text else {
|
|
983
|
+
throw BotaDeviceSDKAppleBridgeInputError.invalidUnsignedInteger
|
|
984
|
+
}
|
|
985
|
+
return value
|
|
986
|
+
}
|
|
987
|
+
|
|
926
988
|
@objc(resolveEncryptedUploadV2ProfileWithRequestID:profile:uploadSessionID:ownerRevision:securityPolicy:materialRegistrationID:completion:)
|
|
927
989
|
public func resolveEncryptedUploadV2Profile(
|
|
928
990
|
requestID: String,
|
|
@@ -1126,11 +1188,12 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
1126
1188
|
}
|
|
1127
1189
|
}
|
|
1128
1190
|
|
|
1129
|
-
@objc(uploadRecordingFileWithTaskID:recordingID:localPath:uploadURL:uploadToken:completeURL:contentType:contentSHA256:relayURL:relayBearerToken:onProgress:completion:)
|
|
1191
|
+
@objc(uploadRecordingFileWithTaskID:recordingID:localPath:fileSizeBytes:uploadURL:uploadToken:completeURL:contentType:contentSHA256:relayURL:relayBearerToken:onProgress:completion:)
|
|
1130
1192
|
public func uploadRecordingFile(
|
|
1131
1193
|
taskID: String,
|
|
1132
1194
|
recordingID: String,
|
|
1133
1195
|
localPath: String,
|
|
1196
|
+
fileSizeBytes: NSNumber?,
|
|
1134
1197
|
uploadURL: String,
|
|
1135
1198
|
uploadToken: String?,
|
|
1136
1199
|
completeURL: String?,
|
|
@@ -1143,6 +1206,11 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
1143
1206
|
) {
|
|
1144
1207
|
Task {
|
|
1145
1208
|
do {
|
|
1209
|
+
let expectedSize = try fileSizeBytes.map { value -> Int64 in
|
|
1210
|
+
let integer = try Self.unsignedInteger(value.doubleValue)
|
|
1211
|
+
guard integer <= UInt64(Int64.max) else { throw CocoaError(.fileReadCorruptFile) }
|
|
1212
|
+
return Int64(integer)
|
|
1213
|
+
}
|
|
1146
1214
|
try await recordingUploads.upload(.init(
|
|
1147
1215
|
taskID: taskID,
|
|
1148
1216
|
recordingID: recordingID,
|
|
@@ -1153,7 +1221,8 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
1153
1221
|
contentType: contentType,
|
|
1154
1222
|
contentSHA256: contentSHA256,
|
|
1155
1223
|
relayURL: relayURL,
|
|
1156
|
-
relayBearerToken: relayBearerToken
|
|
1224
|
+
relayBearerToken: relayBearerToken,
|
|
1225
|
+
fileSizeBytes: expectedSize
|
|
1157
1226
|
)) { progress in
|
|
1158
1227
|
onProgress([
|
|
1159
1228
|
"taskId": progress.taskID,
|
|
@@ -1168,6 +1237,22 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
1168
1237
|
}
|
|
1169
1238
|
}
|
|
1170
1239
|
|
|
1240
|
+
@objc(releaseRecordingFileWithTaskID:localPath:completion:)
|
|
1241
|
+
public func releaseRecordingFile(
|
|
1242
|
+
taskID: String,
|
|
1243
|
+
localPath: String,
|
|
1244
|
+
completion: @escaping @Sendable (NSError?) -> Void
|
|
1245
|
+
) {
|
|
1246
|
+
Task {
|
|
1247
|
+
do {
|
|
1248
|
+
try await recordingUploads.release(taskID: taskID, localPath: localPath)
|
|
1249
|
+
completion(nil)
|
|
1250
|
+
} catch {
|
|
1251
|
+
completion(error as NSError)
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1171
1256
|
@objc(cancelRecordingUploadWithTaskID:completion:)
|
|
1172
1257
|
public func cancelRecordingUpload(
|
|
1173
1258
|
taskID: String,
|
|
@@ -1359,6 +1444,43 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
1359
1444
|
}
|
|
1360
1445
|
}
|
|
1361
1446
|
|
|
1447
|
+
@objc(readDiagnosticEventsWithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:completion:)
|
|
1448
|
+
public func readDiagnosticEvents(
|
|
1449
|
+
id: String, serialNumber: String, deviceType: String, firmwareVersion: String,
|
|
1450
|
+
hardwareRevision: String?, isProvisioned: Bool, connectionState: String, mtu: Double,
|
|
1451
|
+
completion: @escaping @Sendable ([String: Any]?, NSError?) -> Void
|
|
1452
|
+
) {
|
|
1453
|
+
Task {
|
|
1454
|
+
do {
|
|
1455
|
+
let device = try Self.connectedDevice(
|
|
1456
|
+
id: id, serialNumber: serialNumber, deviceType: deviceType,
|
|
1457
|
+
firmwareVersion: firmwareVersion, hardwareRevision: hardwareRevision,
|
|
1458
|
+
isProvisioned: isProvisioned, connectionState: connectionState, mtu: mtu
|
|
1459
|
+
)
|
|
1460
|
+
completion(diagnosticBatchPayload(try await logs.readDiagnosticEvents(device)), nil)
|
|
1461
|
+
} catch { completion(nil, error as NSError) }
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
@objc(acknowledgeDiagnosticEventsWithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:acceptedEventIds:completion:)
|
|
1466
|
+
public func acknowledgeDiagnosticEvents(
|
|
1467
|
+
id: String, serialNumber: String, deviceType: String, firmwareVersion: String,
|
|
1468
|
+
hardwareRevision: String?, isProvisioned: Bool, connectionState: String, mtu: Double,
|
|
1469
|
+
acceptedEventIds: [String], completion: @escaping @Sendable (NSError?) -> Void
|
|
1470
|
+
) {
|
|
1471
|
+
Task {
|
|
1472
|
+
do {
|
|
1473
|
+
let device = try Self.connectedDevice(
|
|
1474
|
+
id: id, serialNumber: serialNumber, deviceType: deviceType,
|
|
1475
|
+
firmwareVersion: firmwareVersion, hardwareRevision: hardwareRevision,
|
|
1476
|
+
isProvisioned: isProvisioned, connectionState: connectionState, mtu: mtu
|
|
1477
|
+
)
|
|
1478
|
+
try await logs.acknowledgeDiagnosticEvents(device, acceptedEventIds: acceptedEventIds)
|
|
1479
|
+
completion(nil)
|
|
1480
|
+
} catch { completion(error as NSError) }
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1362
1484
|
@objc(stopDeviceLogsWithCompletion:)
|
|
1363
1485
|
public func stopDeviceLogs(completion: @escaping @Sendable () -> Void) {
|
|
1364
1486
|
Task {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import BotaAppSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
func diagnosticBatchPayload(_ batch: DeviceDiagnosticsBatch) -> [String: Any] {
|
|
5
|
+
["schema_version": batch.schemaVersion, "events": batch.events.map(diagnosticEventPayload)]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
private func diagnosticEventPayload(_ event: DeviceDiagnosticEvent) -> [String: Any] {
|
|
9
|
+
var payload: [String: Any] = [
|
|
10
|
+
"event_id": event.eventId, "event_type": event.eventType,
|
|
11
|
+
"reason_code": event.reasonCode, "uptime_ms": event.uptimeMs,
|
|
12
|
+
"signature": event.signature, "firmware_build_id": event.firmwareBuildId,
|
|
13
|
+
"subsystem": event.subsystem, "state_before_event": event.stateBeforeEvent,
|
|
14
|
+
]
|
|
15
|
+
if let report = event.report {
|
|
16
|
+
var execution: [String: Any] = ["pc_trace": report.execution.pcTrace]
|
|
17
|
+
execution["task"] = report.execution.task
|
|
18
|
+
execution["reti"] = report.execution.reti
|
|
19
|
+
execution["rets"] = report.execution.rets
|
|
20
|
+
var value: [String: Any] = [
|
|
21
|
+
"fault": [
|
|
22
|
+
"cpu_id": report.fault.cpuId, "cpu_emu": report.fault.cpuEmu,
|
|
23
|
+
"core_emu": report.fault.coreEmu, "hsb_emu": report.fault.hsbEmu,
|
|
24
|
+
"audio_emu": report.fault.audioEmu, "wireless_emu": report.fault.wirelessEmu,
|
|
25
|
+
],
|
|
26
|
+
"execution": execution,
|
|
27
|
+
]
|
|
28
|
+
if let runtime = report.runtime {
|
|
29
|
+
var fields: [String: Any] = [:]
|
|
30
|
+
fields["heap_free_bytes"] = runtime.heapFreeBytes
|
|
31
|
+
fields["task_stack_remaining_bytes"] = runtime.taskStackRemainingBytes
|
|
32
|
+
value["runtime"] = fields
|
|
33
|
+
}
|
|
34
|
+
if let breadcrumbs = report.breadcrumbs {
|
|
35
|
+
value["breadcrumbs"] = breadcrumbs.map { ["delta_ms": $0.deltaMs, "code": $0.code, "arg0": $0.arg0] as [String: Any] }
|
|
36
|
+
}
|
|
37
|
+
payload["report"] = value
|
|
38
|
+
}
|
|
39
|
+
return payload
|
|
40
|
+
}
|
|
@@ -2,6 +2,8 @@ import BotaAppSDK
|
|
|
2
2
|
import Foundation
|
|
3
3
|
|
|
4
4
|
protocol BotaDeviceSDKAppleLogClient: Sendable {
|
|
5
|
+
func readDiagnosticEvents(_ device: ConnectedDevice) async throws -> DeviceDiagnosticsBatch
|
|
6
|
+
func acknowledgeDiagnosticEvents(_ device: ConnectedDevice, acceptedEventIds: [String]) async throws
|
|
5
7
|
func streamLogs(
|
|
6
8
|
_ device: ConnectedDevice
|
|
7
9
|
) async throws -> AsyncThrowingStream<DeviceLogLine, Error>
|
|
@@ -24,6 +26,14 @@ struct BotaDeviceSDKSharedAppleLogClient: BotaDeviceSDKAppleLogClient {
|
|
|
24
26
|
func stop() async throws {
|
|
25
27
|
try await logs.stop()
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
func readDiagnosticEvents(_ device: ConnectedDevice) async throws -> DeviceDiagnosticsBatch {
|
|
31
|
+
try await logs.readDiagnosticEvents(device)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func acknowledgeDiagnosticEvents(_ device: ConnectedDevice, acceptedEventIds: [String]) async throws {
|
|
35
|
+
try await logs.acknowledgeDiagnosticEvents(device, acceptedEventIds: acceptedEventIds)
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
actor BotaDeviceSDKAppleLogs {
|
|
@@ -39,6 +49,14 @@ actor BotaDeviceSDKAppleLogs {
|
|
|
39
49
|
self.client = client
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
func readDiagnosticEvents(_ device: ConnectedDevice) async throws -> DeviceDiagnosticsBatch {
|
|
53
|
+
try await client.readDiagnosticEvents(device)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func acknowledgeDiagnosticEvents(_ device: ConnectedDevice, acceptedEventIds: [String]) async throws {
|
|
57
|
+
try await client.acknowledgeDiagnosticEvents(device, acceptedEventIds: acceptedEventIds)
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
func start(
|
|
43
61
|
_ device: ConnectedDevice,
|
|
44
62
|
onLine: @escaping @Sendable (DeviceLogLine) -> Void,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import Darwin
|
|
2
3
|
|
|
3
4
|
struct BotaDeviceSDKAppleRecordingUploadRequest: Sendable {
|
|
4
5
|
let taskID: String
|
|
@@ -11,6 +12,7 @@ struct BotaDeviceSDKAppleRecordingUploadRequest: Sendable {
|
|
|
11
12
|
let contentSHA256: String?
|
|
12
13
|
let relayURL: String?
|
|
13
14
|
let relayBearerToken: String?
|
|
15
|
+
var fileSizeBytes: Int64? = nil
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
struct BotaDeviceSDKAppleRecordingUploadProgress: Equatable, Sendable {
|
|
@@ -25,6 +27,9 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
25
27
|
case invalidResponse
|
|
26
28
|
case missingFile(String)
|
|
27
29
|
case requestFailed(Int)
|
|
30
|
+
case invalidJournal
|
|
31
|
+
case notCompleted
|
|
32
|
+
case fileSizeChanged
|
|
28
33
|
|
|
29
34
|
var errorDescription: String? {
|
|
30
35
|
switch self {
|
|
@@ -36,24 +41,42 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
36
41
|
"recording file does not exist: \(path)"
|
|
37
42
|
case let .requestFailed(statusCode):
|
|
38
43
|
"recording upload failed with HTTP \(statusCode)"
|
|
44
|
+
case .invalidJournal:
|
|
45
|
+
"invalid upload recovery journal"
|
|
46
|
+
case .notCompleted:
|
|
47
|
+
"recording release requires exact durable completion"
|
|
48
|
+
case .fileSizeChanged:
|
|
49
|
+
"stored recording length changed"
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
private var queueFile: URL
|
|
55
|
+
private var recordingDirectory: URL
|
|
44
56
|
private let session: URLSession
|
|
57
|
+
private let syncDirectory: @Sendable (URL) throws -> Void
|
|
58
|
+
private var journalWriteUncertain = false
|
|
45
59
|
private var operations: [String: Task<Void, Error>] = [:]
|
|
60
|
+
private var cancelledAttempts = Set<String>()
|
|
46
61
|
|
|
47
62
|
init(
|
|
48
63
|
queueFile: URL = BotaDeviceSDKAppleRecordingUploads.defaultQueueFile(),
|
|
49
|
-
session: URLSession = .shared
|
|
64
|
+
session: URLSession = .shared,
|
|
65
|
+
syncDirectory: @escaping @Sendable (URL) throws -> Void = BotaDeviceSDKAppleRecordingUploads.synchronizeDirectory
|
|
50
66
|
) {
|
|
51
67
|
self.queueFile = queueFile
|
|
68
|
+
recordingDirectory = queueFile.deletingLastPathComponent().appendingPathComponent("Recordings", isDirectory: true)
|
|
52
69
|
self.session = session
|
|
70
|
+
self.syncDirectory = syncDirectory
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
func configure(applicationSupportDirectory: URL?) {
|
|
56
|
-
guard let applicationSupportDirectory else {
|
|
74
|
+
guard let applicationSupportDirectory else {
|
|
75
|
+
queueFile = Self.defaultQueueFile()
|
|
76
|
+
recordingDirectory = queueFile.deletingLastPathComponent().appendingPathComponent("Recordings", isDirectory: true)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
recordingDirectory = applicationSupportDirectory.appendingPathComponent("Recordings", isDirectory: true)
|
|
57
80
|
queueFile = applicationSupportDirectory
|
|
58
81
|
.appendingPathComponent("BotaDeviceSDK", isDirectory: true)
|
|
59
82
|
.appendingPathComponent("compat-upload-queue.json")
|
|
@@ -63,6 +86,8 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
63
86
|
_ upload: BotaDeviceSDKAppleRecordingUploadRequest,
|
|
64
87
|
onProgress: @escaping @Sendable (BotaDeviceSDKAppleRecordingUploadProgress) -> Void
|
|
65
88
|
) async throws {
|
|
89
|
+
guard !cancelledAttempts.contains(upload.taskID) else { throw CancellationError() }
|
|
90
|
+
try requireOwnedFile(upload.localPath)
|
|
66
91
|
guard operations[upload.taskID] == nil else {
|
|
67
92
|
throw UploadError.alreadyActive(upload.taskID)
|
|
68
93
|
}
|
|
@@ -72,16 +97,21 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
72
97
|
}
|
|
73
98
|
operations[upload.taskID] = operation
|
|
74
99
|
defer { operations.removeValue(forKey: upload.taskID) }
|
|
75
|
-
try await
|
|
100
|
+
try await withTaskCancellationHandler {
|
|
101
|
+
try await operation.value
|
|
102
|
+
} onCancel: {
|
|
103
|
+
operation.cancel()
|
|
104
|
+
}
|
|
76
105
|
}
|
|
77
106
|
|
|
78
107
|
func cancel(taskID: String) {
|
|
79
|
-
|
|
108
|
+
cancelledAttempts.insert(taskID)
|
|
109
|
+
operations[taskID]?.cancel()
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
func cancelAll() {
|
|
113
|
+
cancelledAttempts.formUnion(operations.keys)
|
|
83
114
|
operations.values.forEach { $0.cancel() }
|
|
84
|
-
operations.removeAll()
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
func loadQueue() throws -> String {
|
|
@@ -90,11 +120,103 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
90
120
|
}
|
|
91
121
|
|
|
92
122
|
func saveQueue(_ serializedTasks: String) throws {
|
|
123
|
+
try writeQueue(Self.serialize(Self.metadata(serializedTasks)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func release(taskID: String, localPath: String) async throws {
|
|
127
|
+
guard !journalWriteUncertain else { throw UploadError.notCompleted }
|
|
128
|
+
let tasks = try Self.metadata(String(contentsOf: queueFile, encoding: .utf8))
|
|
129
|
+
guard tasks.contains(where: {
|
|
130
|
+
$0["id"] as? String == taskID && $0["localPath"] as? String == localPath &&
|
|
131
|
+
$0["status"] as? String == "completed"
|
|
132
|
+
}) else { throw UploadError.notCompleted }
|
|
133
|
+
try removeCompletedFile(localPath)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func removeCompletedFile(_ path: String) throws {
|
|
137
|
+
try requireOwnedFile(path)
|
|
138
|
+
if FileManager.default.fileExists(atPath: path) {
|
|
139
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
|
140
|
+
guard attributes[.type] as? FileAttributeType == .typeRegular else { throw UploadError.notCompleted }
|
|
141
|
+
try FileManager.default.removeItem(atPath: path)
|
|
142
|
+
}
|
|
143
|
+
// A missing file may be an earlier unlink whose directory sync failed.
|
|
144
|
+
try syncDirectory(URL(fileURLWithPath: path).deletingLastPathComponent())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func requireOwnedFile(_ path: String) throws {
|
|
148
|
+
let file = URL(fileURLWithPath: path)
|
|
149
|
+
guard path.hasPrefix("/"), file.pathExtension == "recording",
|
|
150
|
+
UUID(uuidString: file.deletingPathExtension().lastPathComponent) != nil,
|
|
151
|
+
file.resolvingSymlinksInPath().deletingLastPathComponent().standardizedFileURL == recordingDirectory.resolvingSymlinksInPath().standardizedFileURL
|
|
152
|
+
else { throw UploadError.notCompleted }
|
|
153
|
+
if FileManager.default.fileExists(atPath: path) {
|
|
154
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
|
155
|
+
guard attributes[.type] as? FileAttributeType == .typeRegular else { throw UploadError.notCompleted }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private func writeQueue(_ serializedTasks: String) throws {
|
|
160
|
+
journalWriteUncertain = true
|
|
93
161
|
try FileManager.default.createDirectory(
|
|
94
162
|
at: queueFile.deletingLastPathComponent(),
|
|
95
163
|
withIntermediateDirectories: true
|
|
96
164
|
)
|
|
165
|
+
try syncDirectory(queueFile.deletingLastPathComponent().deletingLastPathComponent())
|
|
97
166
|
try Data(serializedTasks.utf8).write(to: queueFile, options: .atomic)
|
|
167
|
+
let handle = try FileHandle(forWritingTo: queueFile)
|
|
168
|
+
defer { try? handle.close() }
|
|
169
|
+
try handle.synchronize()
|
|
170
|
+
try syncDirectory(queueFile.deletingLastPathComponent())
|
|
171
|
+
journalWriteUncertain = false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private static func serialize(_ tasks: [[String: Any]]) throws -> String {
|
|
175
|
+
String(decoding: try JSONSerialization.data(withJSONObject: tasks, options: [.sortedKeys]), as: UTF8.self)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private static func metadata(_ serialized: String) throws -> [[String: Any]] {
|
|
179
|
+
guard let tasks = try JSONSerialization.jsonObject(with: Data(serialized.utf8)) as? [[String: Any]] else {
|
|
180
|
+
throw UploadError.invalidJournal
|
|
181
|
+
}
|
|
182
|
+
let fields: Set<String> = ["id", "recordingId", "deviceId", "recordingUuid", "localPath", "status", "retryCount", "createdAt", "updatedAt", "recoveryScope", "fileSizeBytes", "nextAttemptAt", "contentType", "contentSha256", "relayUpload"]
|
|
183
|
+
var ids = Set<String>()
|
|
184
|
+
return try tasks.map { task in
|
|
185
|
+
guard let id = task["id"] as? String, !id.isEmpty, ids.insert(id).inserted,
|
|
186
|
+
let recordingID = task["recordingId"] as? String, !recordingID.isEmpty,
|
|
187
|
+
let deviceID = task["deviceId"] as? String, !deviceID.isEmpty,
|
|
188
|
+
task["localPath"] is String, task["createdAt"] is String, task["updatedAt"] is String,
|
|
189
|
+
let status = task["status"] as? String, ["pending", "uploading", "completed", "failed"].contains(status),
|
|
190
|
+
validInteger(task["retryCount"])
|
|
191
|
+
else { throw UploadError.invalidJournal }
|
|
192
|
+
for key in ["recordingUuid", "recoveryScope", "contentType", "contentSha256"] where task[key] != nil {
|
|
193
|
+
guard task[key] is String else { throw UploadError.invalidJournal }
|
|
194
|
+
}
|
|
195
|
+
for key in ["fileSizeBytes", "nextAttemptAt"] where task[key] != nil {
|
|
196
|
+
guard validInteger(task[key]) else { throw UploadError.invalidJournal }
|
|
197
|
+
}
|
|
198
|
+
if let relay = task["relayUpload"] {
|
|
199
|
+
guard let number = relay as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() else { throw UploadError.invalidJournal }
|
|
200
|
+
}
|
|
201
|
+
let dates = ISO8601DateFormatter()
|
|
202
|
+
let fractionalDates = ISO8601DateFormatter()
|
|
203
|
+
fractionalDates.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
204
|
+
for key in ["createdAt", "updatedAt"] {
|
|
205
|
+
guard let value = task[key] as? String,
|
|
206
|
+
dates.date(from: value) != nil || fractionalDates.date(from: value) != nil
|
|
207
|
+
else { throw UploadError.invalidJournal }
|
|
208
|
+
}
|
|
209
|
+
var clean = task.filter { fields.contains($0.key) }
|
|
210
|
+
guard clean.values.allSatisfy({ $0 is String || $0 is NSNumber }) else { throw UploadError.invalidJournal }
|
|
211
|
+
clean["relayUpload"] = task["relayUpload"] as? Bool ?? (task["relay"] is [String: Any])
|
|
212
|
+
return clean
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private static func validInteger(_ value: Any?) -> Bool {
|
|
217
|
+
guard let number = value as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID() else { return false }
|
|
218
|
+
let value = number.doubleValue
|
|
219
|
+
return value.isFinite && value >= 0 && value <= 9_007_199_254_740_991 && value.rounded() == value
|
|
98
220
|
}
|
|
99
221
|
|
|
100
222
|
private static func performUpload(
|
|
@@ -108,6 +230,8 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
108
230
|
}
|
|
109
231
|
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
|
|
110
232
|
let totalBytes = (attributes[.size] as? NSNumber)?.int64Value ?? 0
|
|
233
|
+
if let expected = upload.fileSizeBytes, expected != totalBytes { throw UploadError.fileSizeChanged }
|
|
234
|
+
try Task.checkCancellation()
|
|
111
235
|
onProgress(.init(taskID: upload.taskID, completedBytes: 0, totalBytes: totalBytes))
|
|
112
236
|
|
|
113
237
|
let relay = upload.relayURL != nil
|
|
@@ -123,6 +247,7 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
123
247
|
}
|
|
124
248
|
let (_, response) = try await session.upload(for: request, fromFile: fileURL)
|
|
125
249
|
try Self.requireSuccess(response)
|
|
250
|
+
try Task.checkCancellation()
|
|
126
251
|
|
|
127
252
|
if !relay,
|
|
128
253
|
let completeURL = upload.completeURL,
|
|
@@ -144,12 +269,12 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
144
269
|
try requireSuccess(completeResponse)
|
|
145
270
|
}
|
|
146
271
|
|
|
272
|
+
try Task.checkCancellation()
|
|
147
273
|
onProgress(.init(
|
|
148
274
|
taskID: upload.taskID,
|
|
149
275
|
completedBytes: totalBytes,
|
|
150
276
|
totalBytes: totalBytes
|
|
151
277
|
))
|
|
152
|
-
try FileManager.default.removeItem(at: fileURL)
|
|
153
278
|
}
|
|
154
279
|
|
|
155
280
|
private static func requiredURL(_ value: String) throws -> URL {
|
|
@@ -175,4 +300,11 @@ actor BotaDeviceSDKAppleRecordingUploads {
|
|
|
175
300
|
.appendingPathComponent("BotaDeviceSDK", isDirectory: true)
|
|
176
301
|
.appendingPathComponent("compat-upload-queue.json")
|
|
177
302
|
}
|
|
303
|
+
|
|
304
|
+
private static func synchronizeDirectory(_ directory: URL) throws {
|
|
305
|
+
let descriptor = Darwin.open(directory.path, O_RDONLY)
|
|
306
|
+
guard descriptor >= 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) }
|
|
307
|
+
defer { Darwin.close(descriptor) }
|
|
308
|
+
guard Darwin.fsync(descriptor) == 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) }
|
|
309
|
+
}
|
|
178
310
|
}
|