mirror of https://github.com/oxen-io/session-ios
Ensure interactions removed when thread is deleted
In theory, this should have already been handled by the YapDatabaseRelationship extension via edges. However, in practice, there were situations (cause unknown) where interactions would exist without an edge to their corresponding thread. Rather than being clever with the edge/callback machinery, now threads explicitly delete all their interactions, and interactions delete all their attachments (when applicable). Also, a class to clean up spurious interactions / attachments In the process: - refactored TSYapDatabaseObject init to specify designated initializer - added some testing niceties to TSYapDatabaseObject // FREEBIEpull/1/head
parent
2858694ee0
commit
0f9a3334c1
@ -0,0 +1,8 @@
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
#ifndef TSKitiOSTestApp_Prefix_pch
|
||||
#define TSKitiOSTestApp_Prefix_pch
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#endif /* TSKitiOSTestApp_Prefix_pch */
|
@ -0,0 +1,91 @@
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSContactThread.h"
|
||||
#import "TSIncomingMessage.h"
|
||||
#import "TSOutgoingMessage.h"
|
||||
#import "TSStorageManager.h"
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
@interface TSThreadTest : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation TSThreadTest
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
|
||||
// Register views, etc.
|
||||
[[TSStorageManager sharedManager] setupDatabase];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)testDeletingThreadDeletesInteractions
|
||||
{
|
||||
TSContactThread *thread = [[TSContactThread alloc] initWithUniqueId:@"fake-test-thread"];
|
||||
[thread save];
|
||||
|
||||
[TSInteraction removeAllObjectsInCollection];
|
||||
XCTAssertEqual(0, [thread numberOfInteractions]);
|
||||
|
||||
TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:10000
|
||||
inThread:thread
|
||||
messageBody:@"incoming message body"
|
||||
attachments:nil];
|
||||
[incomingMessage save];
|
||||
|
||||
TSOutgoingMessage *outgoingMessage = [[TSOutgoingMessage alloc] initWithTimestamp:20000
|
||||
inThread:thread
|
||||
messageBody:@"outgoing message body"
|
||||
attachments:nil];
|
||||
[outgoingMessage save];
|
||||
|
||||
XCTAssertEqual(2, [thread numberOfInteractions]);
|
||||
|
||||
[thread remove];
|
||||
XCTAssertEqual(0, [thread numberOfInteractions]);
|
||||
XCTAssertEqual(0, [TSInteraction numberOfKeysInCollection]);
|
||||
}
|
||||
|
||||
- (void)testDeletingThreadDeletesAttachmentFiles
|
||||
{
|
||||
TSContactThread *thread = [[TSContactThread alloc] initWithUniqueId:@"fake-test-thread"];
|
||||
[thread save];
|
||||
|
||||
// Sanity check
|
||||
[TSInteraction removeAllObjectsInCollection];
|
||||
XCTAssertEqual(0, [thread numberOfInteractions]);
|
||||
|
||||
TSAttachmentStream *attachment = [[TSAttachmentStream alloc] initWithIdentifier:@"fake-photo-attachment-id"
|
||||
data:[[NSData alloc] init]
|
||||
key:[[NSData alloc] init]
|
||||
contentType:@"image/jpeg"];
|
||||
[attachment save];
|
||||
|
||||
BOOL fileWasCreated = [[NSFileManager defaultManager] fileExistsAtPath:[attachment filePath]];
|
||||
XCTAssert(fileWasCreated);
|
||||
|
||||
TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:10000
|
||||
inThread:thread
|
||||
messageBody:@"incoming message body"
|
||||
attachments:@[ attachment.uniqueId ]];
|
||||
[incomingMessage save];
|
||||
|
||||
// Sanity check
|
||||
XCTAssertEqual(1, [thread numberOfInteractions]);
|
||||
|
||||
[thread remove];
|
||||
XCTAssertEqual(0, [thread numberOfInteractions]);
|
||||
|
||||
BOOL fileStillExists = [[NSFileManager defaultManager] fileExistsAtPath:[attachment filePath]];
|
||||
XCTAssertFalse(fileStillExists);
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
@interface OWSOrphanedDataCleaner : NSObject
|
||||
|
||||
/**
|
||||
* Remove any inaccessible data left behind due to application bugs.
|
||||
*/
|
||||
- (void)removeOrphanedData;
|
||||
|
||||
@end
|
@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
#import "OWSOrphanedDataCleaner.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSInteraction.h"
|
||||
#import "TSMessage.h"
|
||||
#import "TSStorageManager.h"
|
||||
#import "TSThread.h"
|
||||
|
||||
@implementation OWSOrphanedDataCleaner
|
||||
|
||||
- (void)removeOrphanedData
|
||||
{
|
||||
// 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];
|
||||
}
|
||||
|
||||
// Remove any lingering attachments
|
||||
for (NSString *path in [self orphanedFilePaths]) {
|
||||
DDLogWarn(@"Removing orphaned file attachment at path: %@", path);
|
||||
NSError *error;
|
||||
[[NSFileManager defaultManager] removeItemAtPath:path error:&error];
|
||||
if (error) {
|
||||
DDLogError(@"Unable to remove orphaned file attachment at path:%@", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)orphanedInteractionIds
|
||||
{
|
||||
NSMutableArray *interactionIds = [NSMutableArray new];
|
||||
[[TSInteraction dbConnection] 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];
|
||||
}
|
||||
}];
|
||||
}];
|
||||
|
||||
|
||||
return [interactionIds copy];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)orphanedFilePaths
|
||||
{
|
||||
NSError *error;
|
||||
NSMutableArray<NSString *> *filenames =
|
||||
[[[NSFileManager defaultManager] contentsOfDirectoryAtPath:[TSAttachmentStream attachmentsFolder] error:&error]
|
||||
mutableCopy];
|
||||
if (error) {
|
||||
DDLogError(@"error getting orphanedFilePaths:%@", error);
|
||||
return @[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
[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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
// TODO Make sure we're not deleting group update avatars
|
||||
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];
|
||||
}
|
||||
|
||||
return [absolutePathsToDelete copy];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,138 @@
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
#import "OWSOrphanedDataCleaner.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSContactThread.h"
|
||||
#import "TSIncomingMessage.h"
|
||||
#import "TSStorageManager.h"
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
@interface OWSOrphanedDataCleanerTest : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSOrphanedDataCleanerTest
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
// Register views, etc.
|
||||
[[TSStorageManager sharedManager] setupDatabase];
|
||||
|
||||
// Set up initial conditions & Sanity check
|
||||
[TSAttachmentStream deleteAttachments];
|
||||
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
[TSAttachmentStream removeAllObjectsInCollection];
|
||||
XCTAssertEqual(0, [TSAttachmentStream numberOfKeysInCollection]);
|
||||
[TSIncomingMessage removeAllObjectsInCollection];
|
||||
XCTAssertEqual(0, [TSIncomingMessage numberOfKeysInCollection]);
|
||||
[TSThread removeAllObjectsInCollection];
|
||||
XCTAssertEqual(0, [TSThread numberOfKeysInCollection]);
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)testInteractionsWithoutThreadAreDeleted
|
||||
{
|
||||
// This thread is intentionally not saved. It's meant to recreate a situation we've seen where interactions exist
|
||||
// that reference the id of a thread that no longer exists. Presumably this is the result of a deleted thread not
|
||||
// properly deleting it's interactions.
|
||||
TSContactThread *unsavedThread = [[TSContactThread alloc] initWithUniqueId:@"this-thread-does-not-exist"];
|
||||
|
||||
TSIncomingMessage *incomingMessage =
|
||||
[[TSIncomingMessage alloc] initWithTimestamp:1 inThread:unsavedThread messageBody:@"footch" attachments:nil];
|
||||
[incomingMessage save];
|
||||
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
|
||||
|
||||
[[OWSOrphanedDataCleaner new] removeOrphanedData];
|
||||
XCTAssertEqual(0, [TSIncomingMessage numberOfKeysInCollection]);
|
||||
}
|
||||
|
||||
- (void)testInteractionsWithThreadAreNotDeleted
|
||||
{
|
||||
TSContactThread *savedThread = [[TSContactThread alloc] initWithUniqueId:@"this-thread-exists"];
|
||||
[savedThread save];
|
||||
|
||||
TSIncomingMessage *incomingMessage =
|
||||
[[TSIncomingMessage alloc] initWithTimestamp:1 inThread:savedThread messageBody:@"footch" attachments:nil];
|
||||
[incomingMessage save];
|
||||
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
|
||||
|
||||
[[OWSOrphanedDataCleaner new] removeOrphanedData];
|
||||
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
|
||||
}
|
||||
|
||||
- (void)testFilesWithoutInteractionsAreDeleted
|
||||
{
|
||||
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithIdentifier:@"orphaned-attachment"
|
||||
data:[NSData new]
|
||||
key:[NSData new]
|
||||
contentType:@"image/jpeg"];
|
||||
|
||||
[attachmentStream save];
|
||||
NSString *orphanedFilePath = [attachmentStream filePath];
|
||||
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
|
||||
XCTAssert(fileExists);
|
||||
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
|
||||
[[OWSOrphanedDataCleaner new] removeOrphanedData];
|
||||
fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
|
||||
XCTAssertFalse(fileExists);
|
||||
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
}
|
||||
|
||||
- (void)testFilesWithInteractionsAreNotDeleted
|
||||
{
|
||||
TSContactThread *savedThread = [[TSContactThread alloc] initWithUniqueId:@"this-thread-exists"];
|
||||
[savedThread save];
|
||||
|
||||
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithIdentifier:@"legit-attachment"
|
||||
data:[NSData new]
|
||||
key:[NSData new]
|
||||
contentType:@"image/jpeg"];
|
||||
[attachmentStream save];
|
||||
|
||||
TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1
|
||||
inThread:savedThread
|
||||
messageBody:@"footch"
|
||||
attachments:@[ attachmentStream.uniqueId ]];
|
||||
[incomingMessage save];
|
||||
|
||||
NSString *attachmentFilePath = [attachmentStream filePath];
|
||||
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFilePath];
|
||||
XCTAssert(fileExists);
|
||||
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
|
||||
[[OWSOrphanedDataCleaner new] removeOrphanedData];
|
||||
|
||||
fileExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFilePath];
|
||||
XCTAssert(fileExists);
|
||||
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
}
|
||||
|
||||
- (void)testFilesWithoutAttachmentStreamsAreDeleted
|
||||
{
|
||||
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithIdentifier:@"orphaned-attachment"
|
||||
data:[NSData new]
|
||||
key:[NSData new]
|
||||
contentType:@"image/jpeg"];
|
||||
|
||||
// Intentionally not saved, because we want a lingering file.
|
||||
// This relies on a bug(?) in the current TSAttachmentStream init implementation where the file is created during
|
||||
// `init` rather than during `save`. If that bug is fixed, we'll have to update this test to manually create the
|
||||
// file to set up the correct initial state.
|
||||
NSString *orphanedFilePath = [attachmentStream filePath];
|
||||
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
|
||||
XCTAssert(fileExists);
|
||||
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
|
||||
[[OWSOrphanedDataCleaner new] removeOrphanedData];
|
||||
fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
|
||||
XCTAssertFalse(fileExists);
|
||||
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
|
||||
}
|
||||
|
||||
@end
|
Loading…
Reference in New Issue