ish/app/FileProvider/FileProviderExtension.m
2023-06-02 18:03:10 -07:00

465 lines
19 KiB
Objective-C

//
// FileProviderExtension.m
// iSHFiles
//
// Created by Theodore Dubois on 9/20/18.
//
#import "FileProviderExtension.h"
#import "FileProviderItem.h"
#import "FileProviderEnumerator.h"
#import "NSError+ISHErrno.h"
#import "../AppGroup.h"
#import "../ExceptionExfiltrator.h"
#include "fs/fake-db.h"
@interface FileProviderExtension () {
BOOL _mounted;
struct fakefs_mount _mount;
};
@property NSURL *root;
@end
@implementation FileProviderExtension
- (struct fakefs_mount *)mount {
NSAssert(_mounted, @"");
return &_mount;
}
- (BOOL)getMount:(struct fakefs_mount **)mount error:(NSError **)error {
@synchronized (self) {
if (!_mounted) {
if (self.domain == nil) {
*error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNotAuthenticated userInfo:nil];
return NO;
}
NSURL *container = ContainerURL();
NSURL *fs_dir = [[container URLByAppendingPathComponent:@"roots"]
URLByAppendingPathComponent:self.domain.identifier];
_root = [fs_dir URLByAppendingPathComponent:@"data"];
_mount.source = strdup(_root.fileSystemRepresentation);
_mount.root_fd = open(_mount.source, O_RDONLY | O_DIRECTORY);
int err = fake_db_init(&_mount.db, [fs_dir URLByAppendingPathComponent:@"meta.db"].fileSystemRepresentation, _mount.root_fd);
if (err < 0) {
NSLog(@"error opening root: %d", err);
close(_mount.root_fd);
*error = [NSError errorWithISHErrno:err itemIdentifier:NSFileProviderRootContainerItemIdentifier];
return NO;
}
*mount = &_mount;
_mounted = YES;
}
// Run a cleanup every once in a while. The idea here is that this
// function gets called while the file provider is being interacted
// with, so this should generally get time to run at that point, but we
// don't want to do this when the user is not interacting with the file
// provider.
NSDate *lastCleanup = [NSUserDefaults.standardUserDefaults objectForKey:@"LastCleanup"];
lastCleanup = lastCleanup ? lastCleanup : NSDate.distantPast;
if ([lastCleanup timeIntervalSinceDate:NSDate.date] > 60 * 60 /* 1 hour */) {
[self cleanupStorage];
}
[NSUserDefaults.standardUserDefaults setObject:NSDate.date forKey:@"LastCleanup"];
return YES;
}
}
- (NSURL *)storageURL {
NSURL *storage = NSFileProviderManager.defaultManager.documentStorageURL;
if (self.domain != nil)
storage = [storage URLByAppendingPathComponent:self.domain.pathRelativeToDocumentStorage isDirectory:YES];
return storage;
}
- (nullable NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError * _Nullable *)error {
struct fakefs_mount *mount;
if (![self getMount:&mount error:error]) return nil;
NSLog(@"item for id %@", identifier);
NSError *err;
FileProviderItem *item = [[FileProviderItem alloc] initWithIdentifier:identifier mount:&_mount error:&err];
if (item == nil) {
if (error != nil)
*error = err;
return nil;
}
return item;
}
- (nullable NSURL *)URLForItemWithPersistentIdentifier:(NSFileProviderItemIdentifier)identifier {
if ([identifier isEqualToString:NSFileProviderRootContainerItemIdentifier])
return self.storageURL;
FileProviderItem *item = [self itemForIdentifier:identifier error:nil];
if (item == nil)
return nil;
NSURL *url = [self.storageURL URLByAppendingPathComponent:identifier isDirectory:YES];
url = [url URLByAppendingPathComponent:item.path.lastPathComponent isDirectory:NO];
NSLog(@"url for id %@ = %@", identifier, url);
return url;
}
- (nullable NSFileProviderItemIdentifier)persistentIdentifierForItemAtURL:(NSURL *)url {
if ([url.URLByDeletingLastPathComponent isEqual:NSFileProviderManager.defaultManager.documentStorageURL]) {
NSAssert([self.domain.identifier isEqualToString:url.lastPathComponent], @"url isn't the same as our domain");
return NSFileProviderRootContainerItemIdentifier;
}
NSString *identifier = url.pathComponents[url.pathComponents.count - 2];
if (identifier.longLongValue == 0)
return nil; // something must be screwed I guess
NSLog(@"id for url %@ = %@", url, identifier);
return identifier;
}
- (BOOL)enhanceSanityOfURL:(NSURL *)url error:(NSError **)error {
NSURL *dir = url.URLByDeletingLastPathComponent;
NSFileManager *manager = NSFileManager.defaultManager;
BOOL isDir;
if ([manager fileExistsAtPath:dir.path isDirectory:&isDir] && !isDir)
[manager removeItemAtURL:dir error:nil];
return [manager createDirectoryAtURL:dir
withIntermediateDirectories:YES
attributes:nil
error:error];
}
- (void)providePlaceholderAtURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable error))completionHandler {
NSError *err;
FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:&err];
if (item == nil) {
completionHandler(err);
return;
}
if (![self enhanceSanityOfURL:url error:&err]) {
completionHandler(err);
return;
}
if (![NSFileProviderManager writePlaceholderAtURL:[NSFileProviderManager placeholderURLForURL:url]
withMetadata:item
error:&err]) {
completionHandler(err);
return;
}
completionHandler(nil);
}
- (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler {
// Should ensure that the actual file is in the position returned by URLForItemWithIdentifier:, then call the completion handler
NSError *err;
FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:&err];
if (item == nil) {
completionHandler(err);
return;
}
if (![self enhanceSanityOfURL:url error:&err]) {
completionHandler(err);
return;
}
[item loadToURL:url];
completionHandler(nil);
}
- (void)itemChangedAtURL:(NSURL *)url {
FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:nil];
if (item == nil)
return;
[item saveFromURL:url];
}
#pragma mark - Action helpers
// FIXME: not dry enough
// It's ok to use _mount in these because in each case the caller has already invoked itemForIdentifier:error: at least once
- (BOOL)doCreateDirectoryAt:(NSString *)path inode:(ino_t *)inode error:(NSError **)error {
NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]] URLByAppendingPathComponent:path];
db_begin(&_mount.db);
if (![NSFileManager.defaultManager createDirectoryAtURL:url
withIntermediateDirectories:NO
attributes:@{NSFilePosixPermissions: @0777}
error:error]) {
db_rollback(&_mount.db);
return nil;
}
struct ish_stat stat;
NSString *parentPath = [path substringToIndex:[path rangeOfString:@"/" options:NSBackwardsSearch].location];
if (!path_read_stat(&_mount.db, parentPath.fileSystemRepresentation, &stat, NULL)) {
db_rollback(&_mount.db);
*error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNoSuchItem userInfo:nil];
return nil;
}
stat.mode = (stat.mode & ~S_IFMT) | S_IFDIR;
path_create(&_mount.db, path.fileSystemRepresentation, &stat);
if (inode != NULL)
*inode = path_get_inode(&_mount.db, path.fileSystemRepresentation);
db_commit(&_mount.db);
return YES;
}
- (BOOL)doCreateFileAt:(NSString *)path importFrom:(NSURL *)importURL inode:(ino_t *)inode error:(NSError **)error {
NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]] URLByAppendingPathComponent:path];
db_begin(&_mount.db);
if (![NSFileManager.defaultManager copyItemAtURL:importURL
toURL:url
error:error]) {
db_rollback(&_mount.db);
return nil;
}
struct ish_stat stat;
NSString *parentPath = [path substringToIndex:[path rangeOfString:@"/" options:NSBackwardsSearch].location];
if (!path_read_stat(&_mount.db, parentPath.fileSystemRepresentation, &stat, NULL)) {
db_rollback(&_mount.db);
*error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNoSuchItem userInfo:nil];
return nil;
}
stat.mode = (stat.mode & ~S_IFMT & ~0111) | S_IFREG;
path_create(&_mount.db, path.fileSystemRepresentation, &stat);
if (inode != NULL)
*inode = path_get_inode(&_mount.db, path.fileSystemRepresentation);
db_commit(&_mount.db);
return YES;
}
- (NSString *)pathOfItemWithIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError **)error {
FileProviderItem *parent = [self itemForIdentifier:identifier error:error];
if (parent == nil)
return nil;
return parent.path;
}
#pragma mark - Actions
/* TODO: implement the actions for items here
each of the actions follows the same pattern:
- make a note of the change in the local model
- schedule a server request as a background task to inform the server of the change
- call the completion block with the modified item in its post-modification state
*/
- (void)createDirectoryWithName:(NSString *)directoryName inParentItemIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler {
NSError *error;
NSString *parentPath = [self pathOfItemWithIdentifier:parentItemIdentifier error:&error];
if (parentPath == nil) {
completionHandler(nil, error);
return;
}
ino_t inode;
if (![self doCreateDirectoryAt:[parentPath stringByAppendingFormat:@"/%@", directoryName] inode:&inode error:&error]) {
completionHandler(nil, error);
return;
}
FileProviderItem *item = [self itemForIdentifier:[NSString stringWithFormat:@"%lu", (unsigned long) inode] error:&error];
if (item == nil)
completionHandler(nil, error);
else
completionHandler(item, nil);
}
- (void)importDocumentAtURL:(NSURL *)fileURL toParentItemIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler {
NSError *error;
NSString *parentPath = [self pathOfItemWithIdentifier:parentItemIdentifier error:&error];
if (parentPath == nil) {
completionHandler(nil, error);
return;
}
[fileURL startAccessingSecurityScopedResource];
BOOL isDir;
assert([NSFileManager.defaultManager fileExistsAtPath:fileURL.path isDirectory:&isDir] && !isDir);
ino_t inode;
BOOL worked = [self doCreateFileAt:[parentPath stringByAppendingFormat:@"/%@", fileURL.lastPathComponent]
importFrom:fileURL
inode:&inode
error:&error];
[fileURL stopAccessingSecurityScopedResource];
if (!worked) {
completionHandler(nil, error);
return;
}
FileProviderItem *item = [self itemForIdentifier:[NSString stringWithFormat:@"%lu", (unsigned long) inode] error:&error];
if (item == nil)
completionHandler(nil, error);
else
completionHandler(item, nil);
}
- (NSString *)pathFromURL:(NSURL *)url {
NSURL *root = [NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]];
assert([url.path hasPrefix:root.path]);
NSString *path = [url.path substringFromIndex:root.path.length];
assert([path hasPrefix:@"/"]);
if ([path hasSuffix:@"/"])
path = [path substringToIndex:path.length - 1];
return path;
}
- (BOOL)doDelete:(NSString *)path itemIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError **)error {
NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]] URLByAppendingPathComponent:path];
NSDirectoryEnumerator<NSURL *> *enumerator = [NSFileManager.defaultManager enumeratorAtURL:url
includingPropertiesForKeys:nil
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
errorHandler:nil];
for (NSURL *suburl in enumerator) {
if (![self doDelete:[self pathFromURL:suburl] itemIdentifier:identifier error:error])
return NO;
}
db_begin(&_mount.db);
path_unlink(&_mount.db, path.fileSystemRepresentation);
int err = unlinkat(_mount.root_fd, fix_path(path.fileSystemRepresentation), 0);
if (err < 0)
err = unlinkat(_mount.root_fd, fix_path(path.fileSystemRepresentation), AT_REMOVEDIR);
if (err < 0) {
db_rollback(&_mount.db);
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
return NO;
}
db_commit(&_mount.db);
return YES;
}
- (void)deleteItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier completionHandler:(void (^)(NSError * _Nullable))completionHandler {
NSError *error;
NSString *path = [self pathOfItemWithIdentifier:itemIdentifier error:&error];
if (path == nil) {
completionHandler(error);
return;
}
if (![self doDelete:path itemIdentifier:itemIdentifier error:&error])
completionHandler(error);
else
completionHandler(nil);
}
- (BOOL)doRename:(NSString *)src to:(NSString *)dst itemIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError **)error {
db_begin(&_mount.db);
path_rename(&_mount.db, src.fileSystemRepresentation, dst.fileSystemRepresentation);
int err = renameat(_mount.root_fd, fix_path(src.fileSystemRepresentation), _mount.root_fd, fix_path(dst.fileSystemRepresentation));
if (err < 0) {
db_rollback(&_mount.db);
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
return NO;
}
db_commit(&_mount.db);
return YES;
}
- (void)renameItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier toName:(NSString *)itemName completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler {
NSError *error;
FileProviderItem *item = [self itemForIdentifier:itemIdentifier error:&error];
if (item == nil) {
completionHandler(nil, error);
return;
}
NSString *dstPath = [item.path.stringByDeletingLastPathComponent stringByAppendingPathComponent:itemName];
if (![self doRename:item.path to:dstPath itemIdentifier:itemIdentifier error:&error]) {
completionHandler(nil, error);
return;
}
completionHandler(item, nil);
}
- (void)reparentItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier toParentItemWithIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier newName:(NSString *)newName completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler {
NSError *error;
FileProviderItem *item = [self itemForIdentifier:itemIdentifier error:&error];
if (item == nil) {
completionHandler(nil, error);
return;
}
FileProviderItem *parent = [self itemForIdentifier:parentItemIdentifier error:&error];
if (parent == nil) {
completionHandler(nil, error);
return;
}
if (newName == nil)
newName = item.path.lastPathComponent;
if (![self doRename:item.path to:[parent.path stringByAppendingPathComponent:newName] itemIdentifier:itemIdentifier error:&error]) {
completionHandler(nil, error);
return;
}
completionHandler(item, nil);
}
#pragma mark - Enumeration
- (nullable id<NSFileProviderEnumerator>)enumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)containerItemIdentifier error:(NSError **)error {
FileProviderItem *item = [self itemForIdentifier:containerItemIdentifier error:error];
if (item == nil)
return nil;
return [[FileProviderEnumerator alloc] initWithItem:item];
}
- (void)dealloc {
if (_mounted) {
free((void *) _mount.source);
close(_mount.root_fd);
fake_db_deinit(&_mount.db);
}
}
#pragma mark - Storage deletion
// According to an engineer I talked to at WWDC, -stopProvidingItemAtURL: is never ever called, so that can't be used to free up disk space.
// Solution for now is to periodically look for and delete files in file provider storage where the original is missing.
// TODO: Delete files in file provider storage when the original file is deleted
// TODO: Create hardlinks into file provider storage instead of copies
//
- (void)cleanupStorage {
NSAssert(_mounted, @"Mount should exist by this point");
NSFileManager *manager = NSFileManager.defaultManager;
NSArray<NSURL *> *storageDirs = [manager contentsOfDirectoryAtURL:self.storageURL includingPropertiesForKeys:nil options:0 error:nil];
for (NSURL *dir in storageDirs) {
inode_t inode = dir.lastPathComponent.longLongValue;
if (inode == 0)
continue;
// TODO: make this a function in fake-db.c
db_begin(&_mount.db);
sqlite3_bind_int64(_mount.db.stmt.inode_read_stat, 1, inode);
BOOL exists = db_exec(&_mount.db, _mount.db.stmt.inode_read_stat);
db_reset(&_mount.db, _mount.db.stmt.inode_read_stat);
db_rollback(&_mount.db);
if (!exists) {
NSLog(@"removing dead inode %llu", inode);
NSError *err;
if (![manager removeItemAtURL:dir error:&err])
NSLog(@"failed to remove dead inode: %@", err);
}
}
}
// Dead code, leaving it here just in case
- (void)stopProvidingItemAtURL:(NSURL *)url {
FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:nil];
if (item == nil)
return;
[item saveFromURL:url];
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
[NSFileProviderManager writePlaceholderAtURL:[NSFileProviderManager placeholderURLForURL:url]
withMetadata:item
error:nil];
}
+ (void)load {
NSSetUncaughtExceptionHandler(iSHExceptionHandler);
}
@end
void die(const char *msg, ...) {
va_list args;
va_start(args, msg);
[NSException raise:@"ish die" format:[NSString stringWithUTF8String:msg] arguments:args];
abort();
va_end(args);
}
void ish_printk(const char *msg, ...) {
va_list args;
va_start(args, msg);
NSLogv([NSString stringWithUTF8String:msg], args);
va_end(args);
}