Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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