GDTCORFlatFileStorage.m 33 KB

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