1// 2// ETCoreMLAssetManager.mm 3// 4// Copyright © 2024 Apple Inc. All rights reserved. 5// 6// Please refer to the license found in the LICENSE file in the root directory of the source tree. 7 8#import "ETCoreMLAssetManager.h" 9#import <ETCoreMLAsset.h> 10#import <ETCoreMLLogging.h> 11#import <database.hpp> 12#import <iostream> 13#import <json_key_value_store.hpp> 14#import <serde_json.h> 15#import <sstream> 16 17namespace { 18 19using namespace executorchcoreml; 20using namespace executorchcoreml::sqlite; 21 22constexpr size_t kBusyTimeIntervalInMS = 100; 23 24constexpr std::string_view kModelAssetsStoreName = "MODEL_ASSETS_STORE"; 25constexpr std::string_view kModelAssetsMetaStoreName = "MODEL_ASSETS_STORE_META"; 26 27class ModelAssetsStore { 28public: 29 using StoreType = JSONKeyValueStore<std::string, Asset>; 30 31 ModelAssetsStore(std::unique_ptr<StoreType> impl) noexcept 32 :impl_(std::move(impl)) 33 {} 34 35 ModelAssetsStore() noexcept 36 :impl_(nullptr) 37 {} 38 39 ModelAssetsStore(const ModelAssetsStore &) = delete; 40 ModelAssetsStore &operator=(const ModelAssetsStore &) = delete; 41 42 ModelAssetsStore& operator=(ModelAssetsStore&& rhs) noexcept { 43 rhs.impl_.swap(impl_); 44 return *this; 45 } 46 47 ModelAssetsStore(ModelAssetsStore&& rhs) noexcept 48 :impl_(std::move(rhs.impl_)) 49 {} 50 51 inline StoreType *impl() { 52 return impl_.get(); 53 } 54 55private: 56 std::unique_ptr<StoreType> impl_; 57}; 58 59class ModelAssetsMetaStore { 60public: 61 using StoreType = KeyValueStore<std::string, size_t>; 62 63 ModelAssetsMetaStore() noexcept 64 :impl_(nullptr) 65 {} 66 67 ModelAssetsMetaStore(std::unique_ptr<StoreType> impl) noexcept 68 :impl_(std::move(impl)) 69 {} 70 71 ModelAssetsMetaStore(const ModelAssetsMetaStore &) = delete; 72 ModelAssetsMetaStore &operator=(const ModelAssetsMetaStore &) = delete; 73 74 ModelAssetsMetaStore& operator=(ModelAssetsMetaStore&& rhs) noexcept { 75 rhs.impl_.swap(impl_); 76 return *this; 77 } 78 79 ModelAssetsMetaStore(ModelAssetsMetaStore&& rhs) noexcept 80 :impl_(std::move(rhs.impl_)) 81 {} 82 83 inline StoreType *impl() { 84 return impl_.get(); 85 } 86 87private: 88 std::unique_ptr<StoreType> impl_; 89}; 90 91void set_error_from_error_code(const std::error_code& cppError, NSError * __autoreleasing *error) { 92 if (!error || !cppError) { 93 return; 94 } 95 96 NSString *message = @(cppError.message().c_str()); 97 NSString *domain = @(cppError.category().name()); 98 NSInteger code = cppError.value(); 99 NSError *localError = [NSError errorWithDomain:domain code:code userInfo:@{NSLocalizedDescriptionKey : message}]; 100 *error = localError; 101} 102 103std::shared_ptr<Database> make_database(NSURL *database_url, 104 NSTimeInterval busy_time_interval, 105 NSError * __autoreleasing *error) { 106 Database::OpenOptions options; 107 options.set_read_write_option(true); 108 options.set_create_option(true); 109 110 std::error_code ec; 111 auto database = Database::make(database_url.path.UTF8String, 112 options, 113 Database::SynchronousMode::Normal, 114 busy_time_interval, 115 ec); 116 if (!database) { 117 ::set_error_from_error_code(ec, error); 118 return nullptr; 119 } 120 121 return database; 122} 123 124ModelAssetsStore make_assets_store(const std::shared_ptr<Database>& database, 125 NSError * __autoreleasing *error) { 126 std::error_code ec; 127 auto store = ModelAssetsStore::StoreType::make(std::move(database), std::string(kModelAssetsStoreName), ec); 128 if (!store) { 129 ::set_error_from_error_code(ec, error); 130 return ModelAssetsStore(nullptr); 131 } 132 133 return ModelAssetsStore(std::move(store)); 134} 135 136ModelAssetsMetaStore make_assets_meta_store(const std::shared_ptr<Database>& database, 137 NSError * __autoreleasing *error) { 138 std::error_code ec; 139 auto store = ModelAssetsMetaStore::StoreType::make(std::move(database), std::string(kModelAssetsMetaStoreName), ec); 140 if (!store) { 141 ::set_error_from_error_code(ec, error); 142 return ModelAssetsMetaStore(nullptr); 143 } 144 145 return ModelAssetsMetaStore(std::move(store)); 146} 147 148std::optional<size_t> get_total_assets_size(ModelAssetsMetaStore& store, 149 std::error_code& ec) { 150 std::string name = std::string(kModelAssetsStoreName); 151 auto total_size = store.impl()->get(name, ec); 152 153 if (!total_size && !store.impl()->put(name, size_t(0), ec)) { 154 return std::nullopt; 155 } 156 157 size_t result = total_size.has_value() ? total_size.value() : size_t(0); 158 return result; 159} 160 161bool set_total_assets_size(size_t total_size, 162 ModelAssetsMetaStore& store, 163 std::error_code& ec) { 164 if (!store.impl()->put(std::string(kModelAssetsStoreName), total_size, ec)) { 165 return false; 166 } 167 168 return true; 169} 170 171bool exclude_item_from_backup(NSURL *url, NSError * __autoreleasing *error) { 172 return [url setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:error]; 173} 174 175NSURL * _Nullable create_directory_if_needed(NSURL *url, 176 NSString *name, 177 NSFileManager *fm, 178 NSError * __autoreleasing *error) { 179 NSURL *directory_url = [url URLByAppendingPathComponent:name]; 180 if (![fm fileExistsAtPath:directory_url.path] && 181 ![fm createDirectoryAtURL:directory_url withIntermediateDirectories:NO attributes:@{} error:error]) { 182 return nil; 183 } 184 185 ::exclude_item_from_backup(directory_url, nil); 186 187 return directory_url; 188} 189 190bool is_directory_empty(NSURL *url, NSFileManager *fm, NSError * __autoreleasing *error) { 191 BOOL is_directory = NO; 192 if (![fm fileExistsAtPath:url.path isDirectory:&is_directory] && !is_directory) { 193 return true; 194 } 195 196 __block NSError *local_error = nil; 197 BOOL (^errorHandler)(NSURL *url, NSError *error) = ^BOOL(NSURL *url, NSError *enumeration_error) { 198 local_error = enumeration_error; 199 return NO; 200 }; 201 202 NSDirectoryEnumerator *enumerator = [fm enumeratorAtURL:url 203 includingPropertiesForKeys:@[] 204 options:NSDirectoryEnumerationProducesRelativePathURLs 205 errorHandler:errorHandler]; 206 if (local_error && error) { 207 *error = local_error; 208 } 209 210 return [enumerator nextObject] == nil; 211} 212 213NSURL * _Nullable get_asset_url(const Asset& asset) { 214 return [NSURL fileURLWithPath:@(asset.path.c_str())]; 215} 216 217BOOL is_asset_alive(NSMapTable<NSString *, ETCoreMLAsset *> *assets_in_use_map, NSString *identifier) { 218 ETCoreMLAsset *asset = [assets_in_use_map objectForKey:identifier]; 219 return asset && asset.isAlive; 220} 221 222std::vector<executorchcoreml::Asset> 223get_assets_to_remove(ModelAssetsStore& store, 224 ssize_t bytes_to_remove, 225 NSMapTable<NSString *, ETCoreMLAsset *> *assets_in_use_map, 226 std::error_code &error) { 227 std::vector<Asset> assets; 228 store.impl()->get_keys_sorted_by_access_count([store = store.impl(), 229 &bytes_to_remove, 230 &assets, 231 assets_in_use_map, 232 &error](const std::string& key) { 233 if (bytes_to_remove <= 0) { 234 return false; 235 } 236 237 NSString *identifier = @(key.c_str()); 238 // Asset is in use, we can't remove it 239 if (::is_asset_alive(assets_in_use_map, identifier)) { 240 return true; 241 } 242 243 auto asset = store->get(key, error); 244 if (asset) { 245 auto& asset_value = asset.value(); 246 bytes_to_remove -= static_cast<ssize_t>(asset_value.total_size_in_bytes()); 247 assets.emplace_back(std::move(asset_value)); 248 } 249 250 return true; 251 }, SortOrder::Ascending, error); 252 253 return assets; 254} 255} //namespace 256 257@interface ETCoreMLAssetManager () <NSFileManagerDelegate> { 258 ModelAssetsStore _assetsStore; 259 ModelAssetsMetaStore _assetsMetaStore; 260} 261 262@property (assign, readwrite, atomic) NSInteger estimatedSizeInBytes; 263@property (copy, readonly, nonatomic) NSURL *assetsDirectoryURL; 264@property (strong, readonly, nonatomic) dispatch_queue_t syncQueue; 265@property (strong, readonly, nonatomic) dispatch_queue_t trashQueue; 266@property (strong, readonly, nonatomic) NSMapTable<NSString *, ETCoreMLAsset *> *assetsInUseMap; 267 268@end 269 270@implementation ETCoreMLAssetManager 271 272- (nullable instancetype)initWithDatabase:(const std::shared_ptr<Database>&)database 273 assetsDirectoryURL:(NSURL *)assetsDirectoryURL 274 trashDirectoryURL:(NSURL *)trashDirectoryURL 275 maxAssetsSizeInBytes:(NSInteger)maxAssetsSizeInBytes 276 error:(NSError * __autoreleasing *)error { 277 278 auto assetsStore = ::make_assets_store(database, error); 279 if (assetsStore.impl() == nullptr) { 280 return nil; 281 } 282 283 auto assetsMetaStore = ::make_assets_meta_store(database, error); 284 if (assetsMetaStore.impl() == nullptr) { 285 return nil; 286 } 287 288 std::error_code ec; 289 auto sizeInBytes = ::get_total_assets_size(assetsMetaStore, ec); 290 if (!sizeInBytes) { 291 ::set_error_from_error_code(ec, error); 292 return nil; 293 } 294 295 NSFileManager *fileManager = [[NSFileManager alloc] init]; 296 NSURL *managedAssetsDirectoryURL = ::create_directory_if_needed(assetsDirectoryURL, @"models", fileManager, error); 297 if (!managedAssetsDirectoryURL) { 298 return nil; 299 } 300 301 NSURL *managedTrashDirectoryURL = ::create_directory_if_needed(trashDirectoryURL, @"models", fileManager, error); 302 if (!managedTrashDirectoryURL) { 303 return nil; 304 } 305 306 // If directory is empty then purge the stores 307 if (::is_directory_empty(managedAssetsDirectoryURL, fileManager, nil)) { 308 assetsMetaStore.impl()->purge(ec); 309 assetsStore.impl()->purge(ec); 310 } 311 312 if (self = [super init]) { 313 _assetsStore = std::move(assetsStore); 314 _assetsMetaStore = std::move(assetsMetaStore); 315 _assetsDirectoryURL = managedAssetsDirectoryURL; 316 _trashDirectoryURL = managedTrashDirectoryURL; 317 _estimatedSizeInBytes = sizeInBytes.value(); 318 _maxAssetsSizeInBytes = maxAssetsSizeInBytes; 319 320 _fileManager = fileManager; 321 _trashQueue = dispatch_queue_create("com.executorchcoreml.assetmanager.trash", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); 322 _syncQueue = dispatch_queue_create("com.executorchcoreml.assetmanager.sync", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); 323 _assetsInUseMap = [NSMapTable strongToWeakObjectsMapTable]; 324 } 325 326 [self triggerCompaction]; 327 return self; 328} 329 330- (nullable instancetype)initWithDatabaseURL:(NSURL *)databaseURL 331 assetsDirectoryURL:(NSURL *)assetsDirectoryURL 332 trashDirectoryURL:(NSURL *)trashDirectoryURL 333 maxAssetsSizeInBytes:(NSInteger)maxAssetsSizeInBytes 334 error:(NSError * __autoreleasing *)error { 335 auto database = make_database(databaseURL, kBusyTimeIntervalInMS, error); 336 if (!database) { 337 return nil; 338 } 339 340 return [self initWithDatabase:database 341 assetsDirectoryURL:assetsDirectoryURL 342 trashDirectoryURL:trashDirectoryURL 343 maxAssetsSizeInBytes:maxAssetsSizeInBytes 344 error:error]; 345} 346 347- (nullable NSURL *)moveURL:(NSURL *)url 348 toUniqueURLInDirectory:(NSURL *)directoryURL 349 error:(NSError * __autoreleasing *)error { 350 NSURL *dstURL = [directoryURL URLByAppendingPathComponent:[NSUUID UUID].UUIDString]; 351 if (![self.fileManager moveItemAtURL:url toURL:dstURL error:error]) { 352 return nil; 353 } 354 355 return dstURL; 356} 357 358- (void)cleanupAssetIfNeeded:(ETCoreMLAsset *)asset { 359 if (!asset || asset.isValid) { 360 return; 361 } 362 363 NSString *identifier = asset.identifier; 364 dispatch_async(self.syncQueue, ^{ 365 NSError *cleanupError = nil; 366 if (![self _removeAssetWithIdentifier:asset.identifier error:&cleanupError]) { 367 ETCoreMLLogError(cleanupError, 368 "%@: Failed to remove asset with identifier = %@", 369 NSStringFromClass(ETCoreMLAssetManager.class), 370 identifier); 371 } 372 }); 373} 374 375- (nullable ETCoreMLAsset *)_storeAssetAtURL:(NSURL *)srcURL 376 withIdentifier:(NSString *)identifier 377 error:(NSError * __autoreleasing *)error { 378 dispatch_assert_queue(self.syncQueue); 379 NSString *extension = srcURL.lastPathComponent.pathExtension; 380 NSURL *dstURL = [self.assetsDirectoryURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", identifier, extension]]; 381 auto asset = Asset::make(srcURL, identifier, self.fileManager, error); 382 if (!asset) { 383 return nil; 384 } 385 386 auto& assetValue = asset.value(); 387 size_t assetSizeInBytes = assetValue.total_size_in_bytes(); 388 std::error_code ec; 389 // Update the stores inside a transaction, if anything fails it will automatically rollback to the previous state. 390 bool status = _assetsStore.impl()->transaction([self, &assetValue, assetSizeInBytes, srcURL, dstURL, &ec, error]() { 391 const std::string& assetIdentifier = assetValue.identifier; 392 // If an asset exists with the same identifier then remove it. 393 if (![self _removeAssetWithIdentifier:@(assetIdentifier.c_str()) error:error]) { 394 return false; 395 } 396 397 // Update asset path. 398 assetValue.path = dstURL.path.UTF8String; 399 // Store the asset. 400 if (!_assetsStore.impl()->put(assetIdentifier, assetValue, ec)) { 401 return false; 402 } 403 404 // Update the size of the store. 405 if (!::set_total_assets_size(_estimatedSizeInBytes + assetSizeInBytes, _assetsMetaStore, ec)) { 406 return false; 407 } 408 409 // If an asset exists move it 410 [self moveURL:dstURL toUniqueURLInDirectory:self.trashDirectoryURL error:nil]; 411 412 // Move the asset to assets directory. 413 if (![self.fileManager moveItemAtURL:srcURL toURL:dstURL error:error]) { 414 return false; 415 } 416 417 return true; 418 }, Database::TransactionBehavior::Immediate, ec); 419 420 // Update the estimated size if the transaction succeeded. 421 _estimatedSizeInBytes += status ? assetSizeInBytes : 0; 422 ::set_error_from_error_code(ec, error); 423 424 ETCoreMLAsset *result = status ? [[ETCoreMLAsset alloc] initWithBackingAsset:assetValue] : nil; 425 if ([result keepAliveAndReturnError:error]) { 426 [self.assetsInUseMap setObject:result forKey:identifier]; 427 } else { 428 [self cleanupAssetIfNeeded:result]; 429 } 430 431 return result; 432} 433 434- (void)triggerCompaction { 435 if (self.estimatedSizeInBytes < self.maxAssetsSizeInBytes) { 436 return; 437 } 438 439 __weak __typeof(self) weakSelf = self; 440 dispatch_async(self.syncQueue, ^{ 441 NSError *localError = nil; 442 if (![weakSelf _compact:self.maxAssetsSizeInBytes error:&localError]) { 443 ETCoreMLLogError(localError, 444 "%@: Failed to compact asset store.", 445 NSStringFromClass(ETCoreMLAssetManager.class)); 446 } 447 }); 448} 449 450- (nullable ETCoreMLAsset *)storeAssetAtURL:(NSURL *)url 451 withIdentifier:(NSString *)identifier 452 error:(NSError * __autoreleasing *)error { 453 __block ETCoreMLAsset *result = nil; 454 dispatch_sync(self.syncQueue, ^{ 455 result = [self _storeAssetAtURL:url withIdentifier:identifier error:error]; 456 }); 457 458 [self triggerCompaction]; 459 return result; 460} 461 462- (nullable ETCoreMLAsset *)_assetWithIdentifier:(NSString *)identifier 463 error:(NSError * __autoreleasing *)error { 464 dispatch_assert_queue(self.syncQueue); 465 std::string assetIdentifier(identifier.UTF8String); 466 std::error_code ec; 467 auto asset = _assetsStore.impl()->get(assetIdentifier, ec); 468 if (!asset) { 469 ::set_error_from_error_code(ec, error); 470 return nil; 471 } 472 473 const auto& assetValue = asset.value(); 474 ETCoreMLAsset *modelAsset = [[ETCoreMLAsset alloc] initWithBackingAsset:assetValue]; 475 [self.assetsInUseMap setObject:modelAsset forKey:identifier]; 476 477 return modelAsset; 478} 479 480- (nullable ETCoreMLAsset *)assetWithIdentifier:(NSString *)identifier 481 error:(NSError * __autoreleasing *)error { 482 __block ETCoreMLAsset *result = nil; 483 dispatch_sync(self.syncQueue, ^{ 484 result = [self _assetWithIdentifier:identifier error:error]; 485 }); 486 487 if ([result keepAliveAndReturnError:error]) { 488 [self.assetsInUseMap setObject:result forKey:identifier]; 489 } else { 490 [self cleanupAssetIfNeeded:result]; 491 } 492 493 return result; 494} 495 496- (BOOL)_containsAssetWithIdentifier:(NSString *)identifier 497 error:(NSError * __autoreleasing *)error { 498 dispatch_assert_queue(self.syncQueue); 499 std::error_code ec; 500 BOOL result = static_cast<BOOL>(_assetsStore.impl()->exists(std::string(identifier.UTF8String), ec)); 501 ::set_error_from_error_code(ec, error); 502 503 return result; 504} 505 506- (BOOL)hasAssetWithIdentifier:(NSString *)identifier 507 error:(NSError * __autoreleasing *)error { 508 __block BOOL result = NO; 509 dispatch_sync(self.syncQueue, ^{ 510 result = [self _containsAssetWithIdentifier:identifier error:error]; 511 }); 512 513 return result; 514} 515 516- (BOOL)_removeAssetWithIdentifier:(NSString *)identifier 517 error:(NSError * __autoreleasing *)error { 518 dispatch_assert_queue(self.syncQueue); 519 // Asset is alive we can't delete it. 520 if (is_asset_alive(self.assetsInUseMap, identifier)) { 521 return NO; 522 } 523 524 std::error_code ec; 525 std::string assetIdentifier(identifier.UTF8String); 526 auto asset = _assetsStore.impl()->get(assetIdentifier, ec); 527 // If it's an error then we can't proceed. 528 if (ec) { 529 ::set_error_from_error_code(ec, error); 530 return NO; 531 } 532 533 // Asset doesn't exists, we are good. 534 if (!asset) { 535 return YES; 536 } 537 538 const auto& assetValue = asset.value(); 539 size_t assetSizeInBytes = std::min(_estimatedSizeInBytes, static_cast<NSInteger>(assetValue.total_size_in_bytes())); 540 // Update the stores inside a transaction, if anything fails it will automatically rollback to the previous state. 541 bool status = _assetsStore.impl()->transaction([self, &assetValue, assetSizeInBytes, &ec, error]() { 542 if (!self->_assetsStore.impl()->remove(assetValue.identifier, ec)) { 543 return false; 544 } 545 546 if (!::set_total_assets_size(_estimatedSizeInBytes - assetSizeInBytes, _assetsMetaStore, ec)) { 547 return false; 548 } 549 550 NSURL *assetURL = ::get_asset_url(assetValue); 551 if ([self.fileManager fileExistsAtPath:assetURL.path] && 552 ![self moveURL:assetURL toUniqueURLInDirectory:self.trashDirectoryURL error:error]) { 553 return false; 554 } 555 556 return true; 557 }, Database::TransactionBehavior::Immediate, ec); 558 559 // Update the estimated size if the transaction succeeded. 560 _estimatedSizeInBytes -= status ? assetSizeInBytes : 0; 561 ::set_error_from_error_code(ec, error); 562 return static_cast<BOOL>(status); 563} 564 565- (BOOL)removeAssetWithIdentifier:(NSString *)identifier 566 error:(NSError * __autoreleasing *)error { 567 __block BOOL result = NO; 568 dispatch_sync(self.syncQueue, ^{ 569 result = [self _removeAssetWithIdentifier:identifier error:error]; 570 }); 571 572 return result; 573} 574 575- (nullable NSArray<ETCoreMLAsset *> *)_recentlyUsedAssetsWithMaxCount:(NSUInteger)maxCount 576 error:(NSError * __autoreleasing *)error { 577 dispatch_assert_queue(self.syncQueue); 578 579 NSMutableArray<ETCoreMLAsset *> *assets = [NSMutableArray arrayWithCapacity:maxCount]; 580 std::error_code ec; 581 bool status = _assetsStore.impl()->get_keys_sorted_by_access_time([self, maxCount, assets](const std::string& key) { 582 NSError *localError = nil; 583 NSString *identifier = @(key.c_str()); 584 ETCoreMLAsset *asset = [self _assetWithIdentifier:identifier error:&localError]; 585 586 if (asset) { 587 [assets addObject:asset]; 588 } else if (localError) { 589 ETCoreMLLogError(localError, 590 "%@: Failed to retrieve asset with identifier = %@", 591 NSStringFromClass(ETCoreMLAssetManager.class), 592 identifier); 593 } 594 595 return assets.count < maxCount; 596 }, SortOrder::Descending, ec); 597 598 ::set_error_from_error_code(ec, error); 599 return status ? assets : nil; 600} 601 602- (nullable NSArray<ETCoreMLAsset *> *)mostRecentlyUsedAssetsWithMaxCount:(NSUInteger)maxCount 603 error:(NSError * __autoreleasing *)error { 604 __block NSArray<ETCoreMLAsset *> *result = nil; 605 dispatch_sync(self.syncQueue, ^{ 606 result = [self _recentlyUsedAssetsWithMaxCount:maxCount error:error]; 607 }); 608 609 return result; 610} 611 612- (BOOL)_canPurgeStore { 613 dispatch_assert_queue(self.syncQueue); 614 615 NSEnumerator *keyEnumerator = self.assetsInUseMap.keyEnumerator; 616 for (NSString *identifier in keyEnumerator) { 617 if (is_asset_alive(self.assetsInUseMap, identifier)) { 618 return NO; 619 } 620 } 621 622 return YES; 623} 624 625- (NSUInteger)_compact:(NSUInteger)sizeInBytes error:(NSError * __autoreleasing *)error { 626 dispatch_assert_queue(self.syncQueue); 627 628 if (sizeInBytes == 0 && [self _canPurgeStore]) { 629 return [self _purge:error] ? 0 : _estimatedSizeInBytes; 630 } 631 632 if (_estimatedSizeInBytes <= sizeInBytes) { 633 return _estimatedSizeInBytes; 634 } 635 636 std::error_code ec; 637 ssize_t bytesToRemove = _estimatedSizeInBytes - sizeInBytes; 638 const auto& assets = ::get_assets_to_remove(_assetsStore, bytesToRemove, self.assetsInUseMap, ec); 639 640 if (ec) { 641 ::set_error_from_error_code(ec, error); 642 return _estimatedSizeInBytes; 643 } 644 645 for (const auto& asset : assets) { 646 NSError *cleanupError = nil; 647 NSString *identifier = @(asset.identifier.c_str()); 648 if (![self _removeAssetWithIdentifier:identifier error:&cleanupError] && cleanupError) { 649 ETCoreMLLogError(cleanupError, 650 "%@: Failed to remove asset with identifier = %@", 651 NSStringFromClass(ETCoreMLAssetManager.class), 652 identifier); 653 } 654 } 655 656 // Trigger cleanup. 657 __weak __typeof(self) weakSelf = self; 658 dispatch_async(self.trashQueue, ^{ 659 [weakSelf removeFilesInTrashDirectory]; 660 }); 661 662 return _estimatedSizeInBytes; 663} 664 665- (NSUInteger)compact:(NSUInteger)sizeInBytes error:(NSError * __autoreleasing *)error { 666 __block NSUInteger result = 0; 667 dispatch_sync(self.syncQueue, ^{ 668 result = [self _compact:sizeInBytes error:error]; 669 }); 670 671 return result; 672} 673 674- (void)removeFilesInTrashDirectory { 675 dispatch_assert_queue(self.trashQueue); 676 677 NSFileManager *fileManager = [[NSFileManager alloc] init]; 678 fileManager.delegate = self; 679 __block NSError *localError = nil; 680 BOOL (^errorHandler)(NSURL *url, NSError *error) = ^BOOL(NSURL *url, NSError *enumerationError) { 681 localError = enumerationError; 682 return YES; 683 }; 684 685 NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:self.trashDirectoryURL 686 includingPropertiesForKeys:@[] 687 options:NSDirectoryEnumerationSkipsSubdirectoryDescendants 688 errorHandler:errorHandler]; 689 for (NSURL *itemURL in enumerator) { 690 if (![fileManager removeItemAtURL:itemURL error:&localError]) { 691 ETCoreMLLogError(localError, 692 "%@: Failed to remove item in trash directory with name = %@", 693 NSStringFromClass(ETCoreMLAssetManager.class), 694 itemURL.lastPathComponent); 695 } 696 } 697} 698 699- (BOOL)_purge:(NSError * __autoreleasing *)error { 700 dispatch_assert_queue(self.syncQueue); 701 702 std::error_code ec; 703 bool status = _assetsStore.impl()->transaction([self, &ec, error]() { 704 // Purge the assets store. 705 if (!self->_assetsStore.impl()->purge(ec)) { 706 return false; 707 } 708 709 // Purge the assets size store. 710 if (!self->_assetsMetaStore.impl()->purge(ec)) { 711 return false; 712 } 713 714 // Move the the whole assets directory to the temp directory. 715 if (![self moveURL:self.assetsDirectoryURL toUniqueURLInDirectory:self.trashDirectoryURL error:error]) { 716 return false; 717 } 718 719 self->_estimatedSizeInBytes = 0; 720 NSError *localError = nil; 721 // Create the assets directory, if we fail here it's okay. 722 if (![self.fileManager createDirectoryAtURL:self.assetsDirectoryURL withIntermediateDirectories:NO attributes:@{} error:&localError]) { 723 ETCoreMLLogError(localError, 724 "%@: Failed to create assets directory", 725 NSStringFromClass(ETCoreMLAssetManager.class)); 726 } 727 728 return true; 729 }, Database::TransactionBehavior::Immediate, ec); 730 731 ::set_error_from_error_code(ec, error); 732 // Trigger cleanup 733 if (status) { 734 __weak __typeof(self) weakSelf = self; 735 dispatch_async(self.trashQueue, ^{ 736 [weakSelf removeFilesInTrashDirectory]; 737 }); 738 } 739 740 return static_cast<BOOL>(status); 741} 742 743- (BOOL)purgeAndReturnError:(NSError * __autoreleasing *)error { 744 __block BOOL result = 0; 745 dispatch_sync(self.syncQueue, ^{ 746 result = [self _purge:error]; 747 }); 748 749 return result; 750} 751 752- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtURL:(NSURL *)URL { 753 return YES; 754} 755 756@end 757