From 2f28e6a52948aba7aeef327d8f783076b6f3ac8f Mon Sep 17 00:00:00 2001 From: Theodore Dubois Date: Fri, 19 Jun 2020 09:46:07 -0700 Subject: [PATCH] Add cancel buttons for import and export --- app/ProgressReportViewController.m | 15 ++++++++++ app/Roots.h | 1 + app/Roots.m | 18 ++++++++++-- app/Roots.storyboard | 25 +++++++++++++--- app/RootsTableViewController.m | 47 ++++++++++++++++++------------ fs/fakefsify.c | 13 ++++++--- fs/fakefsify.h | 3 +- 7 files changed, 92 insertions(+), 30 deletions(-) diff --git a/app/ProgressReportViewController.m b/app/ProgressReportViewController.m index 575155101c..9b97b37753 100644 --- a/app/ProgressReportViewController.m +++ b/app/ProgressReportViewController.m @@ -13,9 +13,11 @@ @interface ProgressReportViewController () @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @property (weak, nonatomic) IBOutlet UILabel *statusLabel; @property (weak, nonatomic) IBOutlet UIProgressView *bar; +@property (weak, nonatomic) IBOutlet UIButton *cancelButton; @property (nonatomic) double progress; @property (nonatomic) NSString *message; +@property (nonatomic) BOOL cancelled; @property CADisplayLink *timer; @@ -52,6 +54,12 @@ - (void)updateProgress:(double)progressFraction message:(NSString *)progressMess } } +- (BOOL)shouldCancel { + @synchronized (self) { + return _cancelled; + } +} + - (void)update { @synchronized (self) { self.bar.progress = _progress; @@ -59,4 +67,11 @@ - (void)update { } } +- (IBAction)cancel:(id)sender { + @synchronized (self) { + self.cancelled = YES; + self.cancelButton.enabled = NO; + } +} + @end diff --git a/app/Roots.h b/app/Roots.h index 3bd88b32f4..9e8e0780e9 100644 --- a/app/Roots.h +++ b/app/Roots.h @@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ProgressReporter - (void)updateProgress:(double)progressFraction message:(NSString *)progressMessage; +- (BOOL)shouldCancel; @end diff --git a/app/Roots.m b/app/Roots.m index d2ce812935..008b0ed179 100644 --- a/app/Roots.m +++ b/app/Roots.m @@ -109,16 +109,23 @@ - (BOOL)accessInstanceVariablesDirectly { return YES; } -void root_progress_callback(void *cookie, double progress, const char *message) { +void root_progress_callback(void *cookie, double progress, const char *message, bool *should_cancel) { id reporter = (__bridge id) cookie; [reporter updateProgress:progress message:[NSString stringWithUTF8String:message]]; + if ([reporter shouldCancel]) + *should_cancel = true; } - (BOOL)importRootFromArchive:(NSURL *)archive name:(NSString *)name error:(NSError **)error progressReporter:(id _Nullable)progress { NSAssert(![self.roots containsObject:name], @"root already exists: %@", name); struct fakefsify_error fs_err; + NSURL *destination = [RootsDir() URLByAppendingPathComponent:name]; + NSURL *tempDestination = [NSFileManager.defaultManager.temporaryDirectory + URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]]; + if (tempDestination == nil) + return NO; if (!fakefs_import(archive.fileSystemRepresentation, - [RootsDir() URLByAppendingPathComponent:name].fileSystemRepresentation, + tempDestination.fileSystemRepresentation, &fs_err, (struct progress) {(__bridge void *) progress, root_progress_callback})) { NSString *domain = NSPOSIXErrorDomain; if (fs_err.type == ERR_SQLITE) @@ -127,9 +134,14 @@ - (BOOL)importRootFromArchive:(NSURL *)archive name:(NSString *)name error:(NSEr code:fs_err.code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s, line %d", fs_err.message, fs_err.line]}]; + if (fs_err.type == ERR_CANCELLED) + *error = nil; free(fs_err.message); + [NSFileManager.defaultManager removeItemAtURL:tempDestination error:nil]; return NO; } + if (![NSFileManager.defaultManager moveItemAtURL:tempDestination toURL:destination error:error]) + return NO; dispatch_async(dispatch_get_main_queue(), ^{ [[self mutableOrderedSetValueForKey:@"roots"] addObject:name]; }); @@ -149,6 +161,8 @@ - (BOOL)exportRootNamed:(NSString *)name toArchive:(NSURL *)archive error:(NSErr *error = [NSError errorWithDomain:domain code:fs_err.code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:fs_err.message]}]; + if (fs_err.type == ERR_CANCELLED) + *error = nil; free(fs_err.message); return NO; } diff --git a/app/Roots.storyboard b/app/Roots.storyboard index 747fc776fd..3017ec5b91 100644 --- a/app/Roots.storyboard +++ b/app/Roots.storyboard @@ -223,12 +223,12 @@ - + - + - + @@ -253,6 +253,19 @@ + + + + + + @@ -260,11 +273,14 @@ - + + + + @@ -280,6 +296,7 @@ + diff --git a/app/RootsTableViewController.m b/app/RootsTableViewController.m index 8e43d09a33..306964c5bf 100644 --- a/app/RootsTableViewController.m +++ b/app/RootsTableViewController.m @@ -98,9 +98,10 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocum NSError *error; BOOL success = [Roots.instance importRootFromArchive:urls[0] name:name error:&error progressReporter:progressVC]; dispatch_async(dispatch_get_main_queue(), ^{ - [progressVC dismissViewControllerAnimated:YES completion:nil]; - if (!success) - [self presentError:error title:@"Import failed"]; + [progressVC dismissViewControllerAnimated:YES completion:^{ + if (!success && error != nil) + [self presentError:error title:@"Import failed"]; + }]; }); }); } @@ -153,7 +154,13 @@ - (void)browseFiles { } - (void)exportFilesystem { - self.exportURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.tar.gz", self.rootName]]; + self.exportURL = [[NSFileManager.defaultManager.temporaryDirectory + URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]] + URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.tar.gz", self.rootName]]; + [NSFileManager.defaultManager createDirectoryAtURL:self.exportURL.URLByDeletingLastPathComponent + withIntermediateDirectories:YES + attributes:nil + error:nil]; ProgressReportViewController *progressVC = [self.storyboard instantiateViewControllerWithIdentifier:@"progress"]; progressVC.title = [NSString stringWithFormat:@"Exporting %@", self.rootName]; [self presentViewController:progressVC animated:YES completion:nil]; @@ -163,26 +170,28 @@ - (void)exportFilesystem { NSError *err; BOOL success = [Roots.instance exportRootNamed:self.rootName toArchive:self.exportURL error:&err progressReporter:progressVC]; dispatch_async(dispatch_get_main_queue(), ^{ - [progressVC dismissViewControllerAnimated:YES completion:nil]; - if (!success) { - [self presentError:err title:@"Export failed"]; - return; - } - - UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] - initWithURL:self.exportURL - inMode:UIDocumentPickerModeExportToService]; - picker.delegate = self; - if (@available(iOS 13, *)) { - picker.shouldShowFileExtensions = YES; - } - [self presentViewController:picker animated:YES completion:nil]; + [progressVC dismissViewControllerAnimated:YES completion:^{ + if (!success) { + if (err != nil) + [self presentError:err title:@"Export failed"]; + return; + } + + UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] + initWithURL:self.exportURL + inMode:UIDocumentPickerModeExportToService]; + picker.delegate = self; + if (@available(iOS 13, *)) { + picker.shouldShowFileExtensions = YES; + } + [self presentViewController:picker animated:YES completion:nil]; + }]; }); }); } - (void)setExportURL:(NSURL *)exportURL { - [NSFileManager.defaultManager removeItemAtURL:_exportURL error:nil]; + [NSFileManager.defaultManager removeItemAtURL:_exportURL.URLByDeletingLastPathComponent error:nil]; _exportURL = exportURL; } diff --git a/fs/fakefsify.c b/fs/fakefsify.c index a3c64d4e2b..fe3ff6e570 100644 --- a/fs/fakefsify.c +++ b/fs/fakefsify.c @@ -26,10 +26,13 @@ #define POSIX_ERR() FILL_ERR(ERR_POSIX, errno, strerror(errno)) #undef HANDLE_ERR // for sqlite #define HANDLE_ERR(db) FILL_ERR(ERR_SQLITE, sqlite3_extended_errcode(db), sqlite3_errmsg(db)) +#define CANCEL() FILL_ERR(ERR_CANCELLED, 0, ""); -static void progress_update(struct progress *p, double progress, const char *message) { +static bool progress_update(struct progress *p, double progress, const char *message) { + bool cancelled = false; if (p && p->callback) - p->callback(p->cookie, progress, message); + p->callback(p->cookie, progress, message, &cancelled); + return !cancelled; } // This isn't linked with ish which is why there's so much copy/pasted code @@ -119,7 +122,8 @@ bool fakefs_import(const char *archive_path, const char *fs, struct fakefsify_er fprintf(stderr, "warning: skipped possible path traversal %s\n", archive_entry_pathname(entry)); continue; } - progress_update(&p, (double) archive_filter_bytes(archive, -1) / archive_bytes, entry_path); + if (!progress_update(&p, (double) archive_filter_bytes(archive, -1) / archive_bytes, entry_path)) + CANCEL(); int fd = -1; if (archive_entry_filetype(entry) != AE_IFDIR) { @@ -229,7 +233,8 @@ bool fakefs_export(const char *fs, const char *archive_path, struct fakefsify_er path[path_len + 1] = '\0'; archive_entry_set_pathname(entry, path); - progress_update(&p, (double) paths_done / paths_total, path); + if (!progress_update(&p, (double) paths_done / paths_total, path)) + CANCEL(); struct ish_stat stat = *(struct ish_stat *) sqlite3_column_blob(query, 1); archive_entry_set_mode(entry, stat.mode); diff --git a/fs/fakefsify.h b/fs/fakefsify.h index 4245c20759..a273255c96 100644 --- a/fs/fakefsify.h +++ b/fs/fakefsify.h @@ -8,6 +8,7 @@ struct fakefsify_error { ERR_ARCHIVE, ERR_SQLITE, ERR_POSIX, + ERR_CANCELLED, } type; int code; char *message; @@ -15,7 +16,7 @@ struct fakefsify_error { struct progress { void *cookie; - void (*callback)(void *cookie, double progress, const char *message); + void (*callback)(void *cookie, double progress, const char *message, bool *cancel_out); }; bool fakefs_import(const char *archive_path, const char *fs, struct fakefsify_error *err_out, struct progress progress);