Checking File sizes for changes - objective-c

I have an app that watches a folder for incoming jobs and then processes them. A job consists of a Folder with several job files inside the folder. Jobs are usually copied over the internet so when a folder is added to my Watched Folder I'm having my app get the attributes for the files inside the job folder, wait 20 seconds, and compare the current attributes for NSFileSize to see if there have been any changes. When everything matches, and no changes are detected it can pass the job folder along to be processed. This is the code I have:
while (fileSizes == NO && fileCount == NO) {
NSLog(#"going for a loop");
NSArray *jobFiles = [fm subpathsAtPath:jobPath];
NSMutableArray *jobFileAttrs = [[NSMutableArray alloc] init];
int i = 0;
while (i < [jobFiles count]) {
NSString *filePath = [jobPath stringByAppendingPathComponent:[jobFiles objectAtIndex:i]];
[jobFileAttrs addObject:[fm attributesOfItemAtPath:filePath error:nil]];
++i;
}
sleep(20);
NSArray *jobFiles2 = [fm subpathsAtPath:jobPath];
NSMutableArray *jobFileAttrs2 = [[NSMutableArray alloc] init];
i = 0;
while (i < [jobFiles2 count]) {
NSString *filePath = [jobPath stringByAppendingPathComponent:[jobFiles2 objectAtIndex:i]];
[jobFileAttrs2 addObject:[fm attributesOfItemAtPath:filePath error:nil]];
++i;
}
if ([jobFiles count] == [jobFiles2 count]) {
i = 0;
fileSizes = YES;
while (i < [jobFiles count]) {
NSLog(#"Does %ul = %ul", [[jobFileAttrs objectAtIndex:i] objectForKey:NSFileSize], [[jobFileAttrs2 objectAtIndex:i] objectForKey:NSFileSize]);
if ([[jobFileAttrs objectAtIndex:i] objectForKey:NSFileSize] != [[jobFileAttrs2 objectAtIndex:i] objectForKey:NSFileSize]){
fileSizes = NO;
}
++i;
}
if (fileSizes)
fileCount = YES;
}
This code works as intended in Lion, but when I run the App on Snow Leopard I get inconsistent values for the NSFileSize attribute. Every time the loop runs I get a completely different set of value than even the previous loop. This is obviously for a folder full of files that is no longer being copied and should give matching values for the file sizes.
Why doesn't this work in Snow Leopard, and what do I need to do to fix this? Part of my problem is I'm only set up for development on a Lion Machine, so I have to make build, and then transfer it to a snow leopard machine with out a debugger to test. It's making it hard for me to trouble shoot.

There are a few problems with this but for one thing, you are comparing the value of two objects using a boolean operator (!=). This is comparing the pointer locations of the two objects, not their value.
To compare two objects, you must use the isEqual: method:
if (![[[jobFileAttrs objectAtIndex:i] objectForKey:NSFileSize] isEqual:[[jobFileAttrs2 objectAtIndex:i] objectForKey:NSFileSize]])
{
fileSizes = NO;
}
Secondly, this is fundamentally bad design. What your code is doing is called polling, and the presence of sleep() in the code is a bad sign. You should never use sleep in Cocoa code, especially if your code is executing on the main thread as it will block the main run loop.
If you absolutely must use polling, you should use an NSTimer object.
However, in this particular case you don't need to use polling to determine when there has been a change to the folder contents, you can use the FSEvents API instead. The API is C-based and a bit obtuse so you might want to use Stu Connolly's SCEvents Objective-C wrapper.
What you should be doing is maintaining an array of the current files and their file sizes (probably as an NSArray instance variable containing dictionaries with keys for file name and file attributes) and then when you are notified of a change on disk, get the current status of the files and compare that with the information in your stored array. You would then replace your stored array with the updated file information.
The other file monitoring option is kqueues. These differ from FSEvents in that they are specific to a particular file, whereas FSEvents monitors directories, not individual files. In your case, kqueues may actually be more appropriate. Uli Kusterer has written a great Objective-C wrapper for kqueues. If you use this, you just need to start monitoring a particular file and you'll be notified whenever it changes via a delegate. This would be a simpler option if you only need to check one file at a time.
I hope this makes sense.

Related

Cocoa: Can I prevent duplicated launching of one same application?

For example, a user puts my application on his/her desktop. Then he/she copied (instead of moving) it to the /Application folder.
If the one at ~/Desktop has been launched, how can I prevent duplicated launching of the one at ~/Application? Any simple way?
Or if the user launches the one in /Application, I can detect the pre-launched app, and then switch to that immediately?
How about getting a list of all running applications and quitting immediately if your app detects there's another instance already running?
You can get a list of all active apps via NSWorkspace and runningApplications:
NSWorkspace * ws = [NSWorkspace sharedWorkspace];
NSArray *allRunningApps = [ws runningApplications];
It'll return an array of NSRunningApplication instances so all you'd have to do is check the list for e.g. your app's ID via the bundleIdentifier method.
cacau's answer had given me inspiration to achieve my goal. Here are my codes (ARC):
- (BOOL)checkAppDuplicateAndBringToFrontWithBundle:(NSBundle *)bundle
{
NSRunningApplication *app;
NSArray *appArray;
NSUInteger tmp;
pid_t selfPid;
BOOL ret = NO;
selfPid = [[NSRunningApplication currentApplication] processIdentifier];
appArray = [NSRunningApplication runningApplicationsWithBundleIdentifier:[bundle bundleIdentifier]];
for (tmp = 0; tmp < [appArray count]; tmp++)
{
app = [appArray objectAtIndex:tmp];
if ([app processIdentifier] == selfPid)
{
/* do nothing */
}
else
{
[[NSWorkspace sharedWorkspace] launchApplication:[[app bundleURL] path]];
ret = YES;
}
}
return ret;
}
It checks app duplicate by bundle identifier. As bundle duplicated, it brings all other applications to front and returns YES. As receiving YES, application can terminate itself.
However, as cacau has given me significant help, I gave him the reputation point of this question. Thank you!
Would it not be simplest to actually just create a file inside NSTempDirectory, check for it and if it is there, you quit, or perhaps offer a way to delete the file ?
This would work for both running in and out of sandbox.

How do I verify a file's existence in iCloud?

I know that the file exists, because I can download it, but I need to check to see whether it exists. I have tried using
[NSFileManager contentsOfDirectoryAtPath:error:]
but it gives me null. I don't understand why that is because I can still download the files that I'm looking for. Maybe it's an incorrect URL, but the URL that I'm using is the one that I printed at creation of my UIDocument that I'm looking for. Maybe I'm using the wrong method?
EDIT:
I have also tried using NSMetadataQuery, and I can get it to give back notifications, but it doesn't ever have results even though I can explicitly download the files I'm looking for.
To find files in iCloud, you use NSMetadataQuery. That will find both files that have already been downloaded as well as files that are in the user's account but which haven't been downloaded to the local device yet. Using NSFileManager will, at best, only find files that have already been downloaded.
You set it up with something like this:
NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
[self setMetadataQuery:query];
[query setSearchScopes:#[NSMetadataQueryUbiquitousDataScope]];
[query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE '*'", NSMetadataItemFSNameKey]];
You'll want to observe NSMetadataQueryDidStartGatheringNotification, NSMetadataQueryDidUpdateNotification, and probably NSMetadataQueryDidFinishGatheringNotification. Then start the query:
[query startQuery];
With that done, you'll get notifications as the query discovers iCloud files. The notifications will include instances of NSMetadataItem, which you can use to get information like file size, download status, etc.
Use a metadata query - here is some sample code
/*! Creates and starts a metadata query for iCloud files
*/
- (void)createFileQuery {
[_query stopQuery];
if (_query) {
[_query startQuery];
}
else {
_query = [[NSMetadataQuery alloc] init];
[_query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope, nil]];
// NSString * str = [NSString stringWithFormat:#"*.%#",_fileExtension];
NSString *str = #"*";
[_query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE %#", NSMetadataItemFSNameKey, str]];
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(fileListReceived) name:NSMetadataQueryDidFinishGatheringNotification object:_query];
[notificationCenter addObserver:self selector:#selector(fileListReceived) name:NSMetadataQueryDidUpdateNotification object:_query];
[_query startQuery];
}
}
/*! Gets called by the metadata query any time files change. We need to be able to flag files that
we have created so as to not think it has been deleted from iCloud.
*/
- (void)fileListReceived {
LOG(#"fileListReceived called.");
NSArray* results = [[_query results] sortedArrayUsingComparator:^(NSMetadataItem* firstObject, NSMetadataItem* secondObject) {
NSString* firstFileName = [firstObject valueForAttribute:NSMetadataItemFSNameKey];
NSString* secondFileName = [secondObject valueForAttribute:NSMetadataItemFSNameKey];
NSComparisonResult result = [firstFileName.pathExtension compare:secondFileName.pathExtension];
return result == NSOrderedSame ? [firstFileName compare:secondFileName] : result;
}];
//FLOG(#" results of query are %#", results);
for (NSMetadataItem* result in results) {
NSURL* fileURL = [result valueForAttribute:NSMetadataItemURLKey];
NSString* fileName = [result valueForAttribute:NSMetadataItemDisplayNameKey];
NSNumber* percentDownloaded = [result valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey];
NSNumber *isDownloaded = nil;
NSNumber *isDownloading = nil;
NSError *er;
[fileURL getResourceValue: &isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:&er];
[fileURL getResourceValue: &isDownloading forKey:NSURLUbiquitousItemIsDownloadingKey error:&er];
FLOG(#" Found file %#", fileName);
}
}
From the docs:
In iOS, actively download files when required. Items in iCloud but not
yet local are not automatically downloaded by iOS; only their metadata
is automatically downloaded. The initial download of new iCloud-based
documents requires your attention and careful design in your app.
After you explicitly download such an item, the system takes care of
downloading changes arriving from iCloud.
Consider keeping track of
file download status as part of your iOS app’s model layer. Having
this information lets you provide a better user experience: you can
design your app to not surprise users with long delays when they want
to open a document that is not yet local. For each file (or file
package) URL provided by your app’s metadata query, get the value of
the NSURLUbiquitousItemIsDownloadedKeykey by calling the NSURL method
getResourceValue:forKey:error:. Reading a file that has not been
downloaded can take a long time, because the coordinated read
operation blocks until the file finishes downloading (or fails to
download).
For a file (or file package) that is not yet local, you can initiate
download either when, or before, the user requests it. If your app’s
user files are not large or great in number, one strategy to consider
is to actively download all the files indicated by your metadata
query. For each file (or file package) URL provided by the query, make
the corresponding item local by calling the NSFileManager method
startDownloadingUbiquitousItemAtURL:error:. If you pass this method a
URL for an item that is already local, the method performs no work and
returns YES.
Update: iOS7 should use NSURLUbiquitousItemDownloadingStatusKey instead of NSURLUbiquitousItemIsDownloadedKey.

OS X faster file system API than repetitively calling [NSFileManager attributesOfItemAtPath...]?

Is there a faster file system API that I can use if I only need to know if a file is a folder/symlink and its size. I'm currently using [NSFileManager attributesOfItemAtPath...] and only NSFileSize and NSFileType.
Are there any bulk filesystem enumeration APIs I should be using? I suspect this could be faster without having to jump in and out of user code.
My goal is to quickly recurse through directories to get a folders true file size and currently calling attributesOfItemAtPath is my 95% bottleneck.
Some of the code I'm currently using:
NSDictionary* properties = [fileManager attributesOfItemAtPath:filePath error:&error];
long long fileSize = [[properties objectForKey:NSFileSize] longLongValue];
NSObject* fileType = [[properties objectForKey:NSFileType isEqual:NSFileTypeDirectory];
If you want to get really hairy, the Mac OS kernel implements a unique getdirentriesattr() system call which will return a list of files and attributes from a specified directory. It's messy to set up and call, and it's not supported on all filesystems (!), but if you can get it to work for you, it can speed things up significantly.
There's also a closely related searchfs() system call which can be used to rapidly search for files. It's subject to most of the same gotchas.
You can use stat and lstat. Take a look at this answer for calculating directory size.
CPU raises with attributesOfItemAtPath:error:
Whether it's faster or not I'm not certain, but NSURL will give you this information via getResourceValue:forKey:error:
NSError * e;
NSNumber * isDirectory;
BOOL success = [URLToFile getResourceValue:&isDirectory
forKey:NSURLIsDirectoryKey
error:&e];
if( !success ){
// error
}
NSNumber * fileSize;
BOOL success = [URLToFile getResourceValue:&fileSize
forKey:NSURLFileSizeKey
error:&e];
You might also find it convenient to wrap this up if you don't really care about the error:
#implementation NSURL (WSSSimpleResourceValueRetrieval)
- (id)WSSResourceValueForKey: (NSString *)key
{
id value = nil;
BOOL success = [self getResourceValue:&value
forKey:key
error:nil];
if( !success ){
value = nil;
}
return value;
}
#end
This is given as the substitute for the deprecated File Manager function FSGetCatalogInfo(), which is used in a solution in an old Cocoa-dev thread that Dave DeLong gives the thumbs up to.
For the enumeration part, the File System Programming Guide has a section "Getting the Contents of a Directory in a Single Batch Operation", which discusses using contentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:

iOS store file in cache directory for set length of time

We have a POS apartment leasing iPad app that is used to collect a lot of data about a user and their interests (with their knowledge of course).
We use RestKit to sync CoreData with the server, which is totally sweet.
I'm using text files in the cache directory to store a history of their interactions with a guest card, such that it can be submitted in case of error, or sent to us via email, to recreate any guest card in case of some sort of syncing issue.
Although these should be very small text files, probably around 1-3k, I feel as though I should eventually clear these from the cache directory. (As I type this, maybe its so small I shouldn't worry about it).
I was curious if there was any way to clear files from the cache directory after a set amount of time? Say 90 days or so?
Word on the street is that if you use the sanctioned NSCachesDirectory location, if the OS needs that space, it'll delete things that are in that directory. I've never actually seen it happen in practice, but I've heard such things (and it stands to reason, otherwise why bother having special, OS-sanctioned location for cache files.)
That said, this task sounds pretty straightforward. Just fire off a low priority GCD background block to iterate through the files in that directory and delete any whose creation date was > 90 days ago. This is really easy if you only care about how long ago the data was created (as opposed to the last time you accessed the data which is harder to ascertain without keeping track yourself.) NSFileManager is your friend here. Something like this ought to work:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSFileManager* fm = [NSFileManager defaultManager];
NSMutableArray* urlsToDelete = [NSMutableArray array];
for (NSURL* dirUrl in [fm URLsForDirectory: NSCachesDirectory inDomains:NSUserDomainMask])
{
NSDirectoryEnumerator* dirEnum = [fm enumeratorAtURL: dirUrl
includingPropertiesForKeys: [NSArray arrayWithObject: NSFileModificationDate]
options: 0
errorHandler: ^(NSURL* a, NSError* b){ return (BOOL)YES; }];
NSURL* url = nil;
while ((url = [dirEnum nextObject]))
{
NSDate* modDate = [[dirEnum fileAttributes] objectForKey: NSFileModificationDate];
if (modDate && [[NSDate date] compare: [modDate dateByAddingTimeInterval: 60 * 60 * 24 * 90]] == NSOrderedDescending)
{
[urlsToDelete addObject: url];
}
}
}
for (NSURL* url in urlsToDelete)
{
[fm removeItemAtURL: url error: NULL];
}
});
To clarify, if you're looking for some mechanism by which to tell the OS 'delete this if I don't access it for more than 90 days' and have it keep track of this for you, I don't believe such a mechanism exists.

NSBundle pathForResource failing in shell tool

I've noticed some weird behavior with NSBundle when using it in a
command-line program. If, in my program, I take an existing bundle and
make a copy of it and then try to use pathForResource to look up
something in the Resources folder, nil is always returned unless the
bundle I'm looking up existed before my program started. I created a
sample app that replicates the issue and the relevant code is:
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *exePath = [NSString stringWithCString:argv[0]
encoding:NSASCIIStringEncoding];
NSString *path = [exePath stringByDeletingLastPathComponent];
NSString *templatePath = [path stringByAppendingPathComponent:#"TestApp.app"];
// This call works because TestApp.app exists before this program is run
NSString *resourcePath = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:templatePath];
NSLog(#"NOCOPY: %#", resourcePath);
NSString *copyPath = [path stringByAppendingPathComponent:#"TestAppCopy.app"];
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:templatePath
toPath:copyPath
error:nil])
{
// This call will fail if TestAppCopy.app does not exist before
// this program is run
NSString *resourcePath2 = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:copyPath];
NSLog(#"COPY: %#", resourcePath2);
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
}
[pool release];
}
For the purpose of this test app, let's assume that TestApp.app
already exists in the same directory as my test app. If I run this,
the 2nd NSLog call will output: COPY: (null)
Now, if I comment out the final removeItemAtPath call in the if
statement so that when my program exits TestAppCopy.app still exists
and then re-run, the program will work as expected.
I've tried this in a normal Cocoa application and I can't reproduce
the behavior. It only happens in a shell tool target.
Can anyone think of a reason why this is failing?
BTW: I'm trying this on 10.6.4 and I haven't tried on any other
versions of Mac OS X.
I can confirm that it is a bug in CoreFoundation, not Foundation. The bug is due to CFBundle code relying on a directory contents cache containing stale data. The code apparently assumes that neither the bundle directories nor their immediate parent directories will change during application runtime.
The CoreFoundation call corresponding to +[NSBundle pathForResource:ofType:inDirectory:] is CFBundleCopyResourceURLInDirectory(), and it exhibits the same misbehavior. (This is unsurprising, as -pathForResource:ofType:inDirectory: itself uses this call.)
The problem ultimately lies with _CFBundleCopyDirectoryContentsAtPath(). This is called during bundle loading and during all resource lookup. It caches information about the directories it looks up in contentsCache.
Here's the problem: When it comes time to get the contents of TestAppCopy.app, the cached contents of the directory containing TestApp.app don't include TestAppCopy.app. Because the cache ostensibly has the contents of that directory, only the cached contents are searched for TestAppCopy.app. When TestAppCopy.app is not found, the function takes that as a definitive "this path does not exist" and doesn't bother trying to open the directory:
__CFSpinLock(&CFBundleResourceGlobalDataLock);
if (contentsCache) dirDirContents = (CFArrayRef)CFDictionaryGetValue(contentsCache, dirName);
if (dirDirContents) {
Boolean foundIt = false;
CFIndex dirDirIdx, dirDirLength = CFArrayGetCount(dirDirContents);
for (dirDirIdx = 0; !foundIt && dirDirIdx < dirDirLength; dirDirIdx++) if (kCFCompareEqualTo == CFStringCompare(name, CFArrayGetValueAtIndex(dirDirContents, dirDirIdx), kCFCompareCaseInsensitive)) foundIt = true;
if (!foundIt) tryToOpen = false;
}
__CFSpinUnlock(&CFBundleResourceGlobalDataLock);
So, the contents array remains empty, gets cached for this path, and lookup continues. We now have cached the (incorrectly empty) contents of TestAppCopy.app, and as lookup drills down into this directory, we keep hitting bad cached information. Language lookup takes a stab when it finds nothing and hopes there's an en.lproj hanging around, but we still won't find anything, because we're looking in a stale cache.
CoreFoundation includes SPI functions to flush the CFBundle caches. The only place public API calls into them in CoreFoundation is __CFBundleDeallocate(). This flushes all cached information about the bundle's directory itself, but not its parent directory: _CFBundleFlushContentsCacheForPath(), which actually removes the data from the cache, removes only keys matching an anchored, case-insensitive search for the bundle path.
It would seem the only public way a client of CoreFoundation could flush bad information about TestApp.app's parent directory would be to make the parent directory a bundle directory (so TestApp.app lived alongside Contents), create a CFBundle for the parent bundle directory, then release that CFBundle. But, it seems that if you made the mistake of trying to work with the TestAppCopy.app bundle prior to flushing it, the bad data about TestAppCopy.app would not be flushed.
That sounds like a bug in the Foundation. The one key difference between a command line tool like that one and a Cocoa application is the run loop. Try refactoring the above into something like:
#interface Foo:NSObject
#end
#implementation Foo
- (void) doIt { .... your code from main() here .... }
#end
... main(...) {
Foo *f = [Foo new];
[f performSelector: #selector(doIt) withObject: nil afterDelay: 0.1 ...];
[[NSRunLoop currentRunLoop] run];
return 0; // not reached, I'd bet.
}
And see if that "fixes" it. It might. It might not (there are couple of other significant differences, obviously). In any case, do please file a bug via http://bugreport.apple.com/ and add the bug # as a comment.