mirror of
https://github.com/ish-app/ish.git
synced 2026-01-18 13:57:29 +00:00
360 lines
14 KiB
Objective-C
360 lines
14 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"
|
|
#include "kernel/fs.h"
|
|
#define ISH_INTERNAL
|
|
#include "fs/fake.h"
|
|
|
|
struct task *fake_task;
|
|
|
|
@interface FileProviderExtension ()
|
|
@property NSURL *root;
|
|
@property struct mount *mount;
|
|
@end
|
|
|
|
@implementation FileProviderExtension
|
|
|
|
- (instancetype)init {
|
|
if (self = [super init]) {
|
|
self.mount = malloc(sizeof(struct mount));
|
|
if (!self.mount)
|
|
return nil;
|
|
self.mount->fs = &fakefs;
|
|
NSFileManager *manager = NSFileManager.defaultManager;
|
|
NSURL *container = [manager containerURLForSecurityApplicationGroupIdentifier:PRODUCT_APP_GROUP_IDENTIFIER];
|
|
_root = [container URLByAppendingPathComponent:@"roots/alpine/data"];
|
|
self.mount->source = strdup(self.root.fileSystemRepresentation);
|
|
int err = self.mount->fs->mount(self.mount);
|
|
if (err < 0) {
|
|
NSLog(@"error opening root: %d", err);
|
|
return nil;
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (nullable NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError * _Nullable *)error {
|
|
NSLog(@"item for id %@", identifier);
|
|
NSError *err;
|
|
FileProviderItem *item = [[FileProviderItem alloc] initWithIdentifier:identifier mount:self.mount error:&err];
|
|
if (item == nil) {
|
|
if (error != nil)
|
|
*error = err;
|
|
return nil;
|
|
}
|
|
return item;
|
|
}
|
|
|
|
- (nullable NSURL *)URLForItemWithPersistentIdentifier:(NSFileProviderItemIdentifier)identifier {
|
|
FileProviderItem *item = [self itemForIdentifier:identifier error:nil];
|
|
if (item == nil)
|
|
return nil;
|
|
NSURL *storage = NSFileProviderManager.defaultManager.documentStorageURL;
|
|
NSURL *url = [storage 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 {
|
|
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];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
#pragma mark - Action helpers
|
|
|
|
// FIXME: not dry enough
|
|
- (BOOL)doCreateDirectoryAt:(NSString *)path inode:(ino_t *)inode error:(NSError **)error {
|
|
NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:self.mount->source]] URLByAppendingPathComponent:path];
|
|
db_begin(self.mount);
|
|
if (![NSFileManager.defaultManager createDirectoryAtURL:url
|
|
withIntermediateDirectories:NO
|
|
attributes:@{NSFilePosixPermissions: @0777}
|
|
error:error]) {
|
|
db_rollback(self.mount);
|
|
return nil;
|
|
}
|
|
struct ish_stat stat;
|
|
NSString *parentPath = [path substringToIndex:[path rangeOfString:@"/" options:NSBackwardsSearch].location];
|
|
if (!path_read_stat(self.mount, parentPath.fileSystemRepresentation, &stat, NULL)) {
|
|
db_rollback(self.mount);
|
|
*error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNoSuchItem userInfo:nil];
|
|
return nil;
|
|
}
|
|
stat.mode = (stat.mode & ~S_IFMT) | S_IFDIR;
|
|
path_create(self.mount, path.fileSystemRepresentation, &stat);
|
|
if (inode != NULL)
|
|
*inode = path_get_inode(self.mount, path.fileSystemRepresentation);
|
|
db_commit(self.mount);
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)doCreateFileAt:(NSString *)path importFrom:(NSURL *)importURL inode:(ino_t *)inode error:(NSError **)error {
|
|
NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:self.mount->source]] URLByAppendingPathComponent:path];
|
|
db_begin(self.mount);
|
|
if (![NSFileManager.defaultManager copyItemAtURL:importURL
|
|
toURL:url
|
|
error:error]) {
|
|
db_rollback(self.mount);
|
|
return nil;
|
|
}
|
|
struct ish_stat stat;
|
|
NSString *parentPath = [path substringToIndex:[path rangeOfString:@"/" options:NSBackwardsSearch].location];
|
|
if (!path_read_stat(self.mount, parentPath.fileSystemRepresentation, &stat, NULL)) {
|
|
db_rollback(self.mount);
|
|
*error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNoSuchItem userInfo:nil];
|
|
return nil;
|
|
}
|
|
stat.mode = (stat.mode & ~S_IFMT & ~0111) | S_IFREG;
|
|
path_create(self.mount, path.fileSystemRepresentation, &stat);
|
|
if (inode != NULL)
|
|
*inode = path_get_inode(self.mount, path.fileSystemRepresentation);
|
|
db_commit(self.mount);
|
|
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:self.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:self.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;
|
|
}
|
|
int err = fakefs.unlink(self.mount, path.fileSystemRepresentation);
|
|
if (err < 0)
|
|
err = fakefs.rmdir(self.mount, path.fileSystemRepresentation);
|
|
if (err < 0) {
|
|
*error = [NSError errorWithISHErrno:err itemIdentifier:identifier];
|
|
return NO;
|
|
}
|
|
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 {
|
|
int err = fakefs.rename(self.mount, src.fileSystemRepresentation, dst.fileSystemRepresentation);
|
|
if (err < 0) {
|
|
*error = [NSError errorWithISHErrno:err itemIdentifier:identifier];
|
|
return NO;
|
|
}
|
|
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 {
|
|
self.mount->fs->umount(self.mount);
|
|
free(self.mount);
|
|
}
|
|
|
|
@end
|