mirror of
https://github.com/flectar/mail.git
synced 2026-11-13 02:24:19 +00:00
160 lines
8.2 KiB
Objective-C
160 lines
8.2 KiB
Objective-C
#import <UIKit/UIKit.h>
|
|
#import <Foundation/Foundation.h>
|
|
|
|
typedef void (*FlectarDocumentCallback)(long long, const char *, const char *);
|
|
// Accessed only on the main queue. The delegate survives picker dismissal
|
|
// while a coordinated background import is still running.
|
|
static NSMutableDictionary<NSNumber *, id> *requests;
|
|
|
|
static NSError *documentError(NSString *message) {
|
|
return [NSError errorWithDomain:@"FlectarDocuments" code:1
|
|
userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
}
|
|
|
|
@interface FlectarDocumentDelegate : NSObject <UIDocumentPickerDelegate, UIAdaptivePresentationControllerDelegate>
|
|
@property(nonatomic) long long requestId;
|
|
@property(nonatomic) FlectarDocumentCallback callback;
|
|
@property(nonatomic) BOOL exporting;
|
|
@property(nonatomic) BOOL finished;
|
|
@property(atomic) BOOL cancelled;
|
|
@property(nonatomic, strong) UIDocumentPickerViewController *picker;
|
|
@end
|
|
|
|
@implementation FlectarDocumentDelegate
|
|
- (void)finish:(NSString *)path error:(NSString *)error {
|
|
if (self.finished) return;
|
|
self.finished = YES;
|
|
self.callback(self.requestId, path.UTF8String ?: "", error.UTF8String ?: "");
|
|
[requests removeObjectForKey:@(self.requestId)];
|
|
self.picker = nil;
|
|
}
|
|
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
|
|
self.cancelled = YES;
|
|
[self finish:@"" error:@""];
|
|
}
|
|
- (void)presentationControllerDidDismiss:(UIPresentationController *)controller {
|
|
self.cancelled = YES;
|
|
[self finish:@"" error:@""];
|
|
}
|
|
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
|
NSURL *url = urls.firstObject;
|
|
if (!url) { [self finish:@"" error:@"No document was selected."]; return; }
|
|
if (self.exporting) { [self finish:@"exported" error:@""]; return; }
|
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
BOOL scoped = [url startAccessingSecurityScopedResource];
|
|
__block NSError *readError = nil;
|
|
NSError *coordinationError = nil;
|
|
__block NSURL *destination = nil;
|
|
NSURL *folder = [[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory
|
|
inDomains:NSUserDomainMask].firstObject URLByAppendingPathComponent:@"file-imports"
|
|
isDirectory:YES] URLByAppendingPathComponent:NSUUID.UUID.UUIDString isDirectory:YES];
|
|
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
|
|
[coordinator coordinateReadingItemAtURL:url options:0 error:&coordinationError
|
|
byAccessor:^(NSURL *readURL) {
|
|
NSNumber *regular = nil;
|
|
[readURL getResourceValue:®ular forKey:NSURLIsRegularFileKey error:&readError];
|
|
NSString *name = readURL.lastPathComponent;
|
|
if (readError) return;
|
|
if (!regular.boolValue || name.length == 0 || [name isEqualToString:@"."] ||
|
|
[name isEqualToString:@".."] || [name containsString:@"/"] || [name containsString:@"\\"]) {
|
|
readError = documentError(@"Select a regular file with a valid name.");
|
|
return;
|
|
}
|
|
if (self.cancelled) { readError = documentError(@"Document transfer cancelled."); return; }
|
|
destination = [folder URLByAppendingPathComponent:name];
|
|
[[NSFileManager defaultManager] createDirectoryAtURL:folder withIntermediateDirectories:YES
|
|
attributes:@{NSFilePosixPermissions: @0700} error:&readError];
|
|
if (readError) return;
|
|
NSInputStream *input = [NSInputStream inputStreamWithURL:readURL];
|
|
NSOutputStream *output = [NSOutputStream outputStreamWithURL:destination append:NO];
|
|
if (!input || !output) { readError = documentError(@"Could not open the document streams."); return; }
|
|
[input open];
|
|
[output open];
|
|
uint8_t buffer[65536];
|
|
unsigned long long total = 0;
|
|
while (!readError) {
|
|
if (self.cancelled) { readError = documentError(@"Document transfer cancelled."); break; }
|
|
NSInteger count = [input read:buffer maxLength:sizeof(buffer)];
|
|
if (count == 0) break;
|
|
if (count < 0) { readError = input.streamError ?: documentError(@"Could not read the document."); break; }
|
|
total += (unsigned long long)count;
|
|
if (total > 512ULL * 1024 * 1024) {
|
|
readError = documentError(@"File exceeds the transfer limit.");
|
|
break;
|
|
}
|
|
NSInteger offset = 0;
|
|
while (offset < count) {
|
|
NSInteger written = [output write:buffer + offset maxLength:(NSUInteger)(count - offset)];
|
|
if (written <= 0) { readError = output.streamError ?: documentError(@"Could not write the imported file."); break; }
|
|
offset += written;
|
|
}
|
|
}
|
|
[input close];
|
|
[output close];
|
|
}];
|
|
if (scoped) [url stopAccessingSecurityScopedResource];
|
|
NSError *error = coordinationError ?: readError;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error || self.cancelled || self.finished || !destination) {
|
|
[[NSFileManager defaultManager] removeItemAtURL:folder error:nil];
|
|
[self finish:@"" error:error.localizedDescription ?: @""];
|
|
} else {
|
|
// Rust owns the copy from this callback, even if its receiver
|
|
// was cancelled immediately before delivery.
|
|
[self finish:destination.path error:@""];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
@end
|
|
|
|
void flectar_cancel_document(long long requestId) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
FlectarDocumentDelegate *delegate = requests[@(requestId)];
|
|
if (!delegate) return;
|
|
delegate.cancelled = YES;
|
|
[delegate.picker dismissViewControllerAnimated:YES completion:nil];
|
|
[delegate finish:@"" error:@""];
|
|
});
|
|
}
|
|
|
|
void flectar_choose_document(long long requestId, const char *exportPath, FlectarDocumentCallback callback) {
|
|
NSString *path = exportPath ? [NSString stringWithUTF8String:exportPath] : @"";
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (!requests) requests = [NSMutableDictionary dictionary];
|
|
UIWindow *window = nil;
|
|
if (@available(iOS 13.0, *)) {
|
|
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:UIWindowScene.class]) {
|
|
for (UIWindow *candidate in ((UIWindowScene *)scene).windows) {
|
|
if (candidate.isKeyWindow) { window = candidate; break; }
|
|
}
|
|
}
|
|
if (window) break;
|
|
}
|
|
}
|
|
if (!window) window = UIApplication.sharedApplication.keyWindow;
|
|
UIViewController *presenter = window.rootViewController;
|
|
while (presenter.presentedViewController) presenter = presenter.presentedViewController;
|
|
if (!presenter || requests.count) { callback(requestId, "", "A document picker cannot be opened right now."); return; }
|
|
FlectarDocumentDelegate *delegate = [FlectarDocumentDelegate new];
|
|
delegate.requestId = requestId;
|
|
delegate.callback = callback;
|
|
delegate.exporting = path.length > 0;
|
|
UIDocumentPickerViewController *picker;
|
|
if (delegate.exporting) {
|
|
picker = [[UIDocumentPickerViewController alloc] initWithURL:[NSURL fileURLWithPath:path]
|
|
inMode:UIDocumentPickerModeExportToService];
|
|
} else {
|
|
picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.data", @"public.content"]
|
|
inMode:UIDocumentPickerModeImport];
|
|
}
|
|
picker.delegate = delegate;
|
|
picker.allowsMultipleSelection = NO;
|
|
delegate.picker = picker;
|
|
requests[@(requestId)] = delegate;
|
|
[presenter presentViewController:picker animated:YES completion:nil];
|
|
picker.presentationController.delegate = delegate;
|
|
});
|
|
}
|