forked from karelia/BSManagedDocument
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathBSManagedDocument.m
1109 lines (899 loc) · 45.9 KB
/
BSManagedDocument.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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// BSManagedDocument.m
//
// Created by Sasmito Adibowo on 29-08-12.
// Rewritten by Mike Abdullah on 02-11-12.
// Copyright (c) 2012-2013 Karelia Software, Basil Salad Software. All rights reserved.
// http://basilsalad.com
//
// Licensed under the BSD License <http://www.opensource.org/licenses/bsd-license>
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#import "BSManagedDocument.h"
@interface BSManagedDocument ()
@property(nonatomic, copy) NSURL *autosavedContentsTempDirectoryURL;
@end
@implementation BSManagedDocument
#pragma mark UIManagedDocument-inspired methods
+ (NSString *)storeContentName; { return @"StoreContent"; }
+ (NSString *)persistentStoreName; { return @"persistentStore"; }
+ (NSURL *)persistentStoreURLForDocumentURL:(NSURL *)fileURL;
{
NSString *storeContent = [self storeContentName];
if (storeContent) fileURL = [fileURL URLByAppendingPathComponent:storeContent];
fileURL = [fileURL URLByAppendingPathComponent:[self persistentStoreName]];
return fileURL;
}
- (NSManagedObjectContext *)managedObjectContext;
{
if (!_managedObjectContext)
{
// Need 10.7+ to support concurrency types
__block NSManagedObjectContext *context;
if ([NSManagedObjectContext instancesRespondToSelector:@selector(initWithConcurrencyType:)])
{
context = [[self.class.managedObjectContextClass alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
}
else
{
// On 10.6, context MUST be created on the thread/queue that's going to use it
if ([NSThread isMainThread])
{
context = [[self.class.managedObjectContextClass alloc] init];
}
else
{
dispatch_sync(dispatch_get_main_queue(), ^{
context = [[self.class.managedObjectContextClass alloc] init];
});
}
}
[self setManagedObjectContext:context];
#if ! __has_feature(objc_arc)
[context release];
#endif
}
return _managedObjectContext;
}
- (void)setManagedObjectContext:(NSManagedObjectContext *)context;
{
// Setup the rest of the stack for the context
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
// Need 10.7+ to support parent context
if ([context respondsToSelector:@selector(setParentContext:)])
{
NSManagedObjectContext *parentContext = [[self.class.managedObjectContextClass alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
parentContext.undoManager = nil; // no point in it supporting undo
parentContext.persistentStoreCoordinator = coordinator;
[context setParentContext:parentContext];
#if !__has_feature(objc_arc)
[parentContext release];
#endif
}
else
{
[context setPersistentStoreCoordinator:coordinator];
}
#if __has_feature(objc_arc)
_managedObjectContext = context;
#else
[context retain];
[_managedObjectContext release]; _managedObjectContext = context;
#endif
#if !__has_feature(objc_arc)
[coordinator release]; // context hangs onto it for us
#endif
[super setUndoManager:[context undoManager]]; // has to be super as we implement -setUndoManager: to be a no-op
}
// Having this method is a bit of a hack for Sandvox's benefit. I intend to remove it in favour of something neater
+ (Class)managedObjectContextClass; { return [NSManagedObjectContext class]; }
- (NSManagedObjectModel *)managedObjectModel;
{
if (!_managedObjectModel)
{
_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSArray arrayWithObject:[NSBundle mainBundle]]];
#if ! __has_feature(objc_arc)
[_managedObjectModel retain];
#endif
}
return _managedObjectModel;
}
/* Called whenever a document is opened *and* when a new document is first saved.
*/
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)storeURL
ofType:(NSString *)fileType
modelConfiguration:(NSString *)configuration
storeOptions:(NSDictionary *)storeOptions
error:(NSError **)error
{
NSPersistentStoreCoordinator *storeCoordinator = [[self managedObjectContext] persistentStoreCoordinator];
_store = [storeCoordinator addPersistentStoreWithType:[self persistentStoreTypeForFileType:fileType]
configuration:configuration
URL:storeURL
options:storeOptions
error:error];
#if ! __has_feature(objc_arc)
[_store retain];
#endif
return (_store != nil);
}
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)storeURL
ofType:(NSString *)fileType
error:(NSError **)error
{
// On 10.8+, the coordinator whinges but doesn't fail if you leave out NSReadOnlyPersistentStoreOption and the file turns out to be read-only. Supplying a value makes it fail with a (not very helpful) error when the store is read-only
BOOL readonly = ([self respondsToSelector:@selector(isInViewingMode)] && [self isInViewingMode]);
NSDictionary *options = @{
// For apps linked against 10.9+ and supporting 10.6 still, use the old
// style journal. Since the journal lives alongside the persistent store
// I figure there's a chance it could be copied from a new Mac to an old one
// https://developer.apple.com/library/mac/releasenotes/DataManagement/WhatsNew_CoreData_OSX/index.html
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9 && MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_6
NSSQLitePragmasOption : @{ @"journal_mode" : @"DELETE" },
#endif
NSReadOnlyPersistentStoreOption : @(readonly)
};
return [self configurePersistentStoreCoordinatorForURL:storeURL
ofType:fileType
modelConfiguration:nil
storeOptions:options
error:error];
}
- (NSString *)persistentStoreTypeForFileType:(NSString *)fileType { return NSSQLiteStoreType; }
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError **)error; { return YES; }
- (id)additionalContentForURL:(NSURL *)absoluteURL saveOperation:(NSSaveOperationType)saveOperation error:(NSError **)error;
{
// Need to hand back something so as not to indicate there was an error
return [NSNull null];
}
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error;
{
return YES;
}
#pragma mark Core Data-Specific
- (BOOL)updateMetadataForPersistentStore:(NSPersistentStore *)store error:(NSError **)error;
{
return YES;
}
#pragma mark Lifecycle
- (void)close;
{
[super close];
[self deleteAutosavedContentsTempDirectory];
}
// It's simpler to wrap the whole method in a conditional test rather than using a macro for each line.
#if ! __has_feature(objc_arc)
- (void)dealloc;
{
[_managedObjectContext release];
[_managedObjectModel release];
[_store release];
// _additionalContent is unretained so shouldn't be released here
[super dealloc];
}
#endif
#pragma mark Reading Document Data
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
// Preflight the URL
// A) If the file happens not to exist for some reason, Core Data unhelpfully gives "invalid file name" as the error. NSURL gives better descriptions
// B) When reverting a document, the persistent store will already have been removed by the time we try adding the new one (see below). If adding the new store fails that most likely leaves us stranded with no store, so it's preferable to catch errors before removing the store if possible
if (![absoluteURL checkResourceIsReachableAndReturnError:outError]) return NO;
// If have already read, then this is a revert-type affair, so must reload data from disk
if (_store)
{
if (!([NSThread isMainThread])) {
[NSException raise:NSInternalInconsistencyException format:@"%@: I didn't anticipate reverting on a background thread!", NSStringFromSelector(_cmd)];
}
// NSPersistentDocument states: "Revert resets the document’s managed object context. Objects are subsequently loaded from the persistent store on demand, as with opening a new document."
// I've found for atomic stores that -reset only rolls back to the last loaded or saved version of the store; NOT what's actually on disk
// To force it to re-read from disk, the only solution I've found is removing and re-adding the persistent store
NSManagedObjectContext *context = self.managedObjectContext;
if ([context respondsToSelector:@selector(parentContext)])
{
// In my testing, HAVE to do the removal using parent's private queue. Otherwise, it deadlocks, trying to acquire a _PFLock
NSManagedObjectContext *parent = context.parentContext;
while (parent)
{
context = parent; parent = context.parentContext;
}
__block BOOL result;
[context performBlockAndWait:^{
result = [context.persistentStoreCoordinator removePersistentStore:_store error:outError];
}];
}
else
{
if (![context.persistentStoreCoordinator removePersistentStore:_store error:outError])
{
return NO;
}
}
#if !__has_feature(objc_arc)
[_store release];
#endif
_store = nil;
}
// Setup the store
// If the store happens not to exist, because the document is corrupt or in the wrong format, -configurePersistentStoreCoordinatorForURL:… will create a placeholder file which is likely undesirable! The only way to avoid that that I can see is to preflight the URL. Possible race condition, but not in any truly harmful way
NSURL *storeURL = [[self class] persistentStoreURLForDocumentURL:absoluteURL];
if (![storeURL checkResourceIsReachableAndReturnError:outError])
{
// The document architecture presents such an error as "file doesn't exist", which makes no sense to the user, so customize it
if (outError && [*outError code] == NSFileReadNoSuchFileError && [[*outError domain] isEqualToString:NSCocoaErrorDomain])
{
*outError = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFileReadCorruptFileError
userInfo:@{ NSUnderlyingErrorKey : *outError }];
}
return NO;
}
BOOL result = [self configurePersistentStoreCoordinatorForURL:storeURL
ofType:typeName
error:outError];
if (result)
{
result = [self readAdditionalContentFromURL:absoluteURL error:outError];
}
return result;
}
#pragma mark Writing Document Data
- (id)contentsForURL:(NSURL *)url ofType:(NSString *)typeName saveOperation:(NSSaveOperationType)saveOperation error:(NSError **)outError;
{
NSAssert([NSThread isMainThread], @"Somehow -%@ has been called off of the main thread (operation %u to: %@)", NSStringFromSelector(_cmd), (unsigned)saveOperation, [url path]);
// Grab additional content that a subclass might provide
if (outError) *outError = nil; // unusually for me, be forgiving of subclasses which forget to fill in the error
id additionalContent = [self additionalContentForURL:url saveOperation:saveOperation error:outError];
if (!additionalContent)
{
if (outError) NSAssert(*outError != nil, @"-additionalContentForURL:saveOperation:error: failed with a nil error");
return nil;
}
// On 10.7+, save the main context, ready for parent to be saved in a moment
NSManagedObjectContext *context = self.managedObjectContext;
if ([context respondsToSelector:@selector(parentContext)])
{
if (![context save:outError]) return nil;
}
// What we consider to be "contents" is actually a worker block
BOOL (^contents)(NSURL *, NSSaveOperationType, NSURL *, NSError**) = ^(NSURL *url, NSSaveOperationType saveOperation, NSURL *originalContentsURL, NSError **error) {
// For the first save of a document, create the folders on disk before we do anything else
// Then setup persistent store appropriately
BOOL result = YES;
NSURL *storeURL = [self.class persistentStoreURLForDocumentURL:url];
if (!_store)
{
result = [self createPackageDirectoriesAtURL:url
ofType:typeName
forSaveOperation:saveOperation
originalContentsURL:originalContentsURL
error:error];
if (!result) return NO;
result = [self configurePersistentStoreCoordinatorForURL:storeURL
ofType:typeName
error:error];
if (!result) return NO;
}
else if (saveOperation == NSSaveAsOperation)
{
result = [self createPackageDirectoriesAtURL:url
ofType:typeName
forSaveOperation:saveOperation
originalContentsURL:originalContentsURL
error:error];
if (!result) return NO;
/* Save As for an existing store should be special, migrating the store instead of saving
However, in our testing it can cause the next save to blow up if you go:
1. New doc
2. Autosave
3. Save (As)
4. Save
The last step will throw an exception claiming "Object's persistent store is not reachable from this NSManagedObjectContext's coordinator".
NSPersistentStoreCoordinator *coordinator = [_store persistentStoreCoordinator];
[coordinator lock]; // so it knows it's in use
@try
{
NSPersistentStore *migrated = [coordinator migratePersistentStore:_store
toURL:storeURL
options:nil
withType:[self persistentStoreTypeForFileType:typeName]
error:error];
if (!migrated) return NO;
#if ! __has_feature(objc_arc)
[migrated retain];
[_store release];
#endif
_store = migrated;
}
@finally
{
[coordinator unlock];
}
*/
// Instead, we shall fallback to copying the store to the new location
// -writeStoreContent… routine will adjust store URL for us
if (![[NSFileManager defaultManager] copyItemAtURL:[self.class persistentStoreURLForDocumentURL:self.mostRecentlySavedFileURL]
toURL:storeURL
error:error]) return NO;
}
else
{
if (self.class.autosavesInPlace)
{
if (saveOperation == NSAutosaveElsewhereOperation)
{
// Special-case autosave-elsewhere for 10.7+ documents that have been saved
// e.g. reverting a doc that has unautosaved changes
// The system asks us to autosave it to some temp location before closing
// CAN'T save-in-place to achieve that, since the doc system is expecting us to leave the original doc untouched, ready to load up as the "reverted" version
// But the doc system also asks to do this when performing a Save As operation, and choosing to discard unsaved edits to the existing doc. In which case the SQLite store moves out underneath us and we blow up shortly after
// Doc system apparently considers it fine to fail at this, since it passes in NULL as the error pointer
// With great sadness and wretchedness, that's the best workaround I have for the moment
NSURL *fileURL = self.fileURL;
if (fileURL)
{
NSURL *autosaveURL = self.autosavedContentsFileURL;
if (!autosaveURL)
{
// Make a copy of the existing doc to a location we control first
NSURL *autosaveTempDirectory = self.autosavedContentsTempDirectoryURL;
NSAssert(autosaveTempDirectory == nil, @"Somehow have a temp directory, but no knowledge of a doc inside it");
autosaveTempDirectory = [[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:fileURL
create:YES
error:error];
if (!autosaveTempDirectory) return NO;
self.autosavedContentsTempDirectoryURL = autosaveTempDirectory;
autosaveURL = [autosaveTempDirectory URLByAppendingPathComponent:fileURL.lastPathComponent];
if (![self writeBackupToURL:autosaveURL error:error]) return NO;
self.autosavedContentsFileURL = autosaveURL;
}
// Bring the autosaved doc up-to-date
result = [self writeStoreContentToURL:[self.class persistentStoreURLForDocumentURL:autosaveURL]
error:error];
if (!result) return NO;
result = [self writeAdditionalContent:additionalContent
toURL:autosaveURL
originalContentsURL:originalContentsURL
error:error];
if (!result) return NO;
// Then copy that across to the final URL
return [self writeBackupToURL:url error:error];
}
}
}
else
{
if (saveOperation != NSSaveOperation && saveOperation != NSAutosaveInPlaceOperation)
{
if (![storeURL checkResourceIsReachableAndReturnError:NULL])
{
result = [self createPackageDirectoriesAtURL:url
ofType:typeName
forSaveOperation:saveOperation
originalContentsURL:originalContentsURL
error:error];
if (!result) return NO;
// Fake a placeholder file ready for the store to save over
if (![[NSData data] writeToURL:storeURL options:0 error:error]) return NO;
}
}
}
}
// Right, let's get on with it!
if (![self writeStoreContentToURL:storeURL error:error]) return NO;
result = [self writeAdditionalContent:additionalContent toURL:url originalContentsURL:originalContentsURL error:error];
if (result)
{
// Update package's mod date. Two circumstances where this is needed:
// user requests a save when there's no changes; SQLite store doesn't bother to touch the disk in which case
// saving where +storeContentName is non-nil; that folder's mod date updates, but the overall package needs prompting
// Seems simplest to just apply this logic all the time
NSError *error;
if (![url setResourceValue:[NSDate date] forKey:NSURLContentModificationDateKey error:&error])
{
NSLog(@"Updating package mod date failed: %@", error); // not critical, so just log it
}
}
// Restore persistent store URL after Save To-type operations. Even if save failed (just to be on the safe side)
if (saveOperation == NSSaveToOperation)
{
if (![[_store persistentStoreCoordinator] setURL:originalContentsURL forPersistentStore:_store])
{
NSLog(@"Failed to reset store URL after Save To Operation");
}
}
return result;
};
#if !__has_feature(objc_arc)
return [[contents copy] autorelease];
#else
return [contents copy];
#endif
}
- (BOOL)createPackageDirectoriesAtURL:(NSURL *)url
ofType:(NSString *)typeName
forSaveOperation:(NSSaveOperationType)saveOperation
originalContentsURL:(NSURL *)originalContentsURL
error:(NSError **)error;
{
// Create overall package
NSDictionary *attributes = [self fileAttributesToWriteToURL:url
ofType:typeName
forSaveOperation:saveOperation
originalContentsURL:originalContentsURL
error:error];
if (!attributes) return NO;
BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:[url path]
withIntermediateDirectories:NO
attributes:attributes
error:error];
if (!result) return NO;
// Create store content folder too
NSString *storeContent = self.class.storeContentName;
if (storeContent)
{
NSURL *storeContentURL = [url URLByAppendingPathComponent:storeContent];
result = [[NSFileManager defaultManager] createDirectoryAtPath:[storeContentURL path]
withIntermediateDirectories:NO
attributes:attributes
error:error];
if (!result) return NO;
}
// Set the bundle bit for good measure, so that docs won't appear as folders on Macs without your app installed. Don't care if it fails
[self setBundleBitForDirectoryAtURL:url];
return YES;
}
- (void)saveToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation completionHandler:(void (^)(NSError *))completionHandler
{
// Can't touch _additionalContent etc. until existing save has finished
// At first glance, -performActivityWithSynchronousWaiting:usingBlock: seems the right way to do that. But turns out:
// * super is documented to use -performAsynchronousFileAccessUsingBlock: internally
// * Autosaving (as tested on 10.7) is declared to the system as *file access*, rather than an *activity*, so a regular save won't block the UI waiting for autosave to finish
// * If autosaving while quitting, calling -performActivity… here results in deadlock
[self performAsynchronousFileAccessUsingBlock:^(void (^fileAccessCompletionHandler)(void)) {
NSAssert(_contents == nil, @"Can't begin save; another is already in progress. Perhaps you forgot to wrap the call inside of -performActivityWithSynchronousWaiting:usingBlock:");
// Stash contents temporarily into an ivar so -writeToURL:… can access it from the worker thread
NSError *error;
_contents = [self contentsForURL:url ofType:typeName saveOperation:saveOperation error:&error];
if (!_contents)
{
// The docs say "be sure to invoke super", but by my understanding it's fine not to if it's because of a failure, as the filesystem hasn't been touched yet.
fileAccessCompletionHandler();
if (completionHandler) completionHandler(error);
return;
}
#if !__has_feature(objc_arc)
[_contents retain];
#endif
// Kick off async saving work
[super saveToURL:url ofType:typeName forSaveOperation:saveOperation completionHandler:^(NSError *error) {
// If the save failed, it might be an error the user can recover from.
// e.g. the dreaded "file modified by another application"
// NSDocument handles this by presenting the error, which includes recovery options
// If the user does choose to Save Anyway, the doc system leaps straight onto secondary thread to
// accomplish it, without calling this method again.
// Thus we want to hang onto _contents until the overall save operation is finished, rather than
// just this method. The best way I can see to do that is to make the cleanup its own activity, so
// it runs after the end of the current one. Unfortunately there's no guarantee anyone's been
// thoughtful enough to register this as an activity (autosave, I'm looking at you), so only rely
// on it if there actually is a recoverable error
if ([error recoveryAttempter])
{
[self performActivityWithSynchronousWaiting:NO usingBlock:^(void (^activityCompletionHandler)(void)) {
#if !__has_feature(objc_arc)
[_contents release];
#endif
_contents = nil;
activityCompletionHandler();
}];
}
else
{
#if !__has_feature(objc_arc)
[_contents release];
#endif
_contents = nil;
}
// Clean up our custom autosaved contents directory if appropriate
if (!error &&
(saveOperation == NSSaveOperation || saveOperation == NSAutosaveInPlaceOperation || saveOperation == NSSaveAsOperation))
{
[self deleteAutosavedContentsTempDirectory];
}
// And can finally declare we're done
fileAccessCompletionHandler();
if (completionHandler) completionHandler(error);
}];
}];
}
- (BOOL)saveToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation error:(NSError **)outError;
{
BOOL result = [super saveToURL:url ofType:typeName forSaveOperation:saveOperation error:outError];
if (result &&
(saveOperation == NSSaveOperation || saveOperation == NSAutosaveInPlaceOperation || saveOperation == NSSaveAsOperation))
{
[self deleteAutosavedContentsTempDirectory];
}
return result;
}
/* Regular Save operations can write directly to the existing document since Core Data provides atomicity for us
*/
- (BOOL)writeSafelyToURL:(NSURL *)absoluteURL
ofType:(NSString *)typeName
forSaveOperation:(NSSaveOperationType)saveOperation
error:(NSError **)outError
{
if ([typeName isEqualToString:[self fileType]]) // custom doc types probably want standard saving
{
// At this point, we've either captured all document content, or are writing on the main thread, so it's fine to unblock the UI
if ([self respondsToSelector:@selector(unblockUserInteraction)]) [self unblockUserInteraction];
if (saveOperation == NSSaveOperation || saveOperation == NSAutosaveInPlaceOperation ||
(saveOperation == NSAutosaveElsewhereOperation && [absoluteURL isEqual:[self autosavedContentsFileURL]]))
{
NSURL *backupURL = nil;
// As of 10.8, need to make a backup of the document when saving in-place
// Unfortunately, it turns out 10.7 includes -backupFileURL, just that it's private. Checking AppKit number seems to be our best bet, and I have to hardcode that since 10_8 is not defined in the SDK yet. (1187 was found simply by looking at the GM)
if (NSAppKitVersionNumber >= 1187 &&
[self respondsToSelector:@selector(backupFileURL)] &&
(saveOperation == NSSaveOperation || saveOperation == NSAutosaveInPlaceOperation) &&
[[self class] preservesVersions]) // otherwise backupURL has a different meaning
{
backupURL = [self backupFileURL];
if (backupURL)
{
if (![self writeBackupToURL:backupURL error:outError])
{
// If backup fails, seems it's our responsibility to clean up
NSError *error;
if (![[NSFileManager defaultManager] removeItemAtURL:backupURL error:&error])
{
NSLog(@"Unable to cleanup after failed backup: %@", error);
}
return NO;
}
}
}
// NSDocument attempts to write a copy of the document out at a temporary location.
// Core Data cannot support this, so we override it to save directly.
BOOL result = [self writeToURL:absoluteURL
ofType:typeName
forSaveOperation:saveOperation
originalContentsURL:[self fileURL]
error:outError];
if (!result)
{
// Clean up backup if one was made
// If the failure was actualy NSUserCancelledError thanks to
// autosaving being implicitly cancellable and a subclass deciding
// to bail out, this HAS to be done otherwise the doc system will
// weirdly complain that a file by the same name already exists
if (backupURL)
{
NSError *error;
if (![[NSFileManager defaultManager] removeItemAtURL:backupURL error:&error])
{
NSLog(@"Unable to remove backup after failed write: %@", error);
}
}
// The -write… method maybe wasn't to know that it's writing to the live document, so might have modified it. #179730
// We can patch up a bit by updating modification date so user doesn't get baffling document-edited warnings again!
NSDate *modDate;
if ([absoluteURL getResourceValue:&modDate forKey:NSURLContentModificationDateKey error:NULL])
{
if (modDate) // some file systems don't support mod date
{
[self setFileModificationDate:modDate];
}
}
}
return result;
}
}
// Other situations are basically fine to go through the regular channels
return [super writeSafelyToURL:absoluteURL
ofType:typeName
forSaveOperation:saveOperation
error:outError];
}
- (BOOL)writeBackupToURL:(NSURL *)backupURL error:(NSError **)outError;
{
NSURL *source = self.mostRecentlySavedFileURL;
return [[NSFileManager defaultManager] copyItemAtURL:source toURL:backupURL error:outError];
}
- (BOOL)writeToURL:(NSURL *)inURL
ofType:(NSString *)typeName
forSaveOperation:(NSSaveOperationType)saveOp
originalContentsURL:(NSURL *)originalContentsURL
error:(NSError **)error
{
// Grab additional content before proceeding. This should *only* happen when writing entirely on the main thread
// (e.g. Using one of the old synchronous -save… APIs. Note: duplicating a document calls -writeSafely… directly)
// To have gotten here on any thread but the main one is a programming error and unworkable, so we throw an exception
if (!_contents)
{
_contents = [self contentsForURL:inURL ofType:typeName saveOperation:saveOp error:error];
if (!_contents) return NO;
// Worried that _contents hasn't been retained? Never fear, we'll set it straight back to nil before exiting this method, I promise
// And now we're ready to write for real
BOOL result = [self writeToURL:inURL ofType:typeName forSaveOperation:saveOp originalContentsURL:originalContentsURL error:error];
// Finish up. Don't worry, _additionalContent was never retained on this codepath, so doesn't need to be released
_contents = nil;
return result;
}
// We implement contents as a block which is called to perform the writing
BOOL (^contentsBlock)(NSURL *, NSSaveOperationType, NSURL *, NSError**) = _contents;
return contentsBlock(inURL, saveOp, originalContentsURL, error);
}
- (void)setBundleBitForDirectoryAtURL:(NSURL *)url;
{
#if (defined MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8 // have to check as NSURLIsPackageKey only became writable in 10.8
NSError *error;
if (![url setResourceValue:@YES forKey:NSURLIsPackageKey error:&error])
{
NSLog(@"Error marking document as a package: %@", error);
}
#else
FSRef fileRef;
if (CFURLGetFSRef((CFURLRef)url, &fileRef))
{
FSCatalogInfo fileInfo;
OSErr error = FSGetCatalogInfo(&fileRef, kFSCatInfoFinderInfo, &fileInfo, NULL, NULL, NULL);
if (!error)
{
FolderInfo *finderInfo = (FolderInfo *)fileInfo.finderInfo;
finderInfo->finderFlags |= kHasBundle;
error = FSSetCatalogInfo(&fileRef, kFSCatInfoFinderInfo, &fileInfo);
}
if (error) NSLog(@"OSError %i setting bundle bit for %@", error, [url path]);
}
#endif
}
- (BOOL)writeStoreContentToURL:(NSURL *)storeURL error:(NSError **)error;
{
// First update metadata
__block BOOL result = [self updateMetadataForPersistentStore:_store error:error];
if (!result) return NO;
// On 10.6 saving is just one call, all on main thread. 10.7+ have to work on the context's private queue
NSManagedObjectContext *context = [self managedObjectContext];
if ([context respondsToSelector:@selector(parentContext)])
{
[self unblockUserInteraction];
NSManagedObjectContext *parent = [context parentContext];
[parent performBlockAndWait:^{
result = [self preflightURL:storeURL thenSaveContext:parent error:error];
#if ! __has_feature(objc_arc)
// Errors need special handling to guarantee surviving crossing the block. http://www.mikeabdullah.net/cross-thread-error-passing.html
if (!result && error) [*error retain];
#endif
}];
#if ! __has_feature(objc_arc)
if (!result && error) [*error autorelease]; // tidy up since any error was retained on worker thread
#endif
}
else
{
result = [self preflightURL:storeURL thenSaveContext:context error:error];
}
return result;
}
- (BOOL)preflightURL:(NSURL *)storeURL thenSaveContext:(NSManagedObjectContext *)context error:(NSError **)error;
{
// Preflight the save since it tends to crash upon failure pre-Mountain Lion. rdar://problem/10609036
// Could use this code on 10.7+:
//NSNumber *writable;
//result = [URL getResourceValue:&writable forKey:NSURLIsWritableKey error:&error];
if ([[NSFileManager defaultManager] isWritableFileAtPath:[storeURL path]])
{
// Ensure store is saving to right location
if ([context.persistentStoreCoordinator setURL:storeURL forPersistentStore:_store])
{
return [context save:error];
}
}
if (error)
{
// Generic error. Doc/error system takes care of supplying a nice generic message to go with it
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil];
}
return NO;
}
#pragma mark NSDocument
+ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName { return YES; }
- (BOOL)isEntireFileLoaded { return NO; }
- (BOOL)canAsynchronouslyWriteToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation;
{
return [NSDocument instancesRespondToSelector:_cmd]; // opt in on 10.7+
}
- (void)setFileURL:(NSURL *)absoluteURL
{
// Mark persistent store as moved
if (![self autosavedContentsFileURL])
{
[self setURLForPersistentStoreUsingFileURL:absoluteURL];
}
[super setFileURL:absoluteURL];
}
- (void)setURLForPersistentStoreUsingFileURL:(NSURL *)absoluteURL;
{
if (!_store) return;
NSPersistentStoreCoordinator *coordinator = [[self managedObjectContext] persistentStoreCoordinator];
NSURL *storeURL = [[self class] persistentStoreURLForDocumentURL:absoluteURL];
if (![coordinator setURL:storeURL forPersistentStore:_store])
{
NSLog(@"Unable to set store URL");
}
}
#pragma mark Autosave
/* Enable autosave-in-place and versions browser on 10.7+
*/
+ (BOOL)autosavesInPlace { return [NSDocument respondsToSelector:_cmd]; }
+ (BOOL)preservesVersions { return [self autosavesInPlace]; }
- (void)setAutosavedContentsFileURL:(NSURL *)absoluteURL;
{
[super setAutosavedContentsFileURL:absoluteURL];
// Point the store towards the most recent known URL
absoluteURL = [self mostRecentlySavedFileURL];
if (absoluteURL) [self setURLForPersistentStoreUsingFileURL:absoluteURL];
}
- (NSURL *)mostRecentlySavedFileURL;
{
// Before the user chooses where to place a new document, it has an autosaved URL only
// On 10.6-, autosaves save newer versions of the document *separate* from the original doc
NSURL *result = [self autosavedContentsFileURL];
if (!result) result = [self fileURL];
return result;
}
/*
When asked to autosave an existing doc elsewhere, we do so via an
intermedate, temporary copy of the doc. This code tracks that temp folder
so it can be deleted when no longer in use.
*/
@synthesize autosavedContentsTempDirectoryURL = _autosavedContentsTempDirectoryURL;
- (void)deleteAutosavedContentsTempDirectory;
{
NSURL *autosaveTempDir = self.autosavedContentsTempDirectoryURL;
if (autosaveTempDir)
{
#if ! __has_feature(objc_arc)
[[autosaveTempDir retain] autorelease];
#endif
self.autosavedContentsTempDirectoryURL = nil;
NSError *error;
if (![[NSFileManager defaultManager] removeItemAtURL:autosaveTempDir error:&error])
{
NSLog(@"Unable to remove temporary directory: %@", error);
}
}
}
#pragma mark Reverting Documents
- (BOOL)revertToContentsOfURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError;
{
// Tear down old windows. Wrap in an autorelease pool to get us much torn down before the reversion as we can
@autoreleasepool
{
NSArray *controllers = [[self windowControllers] copy]; // we're sometimes handed underlying mutable array. #156271
for (NSWindowController *aController in controllers)
{
[self removeWindowController:aController];
[aController close];
}
#if ! __has_feature(objc_arc)
[controllers release];
#endif
}
@try
{
if (![super revertToContentsOfURL:absoluteURL ofType:typeName error:outError]) return NO;
[self deleteAutosavedContentsTempDirectory];
return YES;
}
@finally
{
[self makeWindowControllers];
[self showWindows];
}
}
#pragma mark Duplicating Documents
- (NSDocument *)duplicateAndReturnError:(NSError **)outError;
{
// If the doc is brand new, have to force the autosave to write to disk
if (!self.fileURL && !self.autosavedContentsFileURL && !self.hasUnautosavedChanges)
{
[self updateChangeCount:NSChangeDone];
NSDocument *result = [self duplicateAndReturnError:outError];
[self updateChangeCount:NSChangeUndone];
return result;