-
Notifications
You must be signed in to change notification settings - Fork 5
WIP: Enhanced Deletion #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
samdeane
wants to merge
18
commits into
cparnot:master
Choose a base branch
from
elegantchaos:sd-file-deletion
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
7bed92f
Started stubbing tombstone support.
samdeane 44e4d37
Added blob deletion tests
samdeane f5ea6c7
Improved deletion tests
samdeane e148d84
Added constant for tombstone extension
samdeane 4235771
Don't return data and don't enumerate files where a tombstone is pres…
samdeane 53a2a83
Renamed tests.
samdeane 403b36e
comment tweak
samdeane 8c73d69
Added blobExistsAtPath
samdeane 091f510
Honour the usingTombstone flag.
samdeane 98bf01e
Added comments about potential cleanup.
samdeane fdd8bfa
Added a note about double-deletion.
samdeane 3b4343d
Added some comments explaining the tests
samdeane 872b52f
Updated to reflect Drew's comments
samdeane 6276465
Fixed blob enumeration. Improved tests. Added enumeration of tombstones.
samdeane e8f52c4
Pass out both the blob path and the tombstone path when enumerating t…
samdeane 4fc6892
Fixed old variable name.
samdeane 583b037
Fixed enumeration - should be skipping directories.
samdeane edf65a0
Added option to ignore missing files when deleting.
samdeane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,8 @@ | |
| NSString *const TimestampAttributeName = @"timestamp"; | ||
| NSString *const ParentTimestampAttributeName = @"parentTimestamp"; | ||
|
|
||
| // tombstone constants | ||
| NSString *const TombstoneFileExtension = @"deleted"; | ||
|
|
||
| // A subclass of NSFileCoordinator that doesn't use coordination. | ||
| // This is used to disable coordination, for a performance boost when it is not needed. | ||
|
|
@@ -1382,7 +1384,48 @@ - (BOOL)writeBlobFromPath:(NSString *)sourcePath toPath:(NSString *)targetSubpat | |
| return YES; | ||
| } | ||
|
|
||
| - (BOOL)writeTombstoneAtPath:(NSString *)tombstonePath forFileAtPath:(NSString *)filePath error:(NSError **)error | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be clear, although I suggest using "registeredDeletion" in the API, the private methods can stick with tombstone. |
||
| { | ||
| if ([[NSFileManager defaultManager] fileExistsAtPath:tombstonePath]) | ||
| { | ||
| // if a tombstone already exists, we need not make it again | ||
| return YES; | ||
| } | ||
|
|
||
| // the tombstone contains the original modification date and file size, along with the current time | ||
| NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:error]; | ||
| NSDictionary* properties = @{ @"modified": attributes.fileModificationDate, @"size": @(attributes.fileSize), @"deleted": [NSDate date] }; | ||
| return [properties writeToFile:tombstonePath atomically:YES]; | ||
| } | ||
|
|
||
|
|
||
| - (BOOL)blobExistsAtPath:(NSString *)path | ||
| { | ||
| NSURL *blobURL = [[self blobDirectoryURL] URLByAppendingPathComponent:path]; | ||
|
|
||
| // if a tombstone file exists, return NO, regardless of the presence of the actual file | ||
| NSURL* tombstoneURL = [blobURL URLByAppendingPathExtension:TombstoneFileExtension]; | ||
| if ([[NSFileManager defaultManager] fileExistsAtPath:tombstoneURL.path]) | ||
| { | ||
| return NO; | ||
| } | ||
|
|
||
| return [[NSFileManager defaultManager] fileExistsAtPath:blobURL.path]; | ||
| } | ||
|
|
||
| - (BOOL)blobIsRegisteredDeletedAtPath:(NSString *)path | ||
| { | ||
| NSURL *blobURL = [[self blobDirectoryURL] URLByAppendingPathComponent:path]; | ||
| NSURL* tombstoneURL = [blobURL URLByAppendingPathExtension:TombstoneFileExtension]; | ||
| return [[NSFileManager defaultManager] fileExistsAtPath:tombstoneURL.path]; | ||
| } | ||
|
|
||
| - (BOOL)deleteBlobAtPath:(NSString *)path error:(NSError **)error | ||
| { | ||
| return [self deleteBlobAtPath:path registeringDeletion: NO ignoreIfMissing:NO error:error]; | ||
| } | ||
|
|
||
| - (BOOL)deleteBlobAtPath:(NSString *)path registeringDeletion: (BOOL)registeringDeletion ignoreIfMissing: (BOOL)ignoreIfMissing error:(NSError **)error | ||
| { | ||
| // nil path = error | ||
| if (path == nil) | ||
|
|
@@ -1407,24 +1450,53 @@ - (BOOL)deleteBlobAtPath:(NSString *)path error:(NSError **)error | |
| // otherwise blobs are stored in a special blob directory | ||
| __block NSError *localError = nil; | ||
| NSURL *fileURL = [[self blobDirectoryURL] URLByAppendingPathComponent:path]; | ||
| NSURL *tombstoneURL = [fileURL URLByAppendingPathExtension:TombstoneFileExtension]; | ||
|
|
||
| NSError *coordinatorError = nil; | ||
| [[self newFileCoordinator] coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForReplacing error:&coordinatorError byAccessor:^(NSURL *newURL) | ||
|
|
||
| [[self newFileCoordinator] coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForReplacing writingItemAtURL:tombstoneURL options:NSFileCoordinatorWritingForReplacing error:&coordinatorError byAccessor:^(NSURL * _Nonnull newURL, NSURL * _Nonnull newTombstoneURL) | ||
| { | ||
| // write to disk (overwrite any file that was at that same path before) | ||
| NSError *error = nil; | ||
| BOOL success = [[NSFileManager defaultManager] removeItemAtURL:fileURL error:&error]; | ||
| if (!success) | ||
| localError = [NSError errorWithObject:self code:__LINE__ localizedDescription:[NSString stringWithFormat:@"Could not delete data blob at path '%@'", newURL.path] underlyingError:error]; | ||
| }]; | ||
| NSError *error = nil; | ||
| BOOL success = YES; | ||
| BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:newURL.path]; | ||
|
|
||
| if (!(ignoreIfMissing || fileExists)) { | ||
| success = NO; | ||
| localError = [NSError errorWithObject:self code:__LINE__ localizedDescription:[NSString stringWithFormat:@"Could not delete non-existent data blob at path '%@'", newURL.path] underlyingError:nil]; | ||
| } | ||
|
|
||
| if (success && registeringDeletion && fileExists) { | ||
| // write tombstone | ||
| success = [self writeTombstoneAtPath:newTombstoneURL.path forFileAtPath:newURL.path error:&error]; | ||
| if (!success) | ||
| { | ||
| localError = [NSError errorWithObject:self code:__LINE__ localizedDescription:[NSString stringWithFormat:@"Could not create tombstone for data blob at path '%@'", newURL.path] underlyingError:error]; | ||
samdeane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (success && fileExists) | ||
| { | ||
| success = [[NSFileManager defaultManager] removeItemAtURL:fileURL error:&error]; | ||
| if (!success) | ||
| { | ||
| localError = [NSError errorWithObject:self code:__LINE__ localizedDescription:[NSString stringWithFormat:@"Could not delete data blob at path '%@'", newURL.path] underlyingError:error]; | ||
samdeane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| }]; | ||
|
|
||
| // error handling | ||
| if (coordinatorError && !localError) | ||
| { | ||
| localError = coordinatorError; | ||
| } | ||
|
|
||
| if (localError) | ||
| { | ||
| ErrorLog(@"Error deleting blob: %@", localError); | ||
| if (error != NULL) | ||
| { | ||
| *error = localError; | ||
| } | ||
| return NO; | ||
| } | ||
| return YES; | ||
|
|
@@ -1456,6 +1528,26 @@ - (NSData *)blobDataAtPath:(NSString *)path error:(NSError **)error; | |
| // otherwise blobs are stored in a special blob directory | ||
| __block NSError *localError = nil; | ||
| NSURL *fileURL = [[self blobDirectoryURL] URLByAppendingPathComponent:path]; | ||
|
|
||
| // check first for a tombstone, which indicates that the | ||
| // blob had been deleted and should be ignored | ||
| NSURL *tombstoneURL = [fileURL URLByAppendingPathExtension:TombstoneFileExtension]; | ||
| if ([[NSFileManager defaultManager] fileExistsAtPath: tombstoneURL.path]) { | ||
| NSString *description = [NSString stringWithFormat:@"Blob at path '%@' has a tombstone, indicating that it has been deleted.", self.storeURL.path]; | ||
| ErrorLog(@"%@", description); | ||
| if (error != NULL) | ||
| { | ||
| *error = [NSError errorWithObject:self code:__LINE__ localizedDescription:description underlyingError:nil]; | ||
samdeane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // we attempt to clean up sync errors here, by deleting the data file if it still exists | ||
| // most times this call will fail because the file won't exist, and if it fails for another | ||
| // reason there's probably nothing we can do, so we ignore any errors | ||
| [[NSFileManager defaultManager] removeItemAtURL:fileURL error:NULL]; | ||
|
|
||
| return nil; | ||
| } | ||
|
|
||
| NSError *coordinatorError = nil; | ||
| __block NSData *data = nil; | ||
| [[self newFileCoordinator] coordinateReadingItemAtURL:fileURL options:NSFileCoordinatorReadingWithoutChanges error:&coordinatorError byAccessor:^(NSURL *newURL) | ||
|
|
@@ -1509,31 +1601,79 @@ - (void)enumerateBlobs:(void(^)(NSString *path))block | |
| } | ||
| else | ||
| { | ||
| __block NSArray *urls; | ||
| __block NSMutableArray *urls = [NSMutableArray array]; | ||
| __block NSMutableSet *tombstones = [NSMutableSet set]; | ||
| [[self newFileCoordinator] coordinateReadingItemAtURL:[self blobDirectoryURL] options:NSFileCoordinatorReadingWithoutChanges error:NULL byAccessor:^(NSURL *newURL) | ||
| { | ||
| NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[self blobDirectoryURL] includingPropertiesForKeys:nil options:(NSDirectoryEnumerationSkipsPackageDescendants|NSDirectoryEnumerationSkipsHiddenFiles|NSDirectoryEnumerationSkipsSubdirectoryDescendants) errorHandler:nil]; | ||
| NSFileManager *fileManager = [[NSFileManager alloc] init]; | ||
| urls = [enumerator.allObjects filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { | ||
| NSURL *url = object; | ||
| for(NSURL* url in enumerator) { | ||
| BOOL isDir = false; | ||
| return [fileManager fileExistsAtPath:url.path isDirectory:&isDir] && !isDir; | ||
| }]]; | ||
| if ([fileManager fileExistsAtPath:url.path isDirectory:&isDir] && isDir) continue; | ||
| if ([url.pathExtension isEqualToString: TombstoneFileExtension]) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Close one :) |
||
| [tombstones addObject:[url URLByDeletingPathExtension]]; | ||
| } else { | ||
| [urls addObject:url]; | ||
| } | ||
| } | ||
| }]; | ||
|
|
||
| NSUInteger prefixLength = self.blobDirectoryURL.path.length+1; // +1 is for the last slash | ||
| for (NSURL *url in urls) { | ||
| if ([tombstones containsObject:url]) { | ||
| // a tombstone file exists for this data file, indicating that it has been deleted | ||
| // we skip it for the enumeration, and we attempt to clean up by deleting the data file | ||
| // (we ignore deletion errors as this method doesn't return success/failure, and the | ||
| // underlying cause of the mismatch is probably something that needs fixing by a higher | ||
| // layer anyway) | ||
| [[NSFileManager defaultManager] removeItemAtURL:url error:NULL]; | ||
| } else { | ||
| // Resolving symbolic link here, because on iOS at least, the directory enumerator | ||
| // uses a sym linked "private" folder, causing the path to be different to what comes | ||
| // out for the blobDirectoryURL. | ||
| NSString *absolutePath = [url URLByResolvingSymlinksInPath].path; | ||
| NSString *relativePath = [absolutePath substringFromIndex:prefixLength]; | ||
| block(relativePath); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| - (void)enumerateDeletedBlobs:(void(^)(NSString *blobPath, NSString *markerPath))block | ||
| { | ||
| if (!self._inMemory) | ||
| { | ||
| __block NSMutableSet *tombstones = [NSMutableSet set]; | ||
| [[self newFileCoordinator] coordinateReadingItemAtURL:[self blobDirectoryURL] options:NSFileCoordinatorReadingWithoutChanges error:NULL byAccessor:^(NSURL *newURL) | ||
| { | ||
| NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[self blobDirectoryURL] includingPropertiesForKeys:nil options:(NSDirectoryEnumerationSkipsPackageDescendants|NSDirectoryEnumerationSkipsHiddenFiles|NSDirectoryEnumerationSkipsSubdirectoryDescendants) errorHandler:nil]; | ||
| NSFileManager *fileManager = [[NSFileManager alloc] init]; | ||
| for(NSURL* url in enumerator) { | ||
| BOOL isDir = false; | ||
| if (![fileManager fileExistsAtPath:url.path isDirectory:&isDir] || isDir) continue; | ||
| if ([url.pathExtension isEqualToString: TombstoneFileExtension]) { | ||
| [tombstones addObject:url]; | ||
| } | ||
| } | ||
| }]; | ||
|
|
||
| NSUInteger prefixLength = self.blobDirectoryURL.path.length+1; // +1 is for the last slash | ||
| for (NSURL *url in tombstones) { | ||
| // Resolving symbolic link here, because on iOS at least, the directory enumerator | ||
| // uses a sym linked "private" folder, causing the path to be different to what comes | ||
| // out for the blobDirectoryURL. | ||
| NSString *absolutePath = [url URLByResolvingSymlinksInPath].path; | ||
| NSString *relativePath = [absolutePath substringFromIndex:prefixLength]; | ||
| block(relativePath); | ||
| NSString *relativeTombstonePath = [absolutePath substringFromIndex:prefixLength]; | ||
| NSString *relativePath = [relativeTombstonePath stringByDeletingPathExtension]; | ||
|
|
||
| // we pass back both the path to the datafile and the path to the tombstone file | ||
| // since the relationship between the two is an implementation detail | ||
| // (right now it's just an extra file extension, but that may not always be true) | ||
| block(relativePath, relativeTombstonePath); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| #pragma mark - Syncing | ||
|
|
||
| - (void)applySyncChangeWithValues:(NSDictionary *)values timestamps:(NSDictionary *)timestamps | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we would want to be able to query if a tombstone exists too. Imagine we are doing the sync engine. It would want to check if a particular file from the cloud is now a tombstone, and should be removed. Note that that is different to the blob simply not being found locally, which would indicate it should be downloaded, not deleted.
So I'm thinking probably want a query for tombstone existence too. Perhaps
blobIsRegisterdDeletedAtPath:. It think this already implies the use of tombstones, otherwise how would we know.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wasn't quite sure whether we needed to treat 'file doesn't exist' as different from 'tombstone exists', or whether they'd turn out to imply the same thing in any calling code. That's what I was aiming at with just having the
existscall, but we may well need the other one instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most of the time you just want the exists call, and it should handle tombstones for you. But in rare occasions, like sync, you want to query whether the formerly existed.