Run orphan cleanup on startup.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent 9115a1f973
commit 96da091e9b

@ -1,10 +1,13 @@
// Copyright (c) 2016 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
@interface OWSOrphanedDataCleaner : NSObject
/**
* Remove any inaccessible data left behind due to application bugs.
*/
- (void)removeOrphanedData;
- (instancetype)init NS_UNAVAILABLE;
+ (void)auditAsync;
+ (void)auditAndCleanupAsync;
@end

@ -11,82 +11,209 @@
@implementation OWSOrphanedDataCleaner
- (void)removeOrphanedData
+ (void)auditAsync
{
// Remove interactions whose threads have been deleted
for (NSString *interactionId in [self orphanedInteractionIds]) {
DDLogWarn(@"Removing orphaned interaction with id: %@", interactionId);
TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId];
[interaction remove];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:NO];
});
}
+ (void)auditAndCleanupAsync
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:YES];
});
}
// Remove any lingering attachments
for (NSString *path in [self orphanedFilePaths]) {
DDLogWarn(@"Removing orphaned file attachment at path: %@", path);
// This method finds and optionally cleans up:
//
// * Orphan messages (with no thread).
// * Orphan attachments (with no message).
// * Orphan attachment files (with no attachment).
// * Missing attachment files (cannot be cleaned up).
+ (void)auditAndCleanup:(BOOL)shouldCleanup
{
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
DDLogDebug(@"attachmentsFolder: %@", attachmentsFolder);
__block int fileCount = 0;
__block long long totalFileSize = 0;
NSMutableSet *diskFilePaths = [NSMutableSet new];
__unsafe_unretained __block void (^visitAttachmentFilesRecursable)(NSString *);
void (^visitAttachmentFiles)(NSString *);
visitAttachmentFiles = ^(NSString *dirPath) {
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:path error:&error];
NSArray<NSString *> *fileNames =
[[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
if (error) {
DDLogError(@"Unable to remove orphaned file attachment at path:%@", path);
OWSFail(@"contentsOfDirectoryAtPath error: %@", error);
return;
}
}
}
for (NSString *fileName in fileNames) {
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
BOOL isDirectory;
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
if (isDirectory) {
visitAttachmentFilesRecursable(filePath);
} else {
NSNumber *fileSize =
[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
if (error) {
OWSFail(@"attributesOfItemAtPath: %@ error: %@", filePath, error);
continue;
}
totalFileSize += fileSize.longLongValue;
fileCount++;
[diskFilePaths addObject:filePath];
}
}
};
visitAttachmentFilesRecursable = visitAttachmentFiles;
visitAttachmentFiles(attachmentsFolder);
- (NSArray<NSString *> *)orphanedInteractionIds
{
NSMutableArray *interactionIds = [NSMutableArray new];
[[TSInteraction dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[TSInteraction enumerateCollectionObjectsWithTransaction:transaction
usingBlock:^(TSInteraction *interaction, BOOL *stop) {
TSThread *thread = [TSThread
fetchObjectWithUniqueID:interaction.uniqueThreadId
transaction:transaction];
if (!thread) {
[interactionIds addObject:interaction.uniqueId];
}
}];
TSStorageManager *storageManager = [TSStorageManager sharedManager];
YapDatabaseConnection *databaseConnection = storageManager.newDatabaseConnection;
__block int attachmentStreamCount = 0;
NSMutableSet<NSString *> *attachmentFilePaths = [NSMutableSet new];
NSMutableSet<NSString *> *attachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
[attachmentIds addObject:attachment.uniqueId];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
return;
}
TSAttachmentStream *attachmentStream
= (TSAttachmentStream *)attachment;
attachmentStreamCount++;
NSString *_Nullable filePath = [attachmentStream filePath];
OWSAssert(filePath);
[attachmentFilePaths addObject:filePath];
}];
}];
DDLogDebug(@"fileCount: %d", fileCount);
DDLogDebug(@"totalFileSize: %lld", totalFileSize);
DDLogDebug(@"attachmentStreams: %d", attachmentStreamCount);
DDLogDebug(@"attachmentStreams with file paths: %zd", attachmentFilePaths.count);
return [interactionIds copy];
}
NSMutableSet<NSString *> *orphanDiskFilePaths = [diskFilePaths mutableCopy];
[orphanDiskFilePaths minusSet:attachmentFilePaths];
NSMutableSet<NSString *> *missingAttachmentFilePaths = [attachmentFilePaths mutableCopy];
[missingAttachmentFilePaths minusSet:diskFilePaths];
- (NSArray<NSString *> *)orphanedFilePaths
{
NSError *error;
NSMutableArray<NSString *> *filenames =
[[[NSFileManager defaultManager] contentsOfDirectoryAtPath:[TSAttachmentStream attachmentsFolder] error:&error]
mutableCopy];
if (error) {
DDLogError(@"error getting orphanedFilePaths:%@", error);
return @[];
}
DDLogDebug(@"orphan disk file paths: %zd", orphanDiskFilePaths.count);
DDLogDebug(@"missing attachment file paths: %zd", missingAttachmentFilePaths.count);
NSMutableDictionary<NSString *, NSString *> *attachmentIdFilenames = [NSMutableDictionary new];
for (NSString *filename in filenames) {
// Remove extension from (e.g.) 1234.png to get the attachmentId "1234"
NSString *attachmentId = [filename stringByDeletingPathExtension];
attachmentIdFilenames[attachmentId] = filename;
}
[self printPaths:orphanDiskFilePaths.allObjects label:@"orphan disk file paths"];
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
NSMutableSet *threadIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysInCollection:TSThread.collection
usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) {
[threadIds addObject:key];
}];
}];
[TSInteraction enumerateCollectionObjectsUsingBlock:^(TSInteraction *interaction, BOOL *stop) {
if ([interaction isKindOfClass:[TSMessage class]]) {
TSMessage *message = (TSMessage *)interaction;
if ([message hasAttachments]) {
for (NSString *attachmentId in message.attachmentIds) {
[attachmentIdFilenames removeObjectForKey:attachmentId];
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
NSMutableSet<NSString *> *messageAttachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysAndObjectsInCollection:TSMessage.collection
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
if (![threadIds containsObject:interaction.uniqueThreadId]) {
[orphanInteractionIds addObject:interaction.uniqueId];
}
if (![interaction isKindOfClass:[TSMessage class]]) {
return;
}
TSMessage *message = (TSMessage *)interaction;
if (message.attachmentIds.count > 0) {
[messageAttachmentIds addObjectsFromArray:message.attachmentIds];
}
}];
}];
DDLogDebug(@"attachmentIds: %zd", attachmentIds.count);
DDLogDebug(@"messageAttachmentIds: %zd", messageAttachmentIds.count);
NSMutableSet<NSString *> *orphanAttachmentIds = [attachmentIds mutableCopy];
[orphanAttachmentIds minusSet:messageAttachmentIds];
NSMutableSet<NSString *> *missingAttachmentIds = [messageAttachmentIds mutableCopy];
[missingAttachmentIds minusSet:attachmentIds];
DDLogDebug(@"orphan attachmentIds: %zd", orphanAttachmentIds.count);
DDLogDebug(@"missing attachmentIds: %zd", missingAttachmentIds.count);
DDLogDebug(@"orphan interactions: %zd", orphanInteractionIds.count);
// We need to avoid cleaning up new attachments and files that are still in the process of
// being created/written, so we don't clean up anything recent.
const NSTimeInterval kMinimumOrphanAge = 15 * 60.f;
if (shouldCleanup) {
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
for (NSString *interactionId in orphanInteractionIds) {
TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId];
if (!interaction) {
// This could just be a race condition, but it should be very unlikely.
OWSFail(@"Could not load interaction: %@", interactionId);
continue;
}
DDLogInfo(@"Removing orphan message: %@", interaction.uniqueId);
[interaction removeWithTransaction:transaction];
}
for (NSString *attachmentId in orphanAttachmentIds) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
if (!attachment) {
// This could just be a race condition, but it should be very unlikely.
OWSFail(@"Could not load attachment: %@", attachmentId);
continue;
}
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
continue;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
// Don't delete attachments which were created in the last N minutes.
if (fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]) < kMinimumOrphanAge) {
DDLogInfo(@"Skipping orphan attachment due to age: %f",
fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]));
continue;
}
DDLogInfo(@"Removing orphan attachment: %@", attachmentStream.uniqueId);
[attachmentStream removeWithTransaction:transaction];
}
}];
for (NSString *filePath in orphanDiskFilePaths) {
NSError *error;
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
if (!attributes || error) {
OWSFail(@"Could not get attributes of file at: %@", filePath);
continue;
}
// Don't delete files which were created in the last N minutes.
if (fabs([attributes.fileModificationDate timeIntervalSinceNow]) < kMinimumOrphanAge) {
DDLogInfo(@"Skipping orphan attachment file due to age: %f",
fabs([attributes.fileModificationDate timeIntervalSinceNow]));
continue;
}
}
}];
NSArray<NSString *> *filenamesToDelete = [attachmentIdFilenames allValues];
NSMutableArray<NSString *> *absolutePathsToDelete = [NSMutableArray arrayWithCapacity:[filenamesToDelete count]];
for (NSString *filename in filenamesToDelete) {
NSString *absolutePath = [[TSAttachmentStream attachmentsFolder] stringByAppendingFormat:@"/%@", filename];
[absolutePathsToDelete addObject:absolutePath];
DDLogInfo(@"Removing orphan attachment file: %@", filePath);
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error) {
OWSFail(@"Could not remove orphan file at: %@", filePath);
}
}
}
}
return [absolutePathsToDelete copy];
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
{
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
DDLogDebug(@"%@: %@", label, path);
}
}
@end

Loading…
Cancel
Save