GDTCORFlatFileStorage.m 33 KB

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