GDTCORFlatFileStorage.m 33 KB

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