1 |
efrain |
1 |
/*
|
|
|
2 |
* Copyright 2018 Google
|
|
|
3 |
*
|
|
|
4 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
5 |
* you may not use this file except in compliance with the License.
|
|
|
6 |
* You may obtain a copy of the License at
|
|
|
7 |
*
|
|
|
8 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
9 |
*
|
|
|
10 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
11 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
12 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
13 |
* See the License for the specific language governing permissions and
|
|
|
14 |
* limitations under the License.
|
|
|
15 |
*/
|
|
|
16 |
|
|
|
17 |
#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORFlatFileStorage.h"
|
|
|
18 |
|
|
|
19 |
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORAssert.h"
|
|
|
20 |
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORLifecycle.h"
|
|
|
21 |
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h"
|
|
|
22 |
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageEventSelector.h"
|
|
|
23 |
#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h"
|
|
|
24 |
#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h"
|
|
|
25 |
|
|
|
26 |
#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCOREvent_Private.h"
|
|
|
27 |
#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h"
|
|
|
28 |
#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadCoordinator.h"
|
|
|
29 |
|
|
|
30 |
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORDirectorySizeTracker.h"
|
|
|
31 |
|
|
|
32 |
NS_ASSUME_NONNULL_BEGIN
|
|
|
33 |
|
|
|
34 |
/** A library data key this class uses to track batchIDs. */
|
|
|
35 |
static NSString *const gBatchIDCounterKey = @"GDTCORFlatFileStorageBatchIDCounter";
|
|
|
36 |
|
|
|
37 |
/** The separator used between metadata elements in filenames. */
|
|
|
38 |
static NSString *const kMetadataSeparator = @"-";
|
|
|
39 |
|
|
|
40 |
NSString *const kGDTCOREventComponentsEventIDKey = @"GDTCOREventComponentsEventIDKey";
|
|
|
41 |
|
|
|
42 |
NSString *const kGDTCOREventComponentsQoSTierKey = @"GDTCOREventComponentsQoSTierKey";
|
|
|
43 |
|
|
|
44 |
NSString *const kGDTCOREventComponentsMappingIDKey = @"GDTCOREventComponentsMappingIDKey";
|
|
|
45 |
|
|
|
46 |
NSString *const kGDTCOREventComponentsExpirationKey = @"GDTCOREventComponentsExpirationKey";
|
|
|
47 |
|
|
|
48 |
NSString *const kGDTCORBatchComponentsTargetKey = @"GDTCORBatchComponentsTargetKey";
|
|
|
49 |
|
|
|
50 |
NSString *const kGDTCORBatchComponentsBatchIDKey = @"GDTCORBatchComponentsBatchIDKey";
|
|
|
51 |
|
|
|
52 |
NSString *const kGDTCORBatchComponentsExpirationKey = @"GDTCORBatchComponentsExpirationKey";
|
|
|
53 |
|
|
|
54 |
NSString *const GDTCORFlatFileStorageErrorDomain = @"GDTCORFlatFileStorage";
|
|
|
55 |
|
|
|
56 |
const uint64_t kGDTCORFlatFileStorageSizeLimit = 20 * 1000 * 1000; // 20 MB.
|
|
|
57 |
|
|
|
58 |
@interface GDTCORFlatFileStorage ()
|
|
|
59 |
|
|
|
60 |
/** An instance of the size tracker to keep track of the disk space consumed by the storage. */
|
|
|
61 |
@property(nonatomic, readonly) GDTCORDirectorySizeTracker *sizeTracker;
|
|
|
62 |
|
|
|
63 |
@end
|
|
|
64 |
|
|
|
65 |
@implementation GDTCORFlatFileStorage
|
|
|
66 |
|
|
|
67 |
@synthesize sizeTracker = _sizeTracker;
|
|
|
68 |
|
|
|
69 |
+ (void)load {
|
|
|
70 |
#if !NDEBUG
|
|
|
71 |
[[GDTCORRegistrar sharedInstance] registerStorage:[self sharedInstance] target:kGDTCORTargetTest];
|
|
|
72 |
#endif // !NDEBUG
|
|
|
73 |
[[GDTCORRegistrar sharedInstance] registerStorage:[self sharedInstance] target:kGDTCORTargetCCT];
|
|
|
74 |
[[GDTCORRegistrar sharedInstance] registerStorage:[self sharedInstance] target:kGDTCORTargetFLL];
|
|
|
75 |
[[GDTCORRegistrar sharedInstance] registerStorage:[self sharedInstance] target:kGDTCORTargetCSH];
|
|
|
76 |
[[GDTCORRegistrar sharedInstance] registerStorage:[self sharedInstance] target:kGDTCORTargetINT];
|
|
|
77 |
}
|
|
|
78 |
|
|
|
79 |
+ (instancetype)sharedInstance {
|
|
|
80 |
static GDTCORFlatFileStorage *sharedStorage;
|
|
|
81 |
static dispatch_once_t onceToken;
|
|
|
82 |
dispatch_once(&onceToken, ^{
|
|
|
83 |
sharedStorage = [[GDTCORFlatFileStorage alloc] init];
|
|
|
84 |
});
|
|
|
85 |
return sharedStorage;
|
|
|
86 |
}
|
|
|
87 |
|
|
|
88 |
- (instancetype)init {
|
|
|
89 |
self = [super init];
|
|
|
90 |
if (self) {
|
|
|
91 |
_storageQueue =
|
|
|
92 |
dispatch_queue_create("com.google.GDTCORFlatFileStorage", DISPATCH_QUEUE_SERIAL);
|
|
|
93 |
_uploadCoordinator = [GDTCORUploadCoordinator sharedInstance];
|
|
|
94 |
}
|
|
|
95 |
return self;
|
|
|
96 |
}
|
|
|
97 |
|
|
|
98 |
- (GDTCORDirectorySizeTracker *)sizeTracker {
|
|
|
99 |
if (_sizeTracker == nil) {
|
|
|
100 |
_sizeTracker =
|
|
|
101 |
[[GDTCORDirectorySizeTracker alloc] initWithDirectoryPath:GDTCORRootDirectory().path];
|
|
|
102 |
}
|
|
|
103 |
return _sizeTracker;
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
#pragma mark - GDTCORStorageProtocol
|
|
|
107 |
|
|
|
108 |
- (void)storeEvent:(GDTCOREvent *)event
|
|
|
109 |
onComplete:(void (^_Nullable)(BOOL wasWritten, NSError *_Nullable error))completion {
|
|
|
110 |
GDTCORLogDebug(@"Saving event: %@", event);
|
|
|
111 |
if (event == nil || event.serializedDataObjectBytes == nil) {
|
|
|
112 |
GDTCORLogDebug(@"%@", @"The event was nil, so it was not saved.");
|
|
|
113 |
if (completion) {
|
|
|
114 |
completion(NO, [NSError errorWithDomain:NSInternalInconsistencyException
|
|
|
115 |
code:-1
|
|
|
116 |
userInfo:nil]);
|
|
|
117 |
}
|
|
|
118 |
return;
|
|
|
119 |
}
|
|
|
120 |
if (!completion) {
|
|
|
121 |
completion = ^(BOOL wasWritten, NSError *_Nullable error) {
|
|
|
122 |
GDTCORLogDebug(@"event %@ stored. success:%@ error:%@", event, wasWritten ? @"YES" : @"NO",
|
|
|
123 |
error);
|
|
|
124 |
};
|
|
|
125 |
}
|
|
|
126 |
|
|
|
127 |
__block GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid;
|
|
|
128 |
bgID = [[GDTCORApplication sharedApplication]
|
|
|
129 |
beginBackgroundTaskWithName:@"GDTStorage"
|
|
|
130 |
expirationHandler:^{
|
|
|
131 |
// End the background task if it's still valid.
|
|
|
132 |
[[GDTCORApplication sharedApplication] endBackgroundTask:bgID];
|
|
|
133 |
bgID = GDTCORBackgroundIdentifierInvalid;
|
|
|
134 |
}];
|
|
|
135 |
|
|
|
136 |
dispatch_async(_storageQueue, ^{
|
|
|
137 |
// Check that a backend implementation is available for this target.
|
|
|
138 |
GDTCORTarget target = event.target;
|
|
|
139 |
NSString *filePath = [GDTCORFlatFileStorage pathForTarget:target
|
|
|
140 |
eventID:event.eventID
|
|
|
141 |
qosTier:@(event.qosTier)
|
|
|
142 |
expirationDate:event.expirationDate
|
|
|
143 |
mappingID:event.mappingID];
|
|
|
144 |
NSError *error;
|
|
|
145 |
NSData *encodedEvent = GDTCOREncodeArchive(event, nil, &error);
|
|
|
146 |
if (error) {
|
|
|
147 |
completion(NO, error);
|
|
|
148 |
return;
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
// Check storage size limit before storing the event.
|
|
|
152 |
uint64_t resultingStorageSize = self.sizeTracker.directoryContentSize + encodedEvent.length;
|
|
|
153 |
if (resultingStorageSize > kGDTCORFlatFileStorageSizeLimit) {
|
|
|
154 |
NSError *error = [NSError
|
|
|
155 |
errorWithDomain:GDTCORFlatFileStorageErrorDomain
|
|
|
156 |
code:GDTCORFlatFileStorageErrorSizeLimitReached
|
|
|
157 |
userInfo:@{
|
|
|
158 |
NSLocalizedFailureReasonErrorKey : @"Storage size limit has been reached."
|
|
|
159 |
}];
|
|
|
160 |
completion(NO, error);
|
|
|
161 |
return;
|
|
|
162 |
}
|
|
|
163 |
|
|
|
164 |
// Write the encoded event to the file.
|
|
|
165 |
BOOL writeResult = GDTCORWriteDataToFile(encodedEvent, filePath, &error);
|
|
|
166 |
if (writeResult == NO || error) {
|
|
|
167 |
GDTCORLogDebug(@"Attempt to write archive failed: path:%@ error:%@", filePath, error);
|
|
|
168 |
completion(NO, error);
|
|
|
169 |
return;
|
|
|
170 |
} else {
|
|
|
171 |
GDTCORLogDebug(@"Writing archive succeeded: %@", filePath);
|
|
|
172 |
completion(YES, nil);
|
|
|
173 |
}
|
|
|
174 |
|
|
|
175 |
// Notify size tracker.
|
|
|
176 |
[self.sizeTracker fileWasAddedAtPath:filePath withSize:encodedEvent.length];
|
|
|
177 |
|
|
|
178 |
// Check the QoS, if it's high priority, notify the target that it has a high priority event.
|
|
|
179 |
if (event.qosTier == GDTCOREventQoSFast) {
|
|
|
180 |
// TODO: Remove a direct dependency on the upload coordinator.
|
|
|
181 |
[self.uploadCoordinator forceUploadForTarget:target];
|
|
|
182 |
}
|
|
|
183 |
|
|
|
184 |
// Cancel or end the associated background task if it's still valid.
|
|
|
185 |
[[GDTCORApplication sharedApplication] endBackgroundTask:bgID];
|
|
|
186 |
bgID = GDTCORBackgroundIdentifierInvalid;
|
|
|
187 |
});
|
|
|
188 |
}
|
|
|
189 |
|
|
|
190 |
- (void)batchWithEventSelector:(nonnull GDTCORStorageEventSelector *)eventSelector
|
|
|
191 |
batchExpiration:(nonnull NSDate *)expiration
|
|
|
192 |
onComplete:
|
|
|
193 |
(nonnull void (^)(NSNumber *_Nullable batchID,
|
|
|
194 |
NSSet<GDTCOREvent *> *_Nullable events))onComplete {
|
|
|
195 |
dispatch_queue_t queue = _storageQueue;
|
|
|
196 |
void (^onPathsForTargetComplete)(NSNumber *, NSSet<NSString *> *_Nonnull) = ^(
|
|
|
197 |
NSNumber *batchID, NSSet<NSString *> *_Nonnull paths) {
|
|
|
198 |
dispatch_async(queue, ^{
|
|
|
199 |
NSMutableSet<GDTCOREvent *> *events = [[NSMutableSet alloc] init];
|
|
|
200 |
for (NSString *eventPath in paths) {
|
|
|
201 |
NSError *error;
|
|
|
202 |
GDTCOREvent *event =
|
|
|
203 |
(GDTCOREvent *)GDTCORDecodeArchive([GDTCOREvent class], eventPath, nil, &error);
|
|
|
204 |
if (event == nil || error) {
|
|
|
205 |
GDTCORLogDebug(@"Error deserializing event: %@", error);
|
|
|
206 |
[[NSFileManager defaultManager] removeItemAtPath:eventPath error:nil];
|
|
|
207 |
continue;
|
|
|
208 |
} else {
|
|
|
209 |
NSString *fileName = [eventPath lastPathComponent];
|
|
|
210 |
NSString *batchPath =
|
|
|
211 |
[GDTCORFlatFileStorage batchPathForTarget:eventSelector.selectedTarget
|
|
|
212 |
batchID:batchID
|
|
|
213 |
expirationDate:expiration];
|
|
|
214 |
[[NSFileManager defaultManager] createDirectoryAtPath:batchPath
|
|
|
215 |
withIntermediateDirectories:YES
|
|
|
216 |
attributes:nil
|
|
|
217 |
error:nil];
|
|
|
218 |
NSString *destinationPath = [batchPath stringByAppendingPathComponent:fileName];
|
|
|
219 |
error = nil;
|
|
|
220 |
[[NSFileManager defaultManager] moveItemAtPath:eventPath
|
|
|
221 |
toPath:destinationPath
|
|
|
222 |
error:&error];
|
|
|
223 |
if (error) {
|
|
|
224 |
GDTCORLogDebug(@"An event file wasn't moveable into the batch directory: %@", error);
|
|
|
225 |
}
|
|
|
226 |
[events addObject:event];
|
|
|
227 |
}
|
|
|
228 |
}
|
|
|
229 |
if (onComplete) {
|
|
|
230 |
if (events.count == 0) {
|
|
|
231 |
onComplete(nil, nil);
|
|
|
232 |
} else {
|
|
|
233 |
onComplete(batchID, events);
|
|
|
234 |
}
|
|
|
235 |
}
|
|
|
236 |
});
|
|
|
237 |
};
|
|
|
238 |
|
|
|
239 |
void (^onBatchIDFetchComplete)(NSNumber *) = ^(NSNumber *batchID) {
|
|
|
240 |
dispatch_async(queue, ^{
|
|
|
241 |
if (batchID == nil) {
|
|
|
242 |
if (onComplete) {
|
|
|
243 |
onComplete(nil, nil);
|
|
|
244 |
return;
|
|
|
245 |
}
|
|
|
246 |
}
|
|
|
247 |
[self pathsForTarget:eventSelector.selectedTarget
|
|
|
248 |
eventIDs:eventSelector.selectedEventIDs
|
|
|
249 |
qosTiers:eventSelector.selectedQosTiers
|
|
|
250 |
mappingIDs:eventSelector.selectedMappingIDs
|
|
|
251 |
onComplete:^(NSSet<NSString *> *_Nonnull paths) {
|
|
|
252 |
onPathsForTargetComplete(batchID, paths);
|
|
|
253 |
}];
|
|
|
254 |
});
|
|
|
255 |
};
|
|
|
256 |
|
|
|
257 |
[self nextBatchID:^(NSNumber *_Nullable batchID) {
|
|
|
258 |
if (batchID == nil) {
|
|
|
259 |
if (onComplete) {
|
|
|
260 |
onComplete(nil, nil);
|
|
|
261 |
}
|
|
|
262 |
} else {
|
|
|
263 |
onBatchIDFetchComplete(batchID);
|
|
|
264 |
}
|
|
|
265 |
}];
|
|
|
266 |
}
|
|
|
267 |
|
|
|
268 |
- (void)removeBatchWithID:(nonnull NSNumber *)batchID
|
|
|
269 |
deleteEvents:(BOOL)deleteEvents
|
|
|
270 |
onComplete:(void (^_Nullable)(void))onComplete {
|
|
|
271 |
dispatch_async(_storageQueue, ^{
|
|
|
272 |
[self syncThreadUnsafeRemoveBatchWithID:batchID deleteEvents:deleteEvents];
|
|
|
273 |
|
|
|
274 |
if (onComplete) {
|
|
|
275 |
onComplete();
|
|
|
276 |
}
|
|
|
277 |
});
|
|
|
278 |
}
|
|
|
279 |
|
|
|
280 |
- (void)batchIDsForTarget:(GDTCORTarget)target
|
|
|
281 |
onComplete:(nonnull void (^)(NSSet<NSNumber *> *_Nullable))onComplete {
|
|
|
282 |
dispatch_async(_storageQueue, ^{
|
|
|
283 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
284 |
NSError *error;
|
|
|
285 |
NSArray<NSString *> *batchPaths =
|
|
|
286 |
[fileManager contentsOfDirectoryAtPath:[GDTCORFlatFileStorage batchDataStoragePath]
|
|
|
287 |
error:&error];
|
|
|
288 |
if (error || batchPaths.count == 0) {
|
|
|
289 |
if (onComplete) {
|
|
|
290 |
onComplete(nil);
|
|
|
291 |
}
|
|
|
292 |
return;
|
|
|
293 |
}
|
|
|
294 |
NSMutableSet<NSNumber *> *batchIDs = [[NSMutableSet alloc] init];
|
|
|
295 |
for (NSString *path in batchPaths) {
|
|
|
296 |
NSDictionary<NSString *, id> *components = [self batchComponentsFromFilename:path];
|
|
|
297 |
NSNumber *targetNumber = components[kGDTCORBatchComponentsTargetKey];
|
|
|
298 |
NSNumber *batchID = components[kGDTCORBatchComponentsBatchIDKey];
|
|
|
299 |
if (batchID != nil && targetNumber.intValue == target) {
|
|
|
300 |
[batchIDs addObject:batchID];
|
|
|
301 |
}
|
|
|
302 |
}
|
|
|
303 |
if (onComplete) {
|
|
|
304 |
onComplete(batchIDs);
|
|
|
305 |
}
|
|
|
306 |
});
|
|
|
307 |
}
|
|
|
308 |
|
|
|
309 |
- (void)libraryDataForKey:(nonnull NSString *)key
|
|
|
310 |
onFetchComplete:(nonnull void (^)(NSData *_Nullable, NSError *_Nullable))onFetchComplete
|
|
|
311 |
setNewValue:(NSData *_Nullable (^_Nullable)(void))setValueBlock {
|
|
|
312 |
dispatch_async(_storageQueue, ^{
|
|
|
313 |
NSString *dataPath = [[[self class] libraryDataStoragePath] stringByAppendingPathComponent:key];
|
|
|
314 |
NSError *error;
|
|
|
315 |
NSData *data = [NSData dataWithContentsOfFile:dataPath options:0 error:&error];
|
|
|
316 |
if (onFetchComplete) {
|
|
|
317 |
onFetchComplete(data, error);
|
|
|
318 |
}
|
|
|
319 |
if (setValueBlock) {
|
|
|
320 |
NSData *newValue = setValueBlock();
|
|
|
321 |
// The -isKindOfClass check is necessary because without an explicit 'return nil' in the block
|
|
|
322 |
// the implicit return value will be the block itself. The compiler doesn't detect this.
|
|
|
323 |
if (newValue != nil && [newValue isKindOfClass:[NSData class]] && newValue.length) {
|
|
|
324 |
NSError *newValueError;
|
|
|
325 |
if ([newValue writeToFile:dataPath options:NSDataWritingAtomic error:&newValueError]) {
|
|
|
326 |
// Update storage size.
|
|
|
327 |
[self.sizeTracker fileWasRemovedAtPath:dataPath withSize:data.length];
|
|
|
328 |
[self.sizeTracker fileWasAddedAtPath:dataPath withSize:newValue.length];
|
|
|
329 |
} else {
|
|
|
330 |
GDTCORLogDebug(@"Error writing new value in libraryDataForKey: %@", newValueError);
|
|
|
331 |
}
|
|
|
332 |
}
|
|
|
333 |
}
|
|
|
334 |
});
|
|
|
335 |
}
|
|
|
336 |
|
|
|
337 |
- (void)storeLibraryData:(NSData *)data
|
|
|
338 |
forKey:(nonnull NSString *)key
|
|
|
339 |
onComplete:(nullable void (^)(NSError *_Nullable error))onComplete {
|
|
|
340 |
if (!data || data.length <= 0) {
|
|
|
341 |
if (onComplete) {
|
|
|
342 |
onComplete([NSError errorWithDomain:NSInternalInconsistencyException code:-1 userInfo:nil]);
|
|
|
343 |
}
|
|
|
344 |
return;
|
|
|
345 |
}
|
|
|
346 |
dispatch_async(_storageQueue, ^{
|
|
|
347 |
NSError *error;
|
|
|
348 |
NSString *dataPath = [[[self class] libraryDataStoragePath] stringByAppendingPathComponent:key];
|
|
|
349 |
if ([data writeToFile:dataPath options:NSDataWritingAtomic error:&error]) {
|
|
|
350 |
[self.sizeTracker fileWasAddedAtPath:dataPath withSize:data.length];
|
|
|
351 |
}
|
|
|
352 |
if (onComplete) {
|
|
|
353 |
onComplete(error);
|
|
|
354 |
}
|
|
|
355 |
});
|
|
|
356 |
}
|
|
|
357 |
|
|
|
358 |
- (void)removeLibraryDataForKey:(nonnull NSString *)key
|
|
|
359 |
onComplete:(nonnull void (^)(NSError *_Nullable error))onComplete {
|
|
|
360 |
dispatch_async(_storageQueue, ^{
|
|
|
361 |
NSError *error;
|
|
|
362 |
NSString *dataPath = [[[self class] libraryDataStoragePath] stringByAppendingPathComponent:key];
|
|
|
363 |
GDTCORStorageSizeBytes fileSize =
|
|
|
364 |
[self.sizeTracker fileSizeAtURL:[NSURL fileURLWithPath:dataPath]];
|
|
|
365 |
|
|
|
366 |
if ([[NSFileManager defaultManager] fileExistsAtPath:dataPath]) {
|
|
|
367 |
if ([[NSFileManager defaultManager] removeItemAtPath:dataPath error:&error]) {
|
|
|
368 |
[self.sizeTracker fileWasRemovedAtPath:dataPath withSize:fileSize];
|
|
|
369 |
}
|
|
|
370 |
if (onComplete) {
|
|
|
371 |
onComplete(error);
|
|
|
372 |
}
|
|
|
373 |
}
|
|
|
374 |
});
|
|
|
375 |
}
|
|
|
376 |
|
|
|
377 |
- (void)hasEventsForTarget:(GDTCORTarget)target onComplete:(void (^)(BOOL hasEvents))onComplete {
|
|
|
378 |
dispatch_async(_storageQueue, ^{
|
|
|
379 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
380 |
NSString *targetPath = [NSString
|
|
|
381 |
stringWithFormat:@"%@/%ld", [GDTCORFlatFileStorage eventDataStoragePath], (long)target];
|
|
|
382 |
[fileManager createDirectoryAtPath:targetPath
|
|
|
383 |
withIntermediateDirectories:YES
|
|
|
384 |
attributes:nil
|
|
|
385 |
error:nil];
|
|
|
386 |
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:targetPath];
|
|
|
387 |
BOOL hasEventAtLeastOneEvent = [enumerator nextObject] != nil;
|
|
|
388 |
if (onComplete) {
|
|
|
389 |
onComplete(hasEventAtLeastOneEvent);
|
|
|
390 |
}
|
|
|
391 |
});
|
|
|
392 |
}
|
|
|
393 |
|
|
|
394 |
- (void)checkForExpirations {
|
|
|
395 |
dispatch_async(_storageQueue, ^{
|
|
|
396 |
GDTCORLogDebug(@"%@", @"Checking for expired events and batches");
|
|
|
397 |
NSTimeInterval now = [NSDate date].timeIntervalSince1970;
|
|
|
398 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
399 |
|
|
|
400 |
// TODO: Storage may not have enough context to remove batches because a batch may be being
|
|
|
401 |
// uploaded but the storage has not context of it.
|
|
|
402 |
|
|
|
403 |
// Find expired batches and move their events back to the main storage.
|
|
|
404 |
// If a batch contains expired events they are expected to be removed further in the method
|
|
|
405 |
// together with other expired events in the main storage.
|
|
|
406 |
NSString *batchDataPath = [GDTCORFlatFileStorage batchDataStoragePath];
|
|
|
407 |
NSArray<NSString *> *batchDataPaths = [fileManager contentsOfDirectoryAtPath:batchDataPath
|
|
|
408 |
error:nil];
|
|
|
409 |
for (NSString *path in batchDataPaths) {
|
|
|
410 |
@autoreleasepool {
|
|
|
411 |
NSString *fileName = [path lastPathComponent];
|
|
|
412 |
NSDictionary<NSString *, id> *batchComponents = [self batchComponentsFromFilename:fileName];
|
|
|
413 |
NSDate *expirationDate = batchComponents[kGDTCORBatchComponentsExpirationKey];
|
|
|
414 |
NSNumber *batchID = batchComponents[kGDTCORBatchComponentsBatchIDKey];
|
|
|
415 |
if (expirationDate != nil && expirationDate.timeIntervalSince1970 < now && batchID != nil) {
|
|
|
416 |
NSNumber *batchID = batchComponents[kGDTCORBatchComponentsBatchIDKey];
|
|
|
417 |
// Move all events from the expired batch back to the main storage.
|
|
|
418 |
[self syncThreadUnsafeRemoveBatchWithID:batchID deleteEvents:NO];
|
|
|
419 |
}
|
|
|
420 |
}
|
|
|
421 |
}
|
|
|
422 |
|
|
|
423 |
// Find expired events and remove them from the storage.
|
|
|
424 |
NSString *eventDataPath = [GDTCORFlatFileStorage eventDataStoragePath];
|
|
|
425 |
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:eventDataPath];
|
|
|
426 |
NSString *path;
|
|
|
427 |
|
|
|
428 |
while (YES) {
|
|
|
429 |
@autoreleasepool {
|
|
|
430 |
// Call `[enumerator nextObject]` under autorelease pool to make sure all autoreleased
|
|
|
431 |
// objects created under the hood are released on each iteration end to avoid unnecessary
|
|
|
432 |
// memory growth.
|
|
|
433 |
path = [enumerator nextObject];
|
|
|
434 |
if (path == nil) {
|
|
|
435 |
break;
|
|
|
436 |
}
|
|
|
437 |
|
|
|
438 |
NSString *fileName = [path lastPathComponent];
|
|
|
439 |
NSDictionary<NSString *, id> *eventComponents = [self eventComponentsFromFilename:fileName];
|
|
|
440 |
NSDate *expirationDate = eventComponents[kGDTCOREventComponentsExpirationKey];
|
|
|
441 |
if (expirationDate != nil && expirationDate.timeIntervalSince1970 < now) {
|
|
|
442 |
NSString *pathToDelete = [eventDataPath stringByAppendingPathComponent:path];
|
|
|
443 |
NSError *error;
|
|
|
444 |
[fileManager removeItemAtPath:pathToDelete error:&error];
|
|
|
445 |
if (error != nil) {
|
|
|
446 |
GDTCORLogDebug(@"There was an error deleting an expired item: %@", error);
|
|
|
447 |
} else {
|
|
|
448 |
GDTCORLogDebug(@"Item deleted because it expired: %@", pathToDelete);
|
|
|
449 |
}
|
|
|
450 |
}
|
|
|
451 |
}
|
|
|
452 |
}
|
|
|
453 |
|
|
|
454 |
[self.sizeTracker resetCachedSize];
|
|
|
455 |
});
|
|
|
456 |
}
|
|
|
457 |
|
|
|
458 |
- (void)storageSizeWithCallback:(void (^)(uint64_t storageSize))onComplete {
|
|
|
459 |
if (!onComplete) {
|
|
|
460 |
return;
|
|
|
461 |
}
|
|
|
462 |
|
|
|
463 |
dispatch_async(_storageQueue, ^{
|
|
|
464 |
onComplete([self.sizeTracker directoryContentSize]);
|
|
|
465 |
});
|
|
|
466 |
}
|
|
|
467 |
|
|
|
468 |
#pragma mark - Private not thread safe methods
|
|
|
469 |
/** Looks for directory paths containing events for a batch with the specified ID.
|
|
|
470 |
* @param batchID A batch ID.
|
|
|
471 |
* @param outError A pointer to `NSError *` to assign as possible error to.
|
|
|
472 |
* @return An array of an array of paths to directories for event batches with a specified batch ID
|
|
|
473 |
* or `nil` in the case of an error. Usually returns a single path but potentially return more in
|
|
|
474 |
* cases when the app is terminated while uploading a batch.
|
|
|
475 |
*/
|
|
|
476 |
- (nullable NSArray<NSString *> *)batchDirPathsForBatchID:(NSNumber *)batchID
|
|
|
477 |
error:(NSError **_Nonnull)outError {
|
|
|
478 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
479 |
NSError *error;
|
|
|
480 |
NSArray<NSString *> *batches =
|
|
|
481 |
[fileManager contentsOfDirectoryAtPath:[GDTCORFlatFileStorage batchDataStoragePath]
|
|
|
482 |
error:&error];
|
|
|
483 |
if (batches == nil) {
|
|
|
484 |
*outError = error;
|
|
|
485 |
GDTCORLogDebug(@"Failed to find event file paths for batchID: %@, error: %@", batchID, error);
|
|
|
486 |
return nil;
|
|
|
487 |
}
|
|
|
488 |
|
|
|
489 |
NSMutableArray<NSString *> *batchDirPaths = [NSMutableArray array];
|
|
|
490 |
for (NSString *path in batches) {
|
|
|
491 |
NSDictionary<NSString *, id> *components = [self batchComponentsFromFilename:path];
|
|
|
492 |
NSNumber *pathBatchID = components[kGDTCORBatchComponentsBatchIDKey];
|
|
|
493 |
if ([pathBatchID isEqual:batchID]) {
|
|
|
494 |
NSString *batchDirPath =
|
|
|
495 |
[[GDTCORFlatFileStorage batchDataStoragePath] stringByAppendingPathComponent:path];
|
|
|
496 |
[batchDirPaths addObject:batchDirPath];
|
|
|
497 |
}
|
|
|
498 |
}
|
|
|
499 |
|
|
|
500 |
return [batchDirPaths copy];
|
|
|
501 |
}
|
|
|
502 |
|
|
|
503 |
/** Makes a copy of the contents of a directory to a directory at the specified path.*/
|
|
|
504 |
- (BOOL)moveContentsOfDirectoryAtPath:(NSString *)sourcePath
|
|
|
505 |
to:(NSString *)destinationPath
|
|
|
506 |
error:(NSError **_Nonnull)outError {
|
|
|
507 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
508 |
|
|
|
509 |
NSError *error;
|
|
|
510 |
NSArray<NSString *> *contentsPaths = [fileManager contentsOfDirectoryAtPath:sourcePath
|
|
|
511 |
error:&error];
|
|
|
512 |
if (contentsPaths == nil) {
|
|
|
513 |
*outError = error;
|
|
|
514 |
return NO;
|
|
|
515 |
}
|
|
|
516 |
|
|
|
517 |
NSMutableArray<NSError *> *errors = [NSMutableArray array];
|
|
|
518 |
for (NSString *path in contentsPaths) {
|
|
|
519 |
NSString *contentDestinationPath = [destinationPath stringByAppendingPathComponent:path];
|
|
|
520 |
NSString *contentSourcePath = [sourcePath stringByAppendingPathComponent:path];
|
|
|
521 |
|
|
|
522 |
NSError *moveError;
|
|
|
523 |
if (![fileManager moveItemAtPath:contentSourcePath
|
|
|
524 |
toPath:contentDestinationPath
|
|
|
525 |
error:&moveError] &&
|
|
|
526 |
moveError) {
|
|
|
527 |
[errors addObject:moveError];
|
|
|
528 |
}
|
|
|
529 |
}
|
|
|
530 |
|
|
|
531 |
if (errors.count == 0) {
|
|
|
532 |
return YES;
|
|
|
533 |
} else {
|
|
|
534 |
NSError *combinedError = [NSError errorWithDomain:@"GDTCORFlatFileStorage"
|
|
|
535 |
code:-1
|
|
|
536 |
userInfo:@{NSUnderlyingErrorKey : errors}];
|
|
|
537 |
*outError = combinedError;
|
|
|
538 |
return NO;
|
|
|
539 |
}
|
|
|
540 |
}
|
|
|
541 |
|
|
|
542 |
- (void)syncThreadUnsafeRemoveBatchWithID:(nonnull NSNumber *)batchID
|
|
|
543 |
deleteEvents:(BOOL)deleteEvents {
|
|
|
544 |
NSError *error;
|
|
|
545 |
NSArray<NSString *> *batchDirPaths = [self batchDirPathsForBatchID:batchID error:&error];
|
|
|
546 |
|
|
|
547 |
if (batchDirPaths == nil) {
|
|
|
548 |
return;
|
|
|
549 |
}
|
|
|
550 |
|
|
|
551 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
552 |
|
|
|
553 |
void (^removeBatchDir)(NSString *batchDirPath) = ^(NSString *batchDirPath) {
|
|
|
554 |
NSError *error;
|
|
|
555 |
if ([fileManager removeItemAtPath:batchDirPath error:&error]) {
|
|
|
556 |
GDTCORLogDebug(@"Batch removed at path: %@", batchDirPath);
|
|
|
557 |
} else {
|
|
|
558 |
GDTCORLogDebug(@"Failed to remove batch at path: %@", batchDirPath);
|
|
|
559 |
}
|
|
|
560 |
};
|
|
|
561 |
|
|
|
562 |
for (NSString *batchDirPath in batchDirPaths) {
|
|
|
563 |
@autoreleasepool {
|
|
|
564 |
if (deleteEvents) {
|
|
|
565 |
removeBatchDir(batchDirPath);
|
|
|
566 |
} else {
|
|
|
567 |
NSString *batchDirName = [batchDirPath lastPathComponent];
|
|
|
568 |
NSDictionary<NSString *, id> *components = [self batchComponentsFromFilename:batchDirName];
|
|
|
569 |
NSString *targetValue = [components[kGDTCORBatchComponentsTargetKey] stringValue];
|
|
|
570 |
NSString *destinationPath;
|
|
|
571 |
if (targetValue) {
|
|
|
572 |
destinationPath = [[GDTCORFlatFileStorage eventDataStoragePath]
|
|
|
573 |
stringByAppendingPathComponent:targetValue];
|
|
|
574 |
}
|
|
|
575 |
|
|
|
576 |
// `- [NSFileManager moveItemAtPath:toPath:error:]` method fails if an item by the
|
|
|
577 |
// destination path already exists (which usually is the case for the current method). Move
|
|
|
578 |
// the events one by one instead.
|
|
|
579 |
if (destinationPath && [self moveContentsOfDirectoryAtPath:batchDirPath
|
|
|
580 |
to:destinationPath
|
|
|
581 |
error:&error]) {
|
|
|
582 |
GDTCORLogDebug(@"Batched events at path: %@ moved back to the storage: %@", batchDirPath,
|
|
|
583 |
destinationPath);
|
|
|
584 |
} else {
|
|
|
585 |
GDTCORLogDebug(@"Error encountered whilst moving events back: %@", error);
|
|
|
586 |
}
|
|
|
587 |
|
|
|
588 |
// Even if not all events where moved back to the storage, there is not much can be done at
|
|
|
589 |
// this point, so cleanup batch directory now to avoid cluttering.
|
|
|
590 |
removeBatchDir(batchDirPath);
|
|
|
591 |
}
|
|
|
592 |
}
|
|
|
593 |
}
|
|
|
594 |
|
|
|
595 |
[self.sizeTracker resetCachedSize];
|
|
|
596 |
}
|
|
|
597 |
|
|
|
598 |
#pragma mark - Private helper methods
|
|
|
599 |
|
|
|
600 |
+ (NSString *)eventDataStoragePath {
|
|
|
601 |
static NSString *eventDataPath;
|
|
|
602 |
static dispatch_once_t onceToken;
|
|
|
603 |
dispatch_once(&onceToken, ^{
|
|
|
604 |
eventDataPath = [NSString stringWithFormat:@"%@/%@/gdt_event_data", GDTCORRootDirectory().path,
|
|
|
605 |
NSStringFromClass([self class])];
|
|
|
606 |
});
|
|
|
607 |
NSError *error;
|
|
|
608 |
[[NSFileManager defaultManager] createDirectoryAtPath:eventDataPath
|
|
|
609 |
withIntermediateDirectories:YES
|
|
|
610 |
attributes:0
|
|
|
611 |
error:&error];
|
|
|
612 |
GDTCORAssert(error == nil, @"Creating the library data path failed: %@", error);
|
|
|
613 |
return eventDataPath;
|
|
|
614 |
}
|
|
|
615 |
|
|
|
616 |
+ (NSString *)batchDataStoragePath {
|
|
|
617 |
static NSString *batchDataPath;
|
|
|
618 |
static dispatch_once_t onceToken;
|
|
|
619 |
dispatch_once(&onceToken, ^{
|
|
|
620 |
batchDataPath = [NSString stringWithFormat:@"%@/%@/gdt_batch_data", GDTCORRootDirectory().path,
|
|
|
621 |
NSStringFromClass([self class])];
|
|
|
622 |
});
|
|
|
623 |
NSError *error;
|
|
|
624 |
[[NSFileManager defaultManager] createDirectoryAtPath:batchDataPath
|
|
|
625 |
withIntermediateDirectories:YES
|
|
|
626 |
attributes:0
|
|
|
627 |
error:&error];
|
|
|
628 |
GDTCORAssert(error == nil, @"Creating the batch data path failed: %@", error);
|
|
|
629 |
return batchDataPath;
|
|
|
630 |
}
|
|
|
631 |
|
|
|
632 |
+ (NSString *)libraryDataStoragePath {
|
|
|
633 |
static NSString *libraryDataPath;
|
|
|
634 |
static dispatch_once_t onceToken;
|
|
|
635 |
dispatch_once(&onceToken, ^{
|
|
|
636 |
libraryDataPath =
|
|
|
637 |
[NSString stringWithFormat:@"%@/%@/gdt_library_data", GDTCORRootDirectory().path,
|
|
|
638 |
NSStringFromClass([self class])];
|
|
|
639 |
});
|
|
|
640 |
NSError *error;
|
|
|
641 |
[[NSFileManager defaultManager] createDirectoryAtPath:libraryDataPath
|
|
|
642 |
withIntermediateDirectories:YES
|
|
|
643 |
attributes:0
|
|
|
644 |
error:&error];
|
|
|
645 |
GDTCORAssert(error == nil, @"Creating the library data path failed: %@", error);
|
|
|
646 |
return libraryDataPath;
|
|
|
647 |
}
|
|
|
648 |
|
|
|
649 |
+ (NSString *)batchPathForTarget:(GDTCORTarget)target
|
|
|
650 |
batchID:(NSNumber *)batchID
|
|
|
651 |
expirationDate:(NSDate *)expirationDate {
|
|
|
652 |
return
|
|
|
653 |
[NSString stringWithFormat:@"%@/%ld%@%@%@%llu", [GDTCORFlatFileStorage batchDataStoragePath],
|
|
|
654 |
(long)target, kMetadataSeparator, batchID, kMetadataSeparator,
|
|
|
655 |
((uint64_t)expirationDate.timeIntervalSince1970)];
|
|
|
656 |
}
|
|
|
657 |
|
|
|
658 |
+ (NSString *)pathForTarget:(GDTCORTarget)target
|
|
|
659 |
eventID:(NSString *)eventID
|
|
|
660 |
qosTier:(NSNumber *)qosTier
|
|
|
661 |
expirationDate:(NSDate *)expirationDate
|
|
|
662 |
mappingID:(NSString *)mappingID {
|
|
|
663 |
NSMutableCharacterSet *allowedChars = [[NSCharacterSet alphanumericCharacterSet] mutableCopy];
|
|
|
664 |
[allowedChars addCharactersInString:kMetadataSeparator];
|
|
|
665 |
mappingID = [mappingID stringByAddingPercentEncodingWithAllowedCharacters:allowedChars];
|
|
|
666 |
return [NSString stringWithFormat:@"%@/%ld/%@%@%@%@%llu%@%@",
|
|
|
667 |
[GDTCORFlatFileStorage eventDataStoragePath], (long)target,
|
|
|
668 |
eventID, kMetadataSeparator, qosTier, kMetadataSeparator,
|
|
|
669 |
((uint64_t)expirationDate.timeIntervalSince1970),
|
|
|
670 |
kMetadataSeparator, mappingID];
|
|
|
671 |
}
|
|
|
672 |
|
|
|
673 |
- (void)pathsForTarget:(GDTCORTarget)target
|
|
|
674 |
eventIDs:(nullable NSSet<NSString *> *)eventIDs
|
|
|
675 |
qosTiers:(nullable NSSet<NSNumber *> *)qosTiers
|
|
|
676 |
mappingIDs:(nullable NSSet<NSString *> *)mappingIDs
|
|
|
677 |
onComplete:(void (^)(NSSet<NSString *> *paths))onComplete {
|
|
|
678 |
void (^completion)(NSSet<NSString *> *) = onComplete == nil ? ^(NSSet<NSString *> *paths){} : onComplete;
|
|
|
679 |
dispatch_async(_storageQueue, ^{
|
|
|
680 |
NSMutableSet<NSString *> *paths = [[NSMutableSet alloc] init];
|
|
|
681 |
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
682 |
NSString *targetPath = [NSString
|
|
|
683 |
stringWithFormat:@"%@/%ld", [GDTCORFlatFileStorage eventDataStoragePath], (long)target];
|
|
|
684 |
[fileManager createDirectoryAtPath:targetPath
|
|
|
685 |
withIntermediateDirectories:YES
|
|
|
686 |
attributes:nil
|
|
|
687 |
error:nil];
|
|
|
688 |
NSError *error;
|
|
|
689 |
NSArray<NSString *> *dirPaths = [fileManager contentsOfDirectoryAtPath:targetPath error:&error];
|
|
|
690 |
if (error) {
|
|
|
691 |
GDTCORLogDebug(@"There was an error reading the contents of the target path: %@", error);
|
|
|
692 |
completion(paths);
|
|
|
693 |
return;
|
|
|
694 |
}
|
|
|
695 |
BOOL checkingIDs = eventIDs.count > 0;
|
|
|
696 |
BOOL checkingQosTiers = qosTiers.count > 0;
|
|
|
697 |
BOOL checkingMappingIDs = mappingIDs.count > 0;
|
|
|
698 |
BOOL checkingAnything = checkingIDs == NO && checkingQosTiers == NO && checkingMappingIDs == NO;
|
|
|
699 |
for (NSString *path in dirPaths) {
|
|
|
700 |
// Skip hidden files that are created as part of atomic file creation.
|
|
|
701 |
if ([path hasPrefix:@"."]) {
|
|
|
702 |
continue;
|
|
|
703 |
}
|
|
|
704 |
NSString *filePath = [targetPath stringByAppendingPathComponent:path];
|
|
|
705 |
if (checkingAnything) {
|
|
|
706 |
[paths addObject:filePath];
|
|
|
707 |
continue;
|
|
|
708 |
}
|
|
|
709 |
NSString *filename = [path lastPathComponent];
|
|
|
710 |
NSDictionary<NSString *, id> *eventComponents = [self eventComponentsFromFilename:filename];
|
|
|
711 |
if (!eventComponents) {
|
|
|
712 |
GDTCORLogDebug(@"There was an error reading the filename components: %@", eventComponents);
|
|
|
713 |
continue;
|
|
|
714 |
}
|
|
|
715 |
NSString *eventID = eventComponents[kGDTCOREventComponentsEventIDKey];
|
|
|
716 |
NSNumber *qosTier = eventComponents[kGDTCOREventComponentsQoSTierKey];
|
|
|
717 |
NSString *mappingID = eventComponents[kGDTCOREventComponentsMappingIDKey];
|
|
|
718 |
|
|
|
719 |
NSNumber *eventIDMatch = checkingIDs ? @([eventIDs containsObject:eventID]) : nil;
|
|
|
720 |
NSNumber *qosTierMatch = checkingQosTiers ? @([qosTiers containsObject:qosTier]) : nil;
|
|
|
721 |
NSNumber *mappingIDMatch =
|
|
|
722 |
checkingMappingIDs
|
|
|
723 |
? @([mappingIDs containsObject:[mappingID stringByRemovingPercentEncoding]])
|
|
|
724 |
: nil;
|
|
|
725 |
if ((eventIDMatch == nil || eventIDMatch.boolValue) &&
|
|
|
726 |
(qosTierMatch == nil || qosTierMatch.boolValue) &&
|
|
|
727 |
(mappingIDMatch == nil || mappingIDMatch.boolValue)) {
|
|
|
728 |
[paths addObject:filePath];
|
|
|
729 |
}
|
|
|
730 |
}
|
|
|
731 |
completion(paths);
|
|
|
732 |
});
|
|
|
733 |
}
|
|
|
734 |
|
|
|
735 |
- (void)nextBatchID:(void (^)(NSNumber *_Nullable batchID))nextBatchID {
|
|
|
736 |
__block int32_t lastBatchID = -1;
|
|
|
737 |
[self libraryDataForKey:gBatchIDCounterKey
|
|
|
738 |
onFetchComplete:^(NSData *_Nullable data, NSError *_Nullable getValueError) {
|
|
|
739 |
if (!getValueError) {
|
|
|
740 |
[data getBytes:(void *)&lastBatchID length:sizeof(int32_t)];
|
|
|
741 |
}
|
|
|
742 |
if (data == nil) {
|
|
|
743 |
lastBatchID = 0;
|
|
|
744 |
}
|
|
|
745 |
if (nextBatchID) {
|
|
|
746 |
nextBatchID(@(lastBatchID));
|
|
|
747 |
}
|
|
|
748 |
}
|
|
|
749 |
setNewValue:^NSData *_Nullable(void) {
|
|
|
750 |
if (lastBatchID != -1) {
|
|
|
751 |
int32_t incrementedValue = lastBatchID + 1;
|
|
|
752 |
return [NSData dataWithBytes:&incrementedValue length:sizeof(int32_t)];
|
|
|
753 |
}
|
|
|
754 |
return nil;
|
|
|
755 |
}];
|
|
|
756 |
}
|
|
|
757 |
|
|
|
758 |
- (nullable NSDictionary<NSString *, id> *)eventComponentsFromFilename:(NSString *)fileName {
|
|
|
759 |
NSArray<NSString *> *components = [fileName componentsSeparatedByString:kMetadataSeparator];
|
|
|
760 |
if (components.count >= 4) {
|
|
|
761 |
NSString *eventID = components[0];
|
|
|
762 |
NSNumber *qosTier = @(components[1].integerValue);
|
|
|
763 |
NSDate *expirationDate = [NSDate dateWithTimeIntervalSince1970:components[2].longLongValue];
|
|
|
764 |
NSString *mappingID = [[components subarrayWithRange:NSMakeRange(3, components.count - 3)]
|
|
|
765 |
componentsJoinedByString:kMetadataSeparator];
|
|
|
766 |
if (eventID == nil || qosTier == nil || mappingID == nil || expirationDate == nil) {
|
|
|
767 |
GDTCORLogDebug(@"There was an error parsing the event filename components: %@", components);
|
|
|
768 |
return nil;
|
|
|
769 |
}
|
|
|
770 |
return @{
|
|
|
771 |
kGDTCOREventComponentsEventIDKey : eventID,
|
|
|
772 |
kGDTCOREventComponentsQoSTierKey : qosTier,
|
|
|
773 |
kGDTCOREventComponentsExpirationKey : expirationDate,
|
|
|
774 |
kGDTCOREventComponentsMappingIDKey : mappingID
|
|
|
775 |
};
|
|
|
776 |
}
|
|
|
777 |
GDTCORLogDebug(@"The event filename could not be split: %@", fileName);
|
|
|
778 |
return nil;
|
|
|
779 |
}
|
|
|
780 |
|
|
|
781 |
- (nullable NSDictionary<NSString *, id> *)batchComponentsFromFilename:(NSString *)fileName {
|
|
|
782 |
NSArray<NSString *> *components = [fileName componentsSeparatedByString:kMetadataSeparator];
|
|
|
783 |
if (components.count == 3) {
|
|
|
784 |
NSNumber *target = @(components[0].integerValue);
|
|
|
785 |
NSNumber *batchID = @(components[1].integerValue);
|
|
|
786 |
NSDate *expirationDate = [NSDate dateWithTimeIntervalSince1970:components[2].doubleValue];
|
|
|
787 |
if (target == nil || batchID == nil || expirationDate == nil) {
|
|
|
788 |
GDTCORLogDebug(@"There was an error parsing the batch filename components: %@", components);
|
|
|
789 |
return nil;
|
|
|
790 |
}
|
|
|
791 |
return @{
|
|
|
792 |
kGDTCORBatchComponentsTargetKey : target,
|
|
|
793 |
kGDTCORBatchComponentsBatchIDKey : batchID,
|
|
|
794 |
kGDTCORBatchComponentsExpirationKey : expirationDate
|
|
|
795 |
};
|
|
|
796 |
}
|
|
|
797 |
GDTCORLogDebug(@"The batch filename could not be split: %@", fileName);
|
|
|
798 |
return nil;
|
|
|
799 |
}
|
|
|
800 |
|
|
|
801 |
#pragma mark - GDTCORLifecycleProtocol
|
|
|
802 |
|
|
|
803 |
- (void)appWillBackground:(GDTCORApplication *)app {
|
|
|
804 |
dispatch_async(_storageQueue, ^{
|
|
|
805 |
// Immediately request a background task to run until the end of the current queue of work,
|
|
|
806 |
// and cancel it once the work is done.
|
|
|
807 |
__block GDTCORBackgroundIdentifier bgID =
|
|
|
808 |
[app beginBackgroundTaskWithName:@"GDTStorage"
|
|
|
809 |
expirationHandler:^{
|
|
|
810 |
[app endBackgroundTask:bgID];
|
|
|
811 |
bgID = GDTCORBackgroundIdentifierInvalid;
|
|
|
812 |
}];
|
|
|
813 |
// End the background task if it's still valid.
|
|
|
814 |
[app endBackgroundTask:bgID];
|
|
|
815 |
bgID = GDTCORBackgroundIdentifierInvalid;
|
|
|
816 |
});
|
|
|
817 |
}
|
|
|
818 |
|
|
|
819 |
- (void)appWillTerminate:(GDTCORApplication *)application {
|
|
|
820 |
dispatch_sync(_storageQueue, ^{
|
|
|
821 |
});
|
|
|
822 |
}
|
|
|
823 |
|
|
|
824 |
@end
|
|
|
825 |
|
|
|
826 |
NS_ASSUME_NONNULL_END
|