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

// FREEBIE
pull/1/head
Michael Kirk 8 years ago
parent 2858694ee0
commit 0f9a3334c1

@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */
308D7DFA789594CEA62740D9 /* libPods-TSKitiOSTestAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DC1A83C39CBC09FB2405A3 /* libPods-TSKitiOSTestAppTests.a */; };
452EE6CF1D4A754C00E934BA /* TSThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 452EE6CE1D4A754C00E934BA /* TSThreadTest.m */; };
452EE6D51D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 452EE6D41D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m */; };
45458B751CC342B600A02153 /* SignedPreKeyDeletionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45458B6A1CC342B600A02153 /* SignedPreKeyDeletionTests.m */; };
45458B761CC342B600A02153 /* TSAttachmentsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45458B6C1CC342B600A02153 /* TSAttachmentsTest.m */; };
45458B771CC342B600A02153 /* TSMessageStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45458B6E1CC342B600A02153 /* TSMessageStorageTests.m */; };
@ -42,6 +44,8 @@
1A50A62A8930EE2BC9B8AC11 /* Pods-TSKitiOSTestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestApp.release.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestApp/Pods-TSKitiOSTestApp.release.xcconfig"; sourceTree = "<group>"; };
31DFDA8F9523F5B15EA2376B /* Pods-TSKitiOSTestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestApp/Pods-TSKitiOSTestApp.debug.xcconfig"; sourceTree = "<group>"; };
36DA6C703F99948D553F4E3F /* Pods-TSKitiOSTestAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestAppTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestAppTests/Pods-TSKitiOSTestAppTests.debug.xcconfig"; sourceTree = "<group>"; };
452EE6CE1D4A754C00E934BA /* TSThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSThreadTest.m; sourceTree = "<group>"; };
452EE6D41D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanedDataCleanerTest.m; sourceTree = "<group>"; };
45458B6A1CC342B600A02153 /* SignedPreKeyDeletionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignedPreKeyDeletionTests.m; sourceTree = "<group>"; };
45458B6C1CC342B600A02153 /* TSAttachmentsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttachmentsTest.m; sourceTree = "<group>"; };
45458B6E1CC342B600A02153 /* TSMessageStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSMessageStorageTests.m; sourceTree = "<group>"; };
@ -51,6 +55,7 @@
45458B731CC342B600A02153 /* CryptographyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CryptographyTests.m; sourceTree = "<group>"; };
45458B741CC342B600A02153 /* MessagePaddingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MessagePaddingTests.m; sourceTree = "<group>"; };
459850C01D22C6F2006FFEDB /* PhoneNumberTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PhoneNumberTest.m; path = ../../../tests/Contacts/PhoneNumberTest.m; sourceTree = "<group>"; };
459FE0DA1D4AD49E00E1071A /* TSKitiOSTestApp-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TSKitiOSTestApp-Prefix.pch"; sourceTree = "<group>"; };
45A856AB1D220BFF0056CD4D /* TSAttributesTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttributesTest.m; sourceTree = "<group>"; };
45C6A0991D2F029B007D8AC0 /* TSMessageTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessageTest.m; path = ../../../tests/Messages/Interactions/TSMessageTest.m; sourceTree = "<group>"; };
B6273DD11C13A2E500738558 /* TSKitiOSTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TSKitiOSTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -116,6 +121,7 @@
45458B6F1CC342B600A02153 /* TSStorageIdentityKeyStoreTests.m */,
45458B701CC342B600A02153 /* TSStoragePreKeyStoreTests.m */,
45458B711CC342B600A02153 /* TSStorageSignedPreKeyStore.m */,
452EE6D41D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m */,
);
name = Storage;
path = ../../../tests/Storage;
@ -135,6 +141,7 @@
isa = PBXGroup;
children = (
459850C01D22C6F2006FFEDB /* PhoneNumberTest.m */,
452EE6CE1D4A754C00E934BA /* TSThreadTest.m */,
);
name = Contacts;
sourceTree = "<group>";
@ -207,6 +214,7 @@
B6273DE21C13A2E500738558 /* LaunchScreen.storyboard */,
B6273DE51C13A2E500738558 /* Info.plist */,
B6273DD41C13A2E500738558 /* Supporting Files */,
459FE0DA1D4AD49E00E1071A /* TSKitiOSTestApp-Prefix.pch */,
);
path = TSKitiOSTestApp;
sourceTree = "<group>";
@ -446,6 +454,8 @@
45458B751CC342B600A02153 /* SignedPreKeyDeletionTests.m in Sources */,
45458B7B1CC342B600A02153 /* CryptographyTests.m in Sources */,
45458B791CC342B600A02153 /* TSStoragePreKeyStoreTests.m in Sources */,
452EE6D51D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m in Sources */,
452EE6CF1D4A754C00E934BA /* TSThreadTest.m in Sources */,
45458B761CC342B600A02153 /* TSAttachmentsTest.m in Sources */,
45C6A09A1D2F029B007D8AC0 /* TSMessageTest.m in Sources */,
459850C11D22C6F2006FFEDB /* PhoneNumberTest.m in Sources */,
@ -597,6 +607,8 @@
baseConfigurationReference = 36DA6C703F99948D553F4E3F /* Pods-TSKitiOSTestAppTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "$(SRCROOT)/$(PROJECT_NAME)/TSKitiOSTestApp-Prefix.pch";
INFOPLIST_FILE = TSKitiOSTestAppTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.whispersystems.TSKitiOSTestAppTests;
@ -610,6 +622,8 @@
baseConfigurationReference = D3737F7A041D7147015C02C2 /* Pods-TSKitiOSTestAppTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "$(SRCROOT)/$(PROJECT_NAME)/TSKitiOSTestApp-Prefix.pch";
INFOPLIST_FILE = TSKitiOSTestAppTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.whispersystems.TSKitiOSTestAppTests;

@ -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

@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = "SignalServiceKit"
s.version = "0.0.6"
s.version = "0.0.7"
s.summary = "An Objective-C library for communicating with the Signal messaging service."
s.description = <<-DESC

@ -1,15 +1,8 @@
//
// TSThread.h
// TextSecureKit
//
// Created by Frederic Jacobs on 16/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "TSYapDatabaseObject.h"
#import <UIKit/UIKit.h>
@class TSInteraction;
@ -42,7 +35,12 @@
- (UIImage *)image;
#endif
#pragma mark Read Status
#pragma mark Interactions
/**
* @return The number of interactions in this thread.
*/
- (NSUInteger)numberOfInteractions;
/**
* Returns whether or not the thread has unread messages.
@ -53,8 +51,6 @@
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark Last Interactions
/**
* Returns the latest date of a message in the thread or the thread creation date if there are no messages in that
*thread.

@ -1,10 +1,5 @@
//
// TSThread.m
// TextSecureKit
//
// Created by Frederic Jacobs on 16/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "TSThread.h"
#import "TSDatabaseView.h"
@ -43,6 +38,33 @@
return self;
}
- (void)remove
{
[[self dbConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self removeWithTransaction:transaction];
}];
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];
__block NSMutableArray<NSString *> *interactionIds = [[NSMutableArray alloc] init];
[self enumerateInteractionsWithTransaction:transaction
usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *transaction) {
[interactionIds addObject:interaction.uniqueId];
}];
for (NSString *interactionId in interactionIds) {
// This might seem redundant since we're fetching the interaction twice, once above to get the uniqueIds
// and then again here. The issue is we can't remove them within the enumeration (you can't mutate an
// enumeration source), but we also want to avoid instantiating an entire threads worth of Interaction objects
// at once. This way we only have a threads worth of interactionId's.
TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
[interaction removeWithTransaction:transaction];
}
}
#pragma mark To be subclassed.
- (BOOL)isGroupThread {
@ -59,7 +81,39 @@
return nil;
}
#pragma mark Read Status
#pragma mark Interactions
/**
* Iterate over this thread's interactions
*/
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
usingBlock:(void (^)(TSInteraction *interaction,
YapDatabaseReadTransaction *transaction))block
{
void (^interactionBlock)(NSString *, NSString *, id, id, NSUInteger, BOOL *) = ^void(NSString *_Nonnull collection,
NSString *_Nonnull key,
id _Nonnull object,
id _Nonnull metadata,
NSUInteger index,
BOOL *_Nonnull stop) {
TSInteraction *interaction = object;
block(interaction, transaction);
};
YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
[interactionsByThread enumerateRowsInGroup:self.uniqueId usingBlock:interactionBlock];
}
- (NSUInteger)numberOfInteractions
{
__block NSUInteger count;
[[self dbConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
count = [interactionsByThread numberOfItemsInGroup:self.uniqueId];
}];
return count;
}
- (BOOL)hasUnreadMessages {
TSInteraction *interaction = self.lastInteraction;
@ -88,8 +142,6 @@
}
}
#pragma mark Last Interactions
- (TSInteraction *) lastInteraction {
__block TSInteraction *last;
[TSStorageManager.sharedManager.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){

@ -6,7 +6,7 @@
#import <UIKit/UIKit.h>
#endif
@interface TSAttachmentStream : TSAttachment <YapDatabaseRelationshipNode>
@interface TSAttachmentStream : TSAttachment
@property (nonatomic) BOOL isDownloaded;
@ -22,9 +22,11 @@
- (BOOL)isAnimated;
- (BOOL)isImage;
- (BOOL)isVideo;
- (NSString *)filePath;
- (NSURL *)mediaURL;
+ (void)deleteAttachments;
+ (NSString *)attachmentsFolder;
+ (NSUInteger)numberOfItemsInAttachmentsFolder;
@end

@ -6,8 +6,6 @@
#import <AVFoundation/AVFoundation.h>
#import <YapDatabase/YapDatabaseTransaction.h>
NSString *const TSAttachementFileRelationshipEdge = @"TSAttachementFileEdge";
@implementation TSAttachmentStream
- (instancetype)initWithIdentifier:(NSString *)identifier
@ -20,6 +18,7 @@ NSString *const TSAttachementFileRelationshipEdge = @"TSAttachementFileEdge";
return self;
}
// TODO move this to save?
[[NSFileManager defaultManager] createFileAtPath:[self filePath] contents:data attributes:nil];
DDLogInfo(@"Created file at %@", [self filePath]);
_isDownloaded = YES;
@ -27,13 +26,20 @@ NSString *const TSAttachementFileRelationshipEdge = @"TSAttachementFileEdge";
return self;
}
- (NSArray *)yapDatabaseRelationshipEdges {
YapDatabaseRelationshipEdge *attachmentFileEdge =
[YapDatabaseRelationshipEdge edgeWithName:TSAttachementFileRelationshipEdge
destinationFileURL:[self mediaURL]
nodeDeleteRules:YDB_DeleteDestinationIfSourceDeleted];
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];
[self removeFile];
}
return @[ attachmentFileEdge ];
- (void)removeFile
{
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:[self filePath] error:&error];
if (error) {
DDLogError(@"remove file errored with: %@", error);
}
}
+ (NSString *)attachmentsFolder
@ -54,7 +60,21 @@ NSString *const TSAttachementFileRelationshipEdge = @"TSAttachementFileEdge";
return attachmentFolder;
}
- (NSString *)filePath {
+ (NSUInteger)numberOfItemsInAttachmentsFolder
{
NSError *error;
NSUInteger count =
[[[NSFileManager defaultManager] contentsOfDirectoryAtPath:[self attachmentsFolder] error:&error] count];
if (error) {
DDLogError(@"Unable to count attachments in attachments folder. Error: %@", error);
}
return count;
}
- (NSString *)filePath
{
return [MIMETypeUtil filePathForAttachment:self.uniqueId
ofMIMEType:self.contentType
inFolder:[[self class] attachmentsFolder]];

@ -5,11 +5,7 @@
@class TSThread;
extern const struct TSMessageRelationships { __unsafe_unretained NSString *threadUniqueId; } TSMessageRelationships;
extern const struct TSMessageEdges { __unsafe_unretained NSString *thread; } TSMessageEdges;
@interface TSInteraction : TSYapDatabaseObject <YapDatabaseRelationshipNode>
@interface TSInteraction : TSYapDatabaseObject
- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread;

@ -1,25 +1,11 @@
//
// TSInteraction.m
// TextSecureKit
//
// Created by Frederic Jacobs on 12/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "TSInteraction.h"
#import "TSDatabaseSecondaryIndexes.h"
#import "TSStorageManager+messageIDs.h"
#import "TSThread.h"
const struct TSMessageRelationships TSMessageRelationships = {
.threadUniqueId = @"threadUniqueId",
};
const struct TSMessageEdges TSMessageEdges = {
.thread = @"thread",
};
@implementation TSInteraction
- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread {
@ -56,23 +42,6 @@ const struct TSMessageEdges TSMessageEdges = {
return interaction;
}
#pragma mark YapDatabaseRelationshipNode
- (NSArray *)yapDatabaseRelationshipEdges {
NSArray *edges = nil;
if (self.uniqueThreadId) {
YapDatabaseRelationshipEdge *threadEdge =
[YapDatabaseRelationshipEdge edgeWithName:TSMessageEdges.thread
destinationKey:self.uniqueThreadId
collection:[TSThread collection]
nodeDeleteRules:YDB_DeleteSourceIfDestinationDeleted];
edges = @[ threadEdge ];
}
return edges;
}
+ (NSString *)collection {
return @"TSInteraction";
}

@ -5,8 +5,6 @@
#import "TSAttachment.h"
#import <YapDatabase/YapDatabaseTransaction.h>
NSString *const TSAttachementsRelationshipEdgeName = @"TSAttachmentEdge";
@implementation TSMessage
- (void)addattachments:(NSArray *)attachments {
@ -23,22 +21,6 @@ NSString *const TSAttachementsRelationshipEdgeName = @"TSAttachmentEdge";
[self.attachments addObject:attachment];
}
- (NSArray *)yapDatabaseRelationshipEdges {
NSMutableArray *edges = [[super yapDatabaseRelationshipEdges] mutableCopy];
if ([self hasAttachments]) {
for (NSString *attachmentId in self.attachments) {
YapDatabaseRelationshipEdge *fileEdge =
[[YapDatabaseRelationshipEdge alloc] initWithName:TSAttachementsRelationshipEdgeName
destinationKey:attachmentId
collection:[TSAttachment collection]
nodeDeleteRules:YDB_DeleteDestinationIfAllSourcesDeleted];
[edges addObject:fileEdge];
}
}
return edges;
}
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(TSThread *)thread
messageBody:(NSString *)body
@ -82,13 +64,14 @@ NSString *const TSAttachementsRelationshipEdgeName = @"TSAttachmentEdge";
}
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction {
for (NSString *attachmentId in _attachments) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
[attachment removeWithTransaction:transaction];
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];
[self.attachments
enumerateObjectsUsingBlock:^(NSString *_Nonnull attachmentId, NSUInteger idx, BOOL *_Nonnull stop) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
[attachment removeWithTransaction:transaction];
}];
}
@end

@ -1,7 +1,6 @@
// Created by Frederic Jacobs on 17/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
#import "TSMessagesManager+sendMessages.h"
#import "ContactsUpdater.h"
#import "NSData+messagePadding.h"
#import "PreKeyBundle+jsonDict.h"
@ -10,6 +9,7 @@
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSInfoMessage.h"
#import "TSMessagesManager+sendMessages.h"
#import "TSNetworkManager.h"
#import "TSServerMessage.h"
#import "TSStorageHeaders.h"

@ -2,8 +2,6 @@
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
#import "TSMessagesManager.h"
#import <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/SessionCipher.h>
#import "NSData+messagePadding.h"
#import "TSAccountManager.h"
#import "TSAttachmentStream.h"
@ -17,6 +15,8 @@
#import "TSMessagesManager+attachments.h"
#import "TSStorageHeaders.h"
#import "TextSecureKitEnv.h"
#import <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/SessionCipher.h>
@interface TSMessagesManager ()

@ -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

@ -60,8 +60,6 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[TSDatabaseView registerUnreadDatabaseView];
[self.database registerExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"];
[self.database registerExtension:[[YapDatabaseRelationship alloc] init] withName:@"TSRelationships"];
}

@ -3,6 +3,7 @@
#import <Mantle/MTLModel+NSCoding.h>
@class YapDatabaseConnection;
@class YapDatabaseReadTransaction;
@class YapDatabaseReadWriteTransaction;
@ -15,8 +16,7 @@
*
* @return Initialized object
*/
- (instancetype)initWithUniqueId:(NSString *)uniqueId;
- (instancetype)initWithUniqueId:(NSString *)uniqueId NS_DESIGNATED_INITIALIZER;
/**
* Returns the collection to which the object belongs.
@ -25,6 +25,41 @@
*/
+ (NSString *)collection;
/**
* Get the number of keys in the models collection. Be aware that if there
* are multiple object types in this collection that the count will include
* the count of other objects in the same collection.
*
* @return The number of keys in the classes collection.
*/
+ (NSUInteger)numberOfKeysInCollection;
/**
* Removes all objects in the classes collection.
*/
+ (void)removeAllObjectsInCollection;
/**
* A memory intesive method to get all objects in the collection. You should prefer using enumeration over this method
* whenever feasible. See `enumerateObjectsInCollectionUsingBlock`
*
* @return All objects in the classes collection.
*/
+ (NSArray *)allObjectsInCollection;
/**
* Enumerates all objects in collection.
*/
+ (void)enumerateCollectionObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block;
+ (void)enumerateCollectionObjectsWithTransaction:(YapDatabaseReadTransaction *)transaction
usingBlock:(void (^)(id object, BOOL *stop))block;
/**
* @return A shared database connection.
*/
- (YapDatabaseConnection *)dbConnection;
+ (YapDatabaseConnection *)dbConnection;
/**
* Fetches the object with the provided identifier

@ -7,61 +7,119 @@
@implementation TSYapDatabaseObject
- (id)init {
if (self = [super init]) {
_uniqueId = [[NSUUID UUID] UUIDString];
}
return self;
- (instancetype)init
{
return [self initWithUniqueId:[[NSUUID UUID] UUIDString]];
}
- (instancetype)initWithUniqueId:(NSString *)aUniqueId {
if (self = [super init]) {
_uniqueId = aUniqueId;
- (instancetype)initWithUniqueId:(NSString *)aUniqueId
{
self = [super init];
if (!self) {
return self;
}
_uniqueId = aUniqueId;
return self;
}
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction {
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[transaction setObject:self forKey:self.uniqueId inCollection:[[self class] collection]];
}
- (void)save {
[[TSStorageManager sharedManager]
.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self saveWithTransaction:transaction];
- (void)save
{
[[self dbConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self saveWithTransaction:transaction];
}];
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction {
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[transaction removeObjectForKey:self.uniqueId inCollection:[[self class] collection]];
}
- (void)remove {
[[TSStorageManager sharedManager]
.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self removeWithTransaction:transaction];
[[transaction ext:@"relationships"] flush];
- (void)remove
{
[[self dbConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self removeWithTransaction:transaction];
}];
}
- (YapDatabaseConnection *)dbConnection
{
return [[self class] dbConnection];
}
#pragma mark Class Methods
+ (NSString *)collection {
+ (YapDatabaseConnection *)dbConnection
{
return [TSStorageManager sharedManager].dbConnection;
}
+ (NSString *)collection
{
return NSStringFromClass([self class]);
}
+ (instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID transaction:(YapDatabaseReadTransaction *)transaction {
return [transaction objectForKey:uniqueID inCollection:[self collection]];
+ (NSUInteger)numberOfKeysInCollection
{
__block NSUInteger count;
[[self dbConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) {
count = [transaction numberOfKeysInCollection:[self collection]];
}];
return count;
}
+ (instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID {
__block id object;
+ (NSArray *)allObjectsInCollection
{
__block NSMutableArray *all = [[NSMutableArray alloc] initWithCapacity:[self numberOfKeysInCollection]];
[self enumerateCollectionObjectsUsingBlock:^(id object, BOOL *stop) {
[all addObject:object];
}];
return [all copy];
}
[[TSStorageManager sharedManager]
.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
object = [transaction objectForKey:uniqueID inCollection:[self collection]];
+ (void)enumerateCollectionObjectsUsingBlock:(void (^)(id object, BOOL *stop))block
{
[[self dbConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self enumerateCollectionObjectsWithTransaction:transaction usingBlock:block];
}];
}
+ (void)enumerateCollectionObjectsWithTransaction:(YapDatabaseReadTransaction *)transaction
usingBlock:(void (^)(id object, BOOL *stop))block
{
// Ignoring most of the YapDB parameters, and just passing through the ones we usually use.
void (^yapBlock)(NSString *key, id object, id metadata, BOOL *stop)
= ^void(NSString *key, id object, id metadata, BOOL *stop) {
block(object, stop);
};
[transaction enumerateRowsInCollection:[self collection] usingBlock:yapBlock];
}
+ (void)removeAllObjectsInCollection
{
[[self dbConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction removeAllObjectsInCollection:[self collection]];
}];
}
+ (instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID transaction:(YapDatabaseReadTransaction *)transaction
{
return [transaction objectForKey:uniqueID inCollection:[self collection]];
}
+ (instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID
{
__block id object;
[[self dbConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) {
object = [transaction objectForKey:uniqueID inCollection:[self collection]];
}];
return object;
}

@ -1,8 +1,8 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "TSAttachment.h"
#import "TSMessage.h"
#import "TSThread.h"
#import "TSAttachment.h"
#import <XCTest/XCTest.h>

@ -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…
Cancel
Save