Skip to content

Commit 1be5de4

Browse files
authored
Provide an arbitrary data persistence mechanism on storage instances (#5637)
* Provide a library data mechanism on storage instances This could allow most singletons to no longer need NSCoding, they just store the individual fields they need with an appropriately unique key * Additional tests And no more implicit removal * Implement read error capture * Make test file data keys more unique Also, there _should_ be an error reading from a non-existent file.
1 parent 0644496 commit 1be5de4

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

GoogleDataTransport/GDTCORLibrary/GDTCORFlatFileStorage.m

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ + (NSString *)archivePath {
5151
return archivePath;
5252
}
5353

54+
+ (NSString *)libraryDataPath {
55+
static NSString *libraryDataPath;
56+
static dispatch_once_t onceToken;
57+
dispatch_once(&onceToken, ^{
58+
libraryDataPath =
59+
[GDTCORRootDirectory() URLByAppendingPathComponent:NSStringFromClass([self class])
60+
isDirectory:YES]
61+
.path;
62+
libraryDataPath = [libraryDataPath stringByAppendingPathComponent:@"gdt_library_data"];
63+
if (![[NSFileManager defaultManager] fileExistsAtPath:libraryDataPath isDirectory:NULL]) {
64+
NSError *error;
65+
[[NSFileManager defaultManager] createDirectoryAtPath:libraryDataPath
66+
withIntermediateDirectories:YES
67+
attributes:0
68+
error:&error];
69+
GDTCORAssert(error == nil, @"Creating the library data path failed: %@", error);
70+
}
71+
});
72+
return libraryDataPath;
73+
}
74+
5475
+ (instancetype)sharedInstance {
5576
static GDTCORFlatFileStorage *sharedStorage;
5677
static dispatch_once_t onceToken;
@@ -187,6 +208,54 @@ - (void)removeEvents:(NSSet<NSNumber *> *)eventIDs {
187208
});
188209
}
189210

211+
#pragma mark - GDTCORStorageProtocol
212+
213+
- (void)libraryDataForKey:(nonnull NSString *)key
214+
onComplete:
215+
(nonnull void (^)(NSData *_Nullable, NSError *_Nullable error))onComplete {
216+
dispatch_async(_storageQueue, ^{
217+
NSString *dataPath = [[[self class] libraryDataPath] stringByAppendingPathComponent:key];
218+
NSError *error;
219+
NSData *data = [NSData dataWithContentsOfFile:dataPath options:0 error:&error];
220+
if (onComplete) {
221+
onComplete(data, error);
222+
}
223+
});
224+
}
225+
226+
- (void)storeLibraryData:(NSData *)data
227+
forKey:(nonnull NSString *)key
228+
onComplete:(nonnull void (^)(NSError *_Nullable error))onComplete {
229+
if (!data || data.length <= 0) {
230+
if (onComplete) {
231+
onComplete([NSError errorWithDomain:NSInternalInconsistencyException code:-1 userInfo:nil]);
232+
}
233+
return;
234+
}
235+
dispatch_async(_storageQueue, ^{
236+
NSError *error;
237+
NSString *dataPath = [[[self class] libraryDataPath] stringByAppendingPathComponent:key];
238+
[data writeToFile:dataPath options:NSDataWritingAtomic error:&error];
239+
if (onComplete) {
240+
onComplete(error);
241+
}
242+
});
243+
}
244+
245+
- (void)removeLibraryDataForKey:(nonnull NSString *)key
246+
onComplete:(nonnull void (^)(NSError *_Nullable error))onComplete {
247+
dispatch_async(_storageQueue, ^{
248+
NSError *error;
249+
NSString *dataPath = [[[self class] libraryDataPath] stringByAppendingPathComponent:key];
250+
if ([[NSFileManager defaultManager] fileExistsAtPath:dataPath]) {
251+
[[NSFileManager defaultManager] removeItemAtPath:dataPath error:&error];
252+
if (onComplete) {
253+
onComplete(error);
254+
}
255+
}
256+
});
257+
}
258+
190259
#pragma mark - Private helper methods
191260

192261
/** Saves the event's dataObject to a file using NSData mechanisms.

GoogleDataTransport/GDTCORLibrary/Public/GDTCORStorageProtocol.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ NS_ASSUME_NONNULL_BEGIN
2525
/** Defines the interface a storage subsystem is expected to implement. */
2626
@protocol GDTCORStorageProtocol <NSObject, GDTCORLifecycleProtocol>
2727

28+
@required
29+
2830
/** Stores an event and calls onComplete with a non-nil error if anything went wrong.
2931
*
3032
* @param event The event to store
@@ -36,6 +38,32 @@ NS_ASSUME_NONNULL_BEGIN
3638
/** Removes the events from storage. */
3739
- (void)removeEvents:(NSSet<NSNumber *> *)eventIDs;
3840

41+
/** Persists the given data with the given key.
42+
*
43+
* @param data The data to store.
44+
* @param key The unique key to store it to.
45+
* @param onComplete An block to be run when storage of the data is complete.
46+
*/
47+
- (void)storeLibraryData:(NSData *)data
48+
forKey:(NSString *)key
49+
onComplete:(void (^)(NSError *_Nullable error))onComplete;
50+
51+
/** Retrieves the stored data for the given key.
52+
*
53+
* @param key The key corresponding to the desired data.
54+
* @param onComplete The callback to invoke with the data once it's retrieved.
55+
*/
56+
- (void)libraryDataForKey:(NSString *)key
57+
onComplete:(void (^)(NSData *_Nullable data, NSError *_Nullable error))onComplete;
58+
59+
/** Removes data from storage and calls the callback when complete.
60+
*
61+
* @param key The key of the data to remove.
62+
* @param onComplete The callback that will be invoked when removing the data is complete.
63+
*/
64+
- (void)removeLibraryDataForKey:(NSString *)key
65+
onComplete:(void (^)(NSError *_Nullable error))onComplete;
66+
3967
@end
4068

4169
NS_ASSUME_NONNULL_END

GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,27 @@ - (void)storeEvent:(GDTCOREvent *)event
2828
- (void)removeEvents:(NSSet<NSNumber *> *)eventIDs {
2929
}
3030

31+
- (void)libraryDataForKey:(nonnull NSString *)key
32+
onComplete:
33+
(nonnull void (^)(NSData *_Nullable, NSError *_Nullable error))onComplete {
34+
if (onComplete) {
35+
onComplete(nil, nil);
36+
}
37+
}
38+
39+
- (void)storeLibraryData:(NSData *)data
40+
forKey:(nonnull NSString *)key
41+
onComplete:(nonnull void (^)(NSError *_Nullable error))onComplete {
42+
if (onComplete) {
43+
onComplete(nil);
44+
}
45+
}
46+
47+
- (void)removeLibraryDataForKey:(nonnull NSString *)key
48+
onComplete:(nonnull void (^)(NSError *_Nullable))onComplete {
49+
if (onComplete) {
50+
onComplete(nil);
51+
}
52+
}
53+
3154
@end

GoogleDataTransport/GDTCORTests/Unit/GDTCORFlatFileStorageTest.m

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,86 @@ - (void)testStoringEventsDuringTerminate {
445445
}
446446
}
447447

448+
- (void)testSaveAndLoadLibraryData {
449+
__weak NSData *weakData;
450+
NSString *dataKey = NSStringFromSelector(_cmd);
451+
@autoreleasepool {
452+
NSData *data = [@"test data" dataUsingEncoding:NSUTF8StringEncoding];
453+
weakData = data;
454+
XCTestExpectation *expectation = [self expectationWithDescription:@"storage completion called"];
455+
[[GDTCORFlatFileStorage sharedInstance] storeLibraryData:data
456+
forKey:dataKey
457+
onComplete:^(NSError *_Nullable error) {
458+
XCTAssertNil(error);
459+
[expectation fulfill];
460+
}];
461+
[self waitForExpectations:@[ expectation ] timeout:10.0];
462+
}
463+
XCTAssertNil(weakData);
464+
XCTestExpectation *expectation = [self expectationWithDescription:@"retrieval completion called"];
465+
[[GDTCORFlatFileStorage sharedInstance]
466+
libraryDataForKey:dataKey
467+
onComplete:^(NSData *_Nullable data, NSError *_Nullable error) {
468+
[expectation fulfill];
469+
XCTAssertNil(error);
470+
XCTAssertEqualObjects(@"test data",
471+
[[NSString alloc] initWithData:data
472+
encoding:NSUTF8StringEncoding]);
473+
}];
474+
[self waitForExpectations:@[ expectation ] timeout:10.0];
475+
}
476+
477+
- (void)testSavingNilLibraryData {
478+
XCTestExpectation *expectation = [self expectationWithDescription:@"storage completion called"];
479+
[[GDTCORFlatFileStorage sharedInstance] storeLibraryData:[NSData data]
480+
forKey:@"test data key"
481+
onComplete:^(NSError *_Nullable error) {
482+
XCTAssertNotNil(error);
483+
[expectation fulfill];
484+
}];
485+
[self waitForExpectations:@[ expectation ] timeout:10.0];
486+
}
487+
488+
- (void)testSaveAndRemoveLibraryData {
489+
NSString *dataKey = NSStringFromSelector(_cmd);
490+
NSData *data = [@"test data" dataUsingEncoding:NSUTF8StringEncoding];
491+
XCTestExpectation *expectation = [self expectationWithDescription:@"storage completion called"];
492+
[[GDTCORFlatFileStorage sharedInstance] storeLibraryData:data
493+
forKey:dataKey
494+
onComplete:^(NSError *_Nullable error) {
495+
XCTAssertNil(error);
496+
[expectation fulfill];
497+
}];
498+
[self waitForExpectations:@[ expectation ] timeout:10.0];
499+
expectation = [self expectationWithDescription:@"retrieval completion called"];
500+
[[GDTCORFlatFileStorage sharedInstance]
501+
libraryDataForKey:dataKey
502+
onComplete:^(NSData *_Nullable data, NSError *_Nullable error) {
503+
[expectation fulfill];
504+
XCTAssertNil(error);
505+
XCTAssertEqualObjects(@"test data",
506+
[[NSString alloc] initWithData:data
507+
encoding:NSUTF8StringEncoding]);
508+
}];
509+
[self waitForExpectations:@[ expectation ] timeout:10.0];
510+
expectation = [self expectationWithDescription:@"removal completion called"];
511+
[[GDTCORFlatFileStorage sharedInstance] removeLibraryDataForKey:dataKey
512+
onComplete:^(NSError *error) {
513+
[expectation fulfill];
514+
XCTAssertNil(error);
515+
}];
516+
[self waitForExpectations:@[ expectation ] timeout:10.0];
517+
expectation = [self expectationWithDescription:@"retrieval completion called"];
518+
[[GDTCORFlatFileStorage sharedInstance]
519+
libraryDataForKey:dataKey
520+
onComplete:^(NSData *_Nullable data, NSError *_Nullable error) {
521+
[expectation fulfill];
522+
XCTAssertNotNil(error);
523+
XCTAssertNil(data);
524+
}];
525+
[self waitForExpectations:@[ expectation ] timeout:10.0];
526+
}
527+
448528
/** Tests migration from v1 of the storage format to v2. */
449529
- (void)testMigrationFromOldVersion {
450530
static NSString *base64EncodedArchive =

0 commit comments

Comments
 (0)