ish/app/FileProvider/FileProviderItem.m
Theodore Dubois 9510680692 Make sure to take sqlite write locks upfront
Fun quirk of sqlite is that if you BEGIN DEFERRED and then do a read
statement and then another process concurrently does a write and then
the first process does a write statement on the same transaction, it
will immediately return SQLITE_BUSY because it's impossible to do a
write against a past version of the database. To fix this we need to use
BEGIN IMMEDIATE to take a write lock upfront on any transaction that
will need to write.
2024-10-27 15:10:14 -07:00

239 lines
7.9 KiB
Objective-C

//
// FileProviderItem.m
// iSHFiles
//
// Created by Theodore Dubois on 9/20/18.
//
#import <MobileCoreServices/MobileCoreServices.h>
#include <sys/stat.h>
#include <dirent.h>
#import "FileProviderExtension.h"
#import "FileProviderItem.h"
#include "fs/fake-db.h"
#include "kernel/errno.h"
@interface FileProviderItem ()
@property (readonly) NSFileProviderItemIdentifier identifier;
@property (readonly) int fd;
@property (readonly) BOOL isRoot;
@end
@implementation FileProviderItem
- (instancetype)initWithIdentifier:(NSFileProviderItemIdentifier)identifier mount:(struct fakefs_mount *)mount error:(NSError *__autoreleasing _Nullable *)error {
if (self = [super init]) {
_identifier = identifier;
_mount = mount;
_fd = [self openNewFDWithError:error];
if (_fd == -1)
return nil;
}
return self;
}
- (BOOL)isRoot {
return [self.identifier isEqualToString:NSFileProviderRootContainerItemIdentifier];
}
- (int)openNewFDWithError:(NSError *__autoreleasing _Nullable *)error {
int fd = -1;
if (self.isRoot) {
fd = open(_mount->source, O_DIRECTORY | O_RDONLY);
} else {
db_begin_read(&_mount->db);
sqlite3_stmt *stmt = _mount->db.stmt.path_from_inode;
sqlite3_bind_int64(_mount->db.stmt.path_from_inode, 1, _identifier.longLongValue);
while (db_exec(&_mount->db, stmt)) {
const char *path = (const char *) sqlite3_column_text(stmt, 0);
fd = openat(_mount->root_fd, fix_path(path), O_RDWR);
if (fd == -1 && errno == EISDIR)
fd = openat(_mount->root_fd, fix_path(path), O_RDONLY);
if (fd == -1 && errno != ENOENT)
break;
}
db_reset(&_mount->db, stmt);
db_commit(&_mount->db);
}
if (fd == -1) {
if (error != nil) {
if (errno == ENOENT)
*error = [NSError fileProviderErrorForNonExistentItemWithIdentifier:_identifier];
else
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
}
NSLog(@"opening %@ failed: %@", self.identifier, *error);
return -1;
}
return fd;
}
- (NSString *)path {
char path[PATH_MAX] = "";
int err = fcntl(_fd, F_GETPATH, path);
[self handleError:err inFunction:@"getpath"];
const char *myPath = path + strlen(_mount->source);
return [NSFileManager.defaultManager stringWithFileSystemRepresentation:myPath length:strlen(myPath)];
}
- (NSURL *)URL {
NSURL *rootURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount->source]];
if (self.isRoot)
return rootURL;
return [rootURL URLByAppendingPathComponent:self.path];
}
- (struct ish_stat)ishStat {
struct ish_stat stat = {};
db_begin_read(&_mount->db);
inode_t inode = _identifier.longLongValue;
if ([_identifier isEqualToString:NSFileProviderRootContainerItemIdentifier])
inode = path_get_inode(&_mount->db, "");
inode_read_stat_or_die(&_mount->db, inode, &stat);
db_commit(&_mount->db);
return stat;
}
- (struct stat)realStat {
struct stat statbuf;
int err = fstat(_fd, &statbuf);
[self handleError:err inFunction:@"realStat"];
return statbuf;
}
- (NSFileProviderItemIdentifier)itemIdentifier {
if (self.isRoot)
return NSFileProviderRootContainerItemIdentifier;
return _identifier;
}
- (NSFileProviderItemIdentifier)parentItemIdentifier {
if (self.isRoot) {
NSLog(@"parent of root %@ is %@", self.path, NSFileProviderRootContainerItemIdentifier);
return NSFileProviderRootContainerItemIdentifier;
}
NSString *parentPath = self.path.stringByDeletingLastPathComponent;
if ([parentPath isEqualToString:@"/"])
return NSFileProviderRootContainerItemIdentifier;
db_begin_read(&_mount->db);
inode_t parentInode = path_get_inode(&_mount->db, parentPath.UTF8String);
db_commit(&_mount->db);
assert(parentInode != 0);
NSString *parent = [NSString stringWithFormat:@"%lu", (unsigned long) parentInode];
NSLog(@"parent of %@ is %@", self.path, parent);
return parent;
}
- (NSFileProviderItemCapabilities)capabilities {
NSFileProviderItemCapabilities caps = NSFileProviderItemCapabilitiesAllowsDeleting | NSFileProviderItemCapabilitiesAllowsRenaming | NSFileProviderItemCapabilitiesAllowsReparenting;
if (S_ISREG(self.ishStat.mode))
caps |= NSFileProviderItemCapabilitiesAllowsReading | NSFileProviderItemCapabilitiesAllowsWriting;
else if (S_ISDIR(self.ishStat.mode))
caps |= NSFileProviderItemCapabilitiesAllowsAddingSubItems | NSFileProviderItemCapabilitiesAllowsContentEnumerating;
else
return 0;
return caps;
}
- (NSString *)filename {
NSString *filename = self.path.lastPathComponent;
if ([filename isEqualToString:@""])
filename = @"/";
NSLog(@"filename %@", filename);
return filename;
}
- (NSNumber *)documentSize {
struct stat statbuf;
int err = fstat(_fd, &statbuf);
[self handleError:err inFunction:@"documentSize"];
return [NSNumber numberWithUnsignedLongLong:statbuf.st_size];
}
- (NSNumber *)childItemCount {
if (!S_ISDIR(self.ishStat.mode))
return @0;
int fd = [self openNewFDWithError:nil];
if (fd == -1)
return @0;
unsigned n = 0;
DIR *dir = fdopendir(fd);
struct dirent *dirent;
while ((dirent = readdir(dir))) {
if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0)
continue;
n++;
}
closedir(dir);
return @(n);
}
- (NSDate *)contentModificationDate {
struct stat statbuf = self.realStat;
return [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtimespec.tv_sec + (NSTimeInterval) statbuf.st_mtimespec.tv_nsec / 1000000000];
}
- (NSString *)typeIdentifier {
if (self.isRoot) {
NSLog(@"uti of %@ is %@", self.path, (NSString *) kUTTypeFolder);
return (NSString *) kUTTypeFolder;
}
mode_t_ mode = self.ishStat.mode;
if ((mode & S_IFMT) == S_IFDIR)
return (NSString *) kUTTypeFolder;
if ((mode & S_IFMT) == S_IFLNK)
return (NSString *) kUTTypeSymLink;
NSString *uti = CFBridgingRelease(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
(__bridge CFStringRef _Nonnull) self.path.pathExtension, nil));
if ([uti hasPrefix:@"dyn."])
uti = (NSString *) kUTTypePlainText;
NSLog(@"uti of %@ is %@", self.path, uti);
return uti;
}
// locking on these keeps the remove/copy operation atomic
// or at least tries to
- (void)loadToURL:(NSURL *)url {
NSLog(@"copying %@ to %@", self.path, url);
NSURL *itemURL = self.URL;
NSError *err;
sqlite3_mutex_enter(_mount->db.lock);
[NSFileManager.defaultManager removeItemAtURL:url error:nil];
BOOL success = [NSFileManager.defaultManager copyItemAtURL:itemURL
toURL:url
error:&err];
sqlite3_mutex_leave(_mount->db.lock);
if (!success) {
NSLog(@"error copying to %@: %@", url, err);
}
}
- (void)saveFromURL:(NSURL *)url {
NSLog(@"copying %@ from %@", self.path, url);
NSURL *itemURL = self.URL;
NSError *err;
sqlite3_mutex_enter(_mount->db.lock);
[NSFileManager.defaultManager removeItemAtURL:itemURL error:nil];
BOOL success = [NSFileManager.defaultManager copyItemAtURL:url
toURL:itemURL
error:&err];
sqlite3_mutex_leave(_mount->db.lock);
if (!success) {
NSLog(@"error copying to %@: %@", url, err);
}
}
- (void)dealloc {
if (self.fd != -1)
close(self.fd);
}
- (void)handleError:(long)err inFunction:(NSString *)func {
if (err < 0) {
[NSException raise:NSGenericException format:@"%@ returned %ld %d", func, err, errno];
}
}
@end