-
-
Notifications
You must be signed in to change notification settings - Fork 907
/
Roots.m
234 lines (213 loc) · 9.36 KB
/
Roots.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//
// Roots.m
// iSH
//
// Created by Theodore Dubois on 6/7/20.
//
#import <FileProvider/FileProvider.h>
#import "Roots.h"
#import "AppGroup.h"
#import "NSObject+SaneKVO.h"
#include "tools/fakefs.h"
static NSURL *RootsDir(void) {
static NSURL *rootsDir;
static dispatch_once_t token;
dispatch_once(&token, ^{
rootsDir = [ContainerURL() URLByAppendingPathComponent:@"roots"];
NSFileManager *manager = [NSFileManager defaultManager];
[manager createDirectoryAtURL:rootsDir
withIntermediateDirectories:YES
attributes:@{}
error:nil];
});
return rootsDir;
}
static NSString *kDefaultRoot = @"Default Root";
@interface Roots ()
@property NSMutableOrderedSet<NSString *> *roots;
@property BOOL updatingDomains;
@property BOOL domainsNeedUpdate;
@property BOOL wantsVersionFile;
@end
@implementation Roots
- (instancetype)init {
if (self = [super init]) {
NSError *error = nil;
NSArray<NSString *> *rootNames = [NSFileManager.defaultManager contentsOfDirectoryAtPath:RootsDir().path error:&error];
NSAssert(error == nil, @"couldn't list roots: %@", error);
self.roots = [rootNames mutableCopy];
if (!self.roots.count) {
// import default root
NSError *error;
if (![self importRootFromArchive:[NSBundle.mainBundle URLForResource:@"root" withExtension:@"tar.gz"]
name:@"default"
error:&error
progressReporter:nil]) {
NSAssert(NO, @"failed to import default root, error %@", error);
}
_wantsVersionFile = YES;
}
[self observe:@[@"roots"] options:0 owner:self usingBlock:^(typeof(self) self) {
if (self.defaultRoot == nil && self.roots.count)
self.defaultRoot = self.roots[0];
[self syncFileProviderDomains];
}];
[self syncFileProviderDomains];
if ((!self.defaultRoot || ![self.roots containsObject:self.defaultRoot]) && self.roots.count)
self.defaultRoot = self.roots.firstObject;
}
return self;
}
- (NSString *)defaultRoot {
return [NSUserDefaults.standardUserDefaults stringForKey:kDefaultRoot];
}
- (void)setDefaultRoot:(NSString *)defaultRoot {
[NSUserDefaults.standardUserDefaults setObject:defaultRoot forKey:kDefaultRoot];
}
- (NSURL *)rootUrl:(NSString *)name {
return [RootsDir() URLByAppendingPathComponent:name];
}
- (void)syncFileProviderDomains {
if (self.updatingDomains) {
self.domainsNeedUpdate = YES;
return;
}
self.updatingDomains = YES;
self.domainsNeedUpdate = NO;
[NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray<NSFileProviderDomain *> *domains, NSError *error) {
void (^onError)(NSError *error) = ^(NSError *error) {
if (error != nil)
NSLog(@"error adjusting domains: %@", error);
};
onError(error);
NSMutableOrderedSet<NSString *> *missingRoots = [self.roots mutableCopy];
for (NSFileProviderDomain *domain in domains) {
if ([missingRoots containsObject:domain.identifier]) {
[missingRoots removeObject:domain.identifier];
} else {
[NSFileManager.defaultManager removeItemAtURL:
[NSFileProviderManager.defaultManager.documentStorageURL
URLByAppendingPathComponent:domain.pathRelativeToDocumentStorage]
error:nil];
[NSFileProviderManager removeDomain:domain completionHandler:onError];
}
}
for (NSString *rootId in missingRoots) {
[NSFileProviderManager addDomain:[[NSFileProviderDomain alloc] initWithIdentifier:rootId
displayName:rootId
pathRelativeToDocumentStorage:rootId]
completionHandler:onError];
}
if (self.domainsNeedUpdate)
[self syncFileProviderDomains];
self.updatingDomains = NO;
}];
}
- (BOOL)accessInstanceVariablesDirectly {
return YES;
}
void root_progress_callback(void *cookie, double progress, const char *message, bool *should_cancel) {
id <ProgressReporter> reporter = (__bridge id<ProgressReporter>) 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<ProgressReporter> _Nullable)progress {
NSAssert(![self.roots containsObject:name], @"root already exists: %@", name);
struct fakefsify_error fs_err;
NSURL *destination = [self rootUrl:name];
NSURL *tempDestination = [NSFileManager.defaultManager.temporaryDirectory
URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
if (tempDestination == nil)
return NO;
if (!fakefs_import(archive.fileSystemRepresentation,
tempDestination.fileSystemRepresentation,
&fs_err, (struct progress) {(__bridge void *) progress, root_progress_callback})) {
NSString *domain = NSPOSIXErrorDomain;
if (fs_err.type == ERR_SQLITE)
domain = @"SQLite";
*error = [NSError errorWithDomain:domain
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;
void (^addRoot)(void) = ^{
[[self mutableOrderedSetValueForKey:@"roots"] addObject:name];
};
if (!NSThread.isMainThread)
dispatch_sync(dispatch_get_main_queue(), addRoot);
else
addRoot();
return YES;
}
- (BOOL)exportRootNamed:(NSString *)name toArchive:(NSURL *)archive error:(NSError **)error progressReporter:(id<ProgressReporter> _Nullable)progress {
NSAssert([self.roots containsObject:name], @"trying to export a root that doesn't exist: %@", name);
struct fakefsify_error fs_err;
if (!fakefs_export([self rootUrl:name].fileSystemRepresentation,
archive.fileSystemRepresentation,
&fs_err, (struct progress) {(__bridge void *) progress, root_progress_callback})) {
// TODO: dedup with above method
NSString *domain = NSPOSIXErrorDomain;
if (fs_err.type == ERR_SQLITE)
domain = @"SQLite";
*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;
}
return YES;
}
- (BOOL)destroyRootNamed:(NSString *)name error:(NSError **)error {
if ([name isEqualToString:self.defaultRoot]) {
*error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Cannot delete the default filesystem"}];
return NO;
}
NSAssert([self.roots containsObject:name], @"root does not exist: %@", name);
if (![NSFileManager.defaultManager removeItemAtURL:[self rootUrl:name] error:error])
return NO;
[[self mutableOrderedSetValueForKey:@"roots"] removeObject:name];
return YES;
}
- (BOOL)renameRoot:(NSString *)name toName:(NSString *)newName error:(NSError **)error {
if (name.length == 0) {
*error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Filesystem name can't be empty"}];
return NO;
}
if ([name containsString:@"/"]) {
*error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Filesystem name can't contain /"}];
return NO;
}
if ([name isEqualToString:@"."] || [name isEqualToString:@".."]) {
*error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Filesystem name can't be . or .."}];
return NO;
}
if ([name isEqualToString:self.defaultRoot]) {
*error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Cannot rename the default filesystem"}];
return NO;
}
NSAssert([self.roots containsObject:name], @"root does not exist: %@", name);
if (![NSFileManager.defaultManager moveItemAtURL:[self rootUrl:name] toURL:[self rootUrl:newName] error:error])
return NO;
NSUInteger index = [self.roots indexOfObject:name];
[[self mutableOrderedSetValueForKey:@"roots"] replaceObjectAtIndex:index withObject:newName];
return YES;
}
+ (instancetype)instance {
static Roots *instance;
static dispatch_once_t token;
dispatch_once(&token, ^{
instance = [Roots new];
});
return instance;
}
@end