NSFileWrapper returns nil, sometimes - macos

I'm using NSFileWrapper for my package document. Sometimes, when I request the data of a file inside the package I get nil.
This is how I query the data of a file inside the package:
- (NSData*) dataOfFile(NSString*)filename {
NSFileWrapper *fileWrapper = [self.documentFileWrapper.fileWrappers objectForKey:filename];
return fileWrapper.regularFileContents; // This returns nil sometimes. Why?
}
This method eventually starts returning nil for some files (not all). Sadly, I haven't managed to reproduce the problem consistently.
In case it helps, this is how I open the package:
- (BOOL) readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
self.documentFileWrapper = fileWrapper;
return YES;
}
This is how I update the data of a file inside the package:
- (void) updateFile:(NSString*)filename withData:(NSData*)data {
SBFileWrapper *fileWrapper = [self.documentFileWrapper.fileWrappers objectForKey:filename];
if (fileWrapper) {
[self.documentFileWrapper removeFileWrapper:fileWrapper];
}
NSFileWrapper *fileWrapper = [[SBFileWrapper alloc] initRegularFileWithContents:data ];
fileWrapper.preferredFilename = filename;
[self.documentFileWrapper addFileWrapper:fileWrapper];
}
This is how I save the package:
- (NSFileWrapper*) fileWrapperOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
return self.documentFileWrapper;
}
Why can this be happening? Is there a way to prevent it?
The documentation of regularFileContents appears to talk about this problem:
This method may return nil if the user modifies the file after you
call readFromURL:options:error: or initWithURL:options:error: but
before NSFileWrapper has read the contents of the file. Use the
NSFileWrapperReadingImmediate reading option to reduce the likelihood
of that problem.
But I don't understand what has to be changed in the code above to prevent this situation.
Failed Experiments
I tried saving the document if regularFileContents return nil but it still returns nil afterwards. Like this:
- (NSData*) dataOfFile(NSString*)filename {
NSFileWrapper *fileWrapper = [self.documentFileWrapper.fileWrappers objectForKey:filename];
NSData *data = fileWrapper.regularFileContents;
if (!data) {
[self saveDocument:nil];
fileWrapper = [self.documentFileWrapper.fileWrappers objectForKey:filename];
data = fileWrapper.regularFileContents;
}
return data;
}

There is not enough code to see what's really going on. However the root cause is that NSFileWrapper is just what its name implies: an object that represents a file or directory. Therefore, the actual file or directory can easily get "out of synch" with the object, which lives in memory. Whenever NSFileWrapper determines that this has occurred, it returns nil for certain operations. The solution is to make NSFileWrapper objects short-lived. Create and open just when you need them and then save and close as soon as possible.
In particular, it looks like your code is keeping a pointer to a package directory wrapper around for a long time and assuming that it's always valid. If the directory changes for any reason, this isn't the case. Recode so that you get a fresh package directory wrapper each time you need it, and the problem ought to go away.

If the file changes on disk then you'll get nil (as #Gene says). However, you can check this by using matchesContentsOfURL: method which:
determines whether a disk representation may have changed, based on the file attributes stored the last time the file was read or written. If the file wrapper’s modification time or access permissions are different from those of the file on disk, this method returns YES. You can then use readFromURL:options:error:
This from Working with File Wrappers Apple documentation.
Note this from the intro to that section:
Because the purpose of a file wrapper is to represent files in memory, it’s very loosely coupled to any disk representation. A file wrapper doesn’t record the path to the disk representation of its contents. This allows you to save the same file wrapper with different URLs, but it also requires you to record those URLs if you want to update the file wrapper from disk later.
So you'll have to save the url to the original file if you want/need to re-read it.
Be interesting to hear what matchesContentsofURL: returns when you're seeing nil results.

Related

NSData dataWithContentsOfURL returning nil specifically in iOS 8

I am tring to get the NSData from a local file using [NSData dataWithContentsOfURL:] but am getting nil even when the fileSize returned by [NSFileManager defaultManager] is a positive integer.
Surprisingly, I got this issue when the base sdk was increased from iOS 7 to iOS 8.
I am not sure whether it is an iOS bug or what as it worked in iOS 7 base SDK and not with iOS 8, but here is the solution I found after wasting a couple of hours debugging the code :-(
Use [NSData dataWithContentsOfFile:(NSString *)] instead of dataWithContentsOfURL
Are you sure you pass the Bundle path as well ?
Try with this
NSString* path = [[NSBundle mainBundle].bundlePath stringByAppendingPathComponent:yourPath];
NSData* data = [NSData dataWithContentsOfURL:path];
NSData and URLs: There be dragons
+[NSData dataWithContentsOfURL:] can return nil for any number of reasons. If anything goes wrong when attempting to use that URL this method will return nil. For example the file may not exist, the network connection may time out, or the URL may even be malformed. nil is returned and the application has no idea why.
+ [NSData dataWithContentsOfURL:options:error:] on the other hand, will tell the caller what went wrong. When nil is returned the error argument will be populated with an object that describes the problem that occured. Using this method would directly answer the question of "why".
Both of these are synchronous methods and their use for working with files, network resources, and especially files served from a network resource is discouraged. These methods will block the caller and are not really intended for these kinds of uses. It's better to use an input stream or NSURLSession instead.
you can reference enter link description here
My case. it had a space in the URL
remove space

NSFileManager removeItemAtPath:error: does not respect POSIX permissions

I have some code that checks for the existence of a file and, if it is present, deletes it. The problem is, I am unable to get it to fail even if the file should not be writeable. My code looks like:
if([theManager fileExistsAtPath:savingAs isDirectory:&destIsDir])
{
BOOL itemRemoved=[theManager removeItemAtPath:savingAs error:&err];
if(!itemRemoved)
{
// why?
NSAlert *rebuildAlert=[NSAlert alertWithMessageText:#"Error removing item"
defaultButton:nil alternateButton:nil otherButton:nil
informativeTextWithFormat:#"%#",[err localizedDescription]];
[rebuildAlert runModal];
proceed=NO;
}
}
Even if I set ownership to root:wheel and mode to 000 (i.e. not readable, writeable or executable by anyone) the file is still silently deleted. The account I'm running this from is a user account with Admin privileges but even so, being able to kill files owned by root doesn't seem very safe. The only way to throw an error is to lock the file using chflags uchg filename. I have also implemented (as a stub for now) the fileManager:shouldRemoveItemAtPath: delegate method where I could check permissions if necessary. The problem is that returning NO from this method does not cause removeItemAtPath: to return an error. Re-checking with fileExistsAtPath: seems cumbersome. Finally, there doesn't seem to be a simple method of disambiguating which instance of NSFileManager is issuing the call to removeItemAtPath: in the delegate method. Typically these instances are transitory objects so their id's are not valid for any significant length of time. I could sub-class NSFileManager and add a tag instance variable but that seems like a sledgehammer to crack a nut.
In summary:
1) is it correct behaviour for removeItemAtPath to ignore files it does not own?
2) disallowing file deletion in the delegate method is not communicated back to the caller of removeItemAtPath
3) determining which invocation is calling the delegate method is hard
This is the correct behavior. Erasing a file does not require read access to the file, only the directory containing it. Think of a directory as a list of files, and erasing a file as simply removing it from that list, and it will all make sense.

How to properly save a QTMovie after editing using QTKit?

I am making minor edits to a QTMovie in an application using NSDocument architecture (such as adding a track, as shown below). After the edit, I want to save to the original file. However, I keep getting a 'file is busy' error. I assume this is due to some oversight I made in the handling of the files, or a failure in how I am using NSDocument. Any tips would be helpful! Here is (some of) the relevant code:
// open file that has the track I want to add to my movie
QTMovie* tempMovie = [QTMovie movieWithURL:outputFileURL error:nil];
// Use old API to add the track
AddMovieSelection([movie quickTimeMovie], [tempMovie quickTimeMovie]);
// get the filename from the NSDocument
NSString *filename = [[self fileURL] path];
NSLog(#"writing to filename: %#", filename);
// FLATTEN the movie file so I don't get external references
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:1];
[attributes setObject:[NSNumber numberWithBool:YES] forKey:QTMovieFlatten];
// attempt to write
NSError *error;
// this is where I get the "file is busy"
if (![movie writeToFile:filename withAttributes:attributes error:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
NSRunAlertPanel(#"Error", [error localizedDescription], nil, nil, nil);
}
Do I have to first release the movie in my NSDocument? What is the "proper" way to do that? Keep in mind, I am not necessarily finished with this document, I am not closing it. I have just finished this operation, and I want the file on disk to reflect my changes. I would love to use [movie updateMovieFile], but that call doesn't seem to flatten the movie. I don't want any external references in my file.
I am not too familiar with the QuickTime C API, so I honestly can't tell you anything about what is going wrong there. Absolute guesswork: Maybe a call to EndMediaEdits is missing?
Though that shouldn't be required by AddMovieSelection, you said "[...] such as adding a track [...]". So maybe there is something else going on, like AddMediaSample or something similar?
That said, if you don't need to target anything below 10.5 and all you need to do is add some segment from another movie, you can achieve that without dropping down to the C API:
Have a look at
-[QTMovie insertSegmentOfTrack:fromRange:scaledToRange:]
and
-[QTMovie insertSegmentOfMovie:fromRange:scaledToRange:], if you want to have the inserted segment "overlayed" (temporally speaking).
-[QTMovie insertSegmentOfMovie:timeRange:atTime:] and -[QTMovie insertSegmentOfTrack:timeRange:atTime:], if you want { movieA.firstPart, movieB, movieA.secondPart }.
Do I have to first release the movie in my NSDocument?
You mean in order to write it to disk? No: That should even result in a crash.
The role of release is to handle memory-management. It doesn't have much to do with the busy-state of a file.
Turns out I just wasn't using the NSDocument architecture properly. When I changed it to use Save/SaveAs properly, this problem went away.

"Open With" menu for remote files

I'm getting troubles getting the applications associated with a remote file how has a URL like: "http://servername/folder/file.png".
The code I use is:
...
NSURL *url = [NSURL fileURLWithPath:#"http://servername/folder/file.png"];
NSArray *apps = (NSArray *)LSCopyApplicationURLsForURL((CFURLRef) url,kLSRolesAll);
...
I also try using URLWithString: but the result is the same.
What is the correct approach for this?
Edit after Joshua replay:
Sorry for may bad redaction. The problem is that the LSCopyApplicationURLsForURL call return nil.
First, -fileURLWithPath: expects a file system path like #"/Users/me/Desktop/MyFile.png".
Second, you don't say what the "same" results are and what is wrong with them, so it's difficult to tell you what the problem is.
The docs say this:
If the item URL’s scheme is file
(designating either a file or a
directory), the selection of suitable
applications is based on the
designated item’s filename extension,
file type, and creator signature,
along with the role specified by the
inRolesMask parameter; otherwise, it
is based on the URL scheme (such as
http, ftp, or mailto).
So what are you getting and what do you expect to get?

How do I create an import-only document type in Cocoa?

There's a file type my application import but not save. I've added an entry to the document types and set it to read-only, but that doesn't yield the import behaviour that I'm looking for. Instead, my app will just open the file and when I save the original file is overwritten in my own file format.
How to set up my document or document types to make it so that a new document is created with the data from the original document, instead of the original being opened?
1. Declare the file types as Document Types
Within your Xcode project, add a Document Type for all the file formats your application supports. Set the Role of each type according to your application's abilities:
Mark read/write capable file types as Editor;
Mark import only file types as Viewer.
Set the Class to the document type you want to handle each file type. One document class can handle multiple file types.
In the example below, there are three file types declared: font-pestle, otf, and ttf. The first, font-pestle, is the native format of the application. This type has the role Editor.
The remaining two formats, otf and ttf, can be imported but not written by the application; thus they are marked as Viewer.
2. Additional file types in your NSDocument subclass
With the Document Types added, the application will automatically allow users to open files of the specified types.
You need to add file type handling code to your document class. In the ideal case, add the branching code to the readFromData:ofType:error: method:
- (BOOL)readFromData:(NSData*)someData ofType:(NSString*)typeName error:(NSError**)outError
{
if ([NSWorkspace.sharedWorkspace type:#"eu.miln.font-pestle" conformsToType:typeName] == YES)
{
// read native format
}
else if ([NSWorkspace.sharedWorkspace type:#"public.opentype-font" conformsToType:typeName] == YES)
{
// read import only format
// disassociate document from file; makes document "untitled"
self.fileURL = nil;
// associate with primary file type
self.fileType = #"eu.miln.font-pestle";
}
else // ...
}
The self.fileURL = nil; is important. By setting fileURL to nil, you are saying the document is not associated with any on-disk file and should be treated as a new document.
To allow auto-saving, implement the NSDocument method autosavingFileType to return the primary file type.
Alex, thanks for your answer, but I found a way that I like a bit more:
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName
error:(NSError **)outError
{
*outError = nil;
if ([typeName isEqualToString:#"SomeReadOnlyType"])
{
// .. (load data here)
[self setFileURL:nil];
return result;
}
else
{
// .. (do whatever you do for other documents here)
}
}
This way it's still possible to use the document system provided by Cocoa instead fo rolling my own.
I've also documented the solution here: http://www.cocoadev.com/index.pl?CFBundleTypeRole a bit down the page.
I don't believe that import functionality is supported by default in Cocoa. When the user clicks the Open button in the open panel, the framework calls openDocumentWithContentsOfURL:display:error: on NSDocumentController. This is where the document system figures out what type of file you're opening and consults with the Info.plist file to figure out which NSDocument subclass to use to open the document.
You could subclass NSDocumentController and override the openDocumentWithContentsOfURL:display:error: method to intercept the file types that should be imported rather than opened. In your NSDocument subclass, write a new initializer with a name like initWithImportedContentsOfURL:type:error: (or something with a better name :-) ) to create a new untitled document and read in the contents of the imported file.

Resources