I'm executing a zip command from within my app using NSTask. It's passed as arguments some paths which point to the files/folders to be zipped.
The problem is that without the -j option, the final zip ends up with absurd filepaths inside the zip, (like "/private/var/folders/A5/A5CusLQaEo4mop-reb-SYE+++TI/-Tmp-/9101A216-5A6A-4CD6-A477-E4B86E007476-51228-00014BCB9514323F/myfile.rtf"). However, if I add the -j option, then I constantly run into name collisions if any file anywhere deep inside a nested folder has
I've tried setting the path before executing the NSTask:
[[NSFileManager defaultManager] changeCurrentDirectoryPath:path];
In the hope that the documentation for zip was telling the truth:
By default, zip will store the full path
(relative to the current directory)
But this did not work as expected. Adjusting settings of -j and -p and -r simply produces the above mentioned problems in different combinations.
QUESTION:
How can I take a set of directories like
/some/long/path/sub1/file1.txt
/some/long/path/sub2/file1.txt
and zip them into a zip whose contents are
/sub1/file1.txt
/sub2/file1.txt
Thanks for any advice on the subtleties of zip.
-----EDIT
One other thing I forgot to add is that the original directory being passed is "path", so the desired outcome is also to my mind the expected outcome.
Instead of
[[NSFileManager defaultManager] changeCurrentDirectoryPath:path];
use -[NSTask setCurrentDirectoryPath:] prior to launching the task. For example:
NSString *targetZipPath = #"/tmp/foo.zip";
NSArray *args = [NSArray arrayWithObjects:#"-r", targetZipPath,
#"sub1", #"sub2", nil];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/zip"];
[task setArguments:args];
// set path to be the parent directory of sub1, sub2
[task setCurrentDirectoryPath:path];
…
This is not a universal solution, in that it won't handle multiple directories well, but the solution I'm using for a single directory of unknown contents (ie, mixed files/folders/bundles) is to enumerate the contents of the directory and add them individually as arguments to zip, rather than simply zipping the entire directory at once.
Specifically:
+ (BOOL)zipDirectory:(NSURL *)directoryURL toArchive:(NSString *)archivePath;
{
//Delete existing zip
if ( [[NSFileManager defaultManager] fileExistsAtPath:archivePath] ) {
[[NSFileManager defaultManager] removeItemAtPath:archivePath error:nil];
}
//Specify action
NSString *toolPath = #"/usr/bin/zip";
//Get directory contents
NSArray *pathsArray = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[directoryURL path] error:nil];
//Add arguments
NSMutableArray *arguments = [[[NSMutableArray alloc] init] autorelease];
[arguments insertObject:#"-r" atIndex:0];
[arguments insertObject:archivePath atIndex:0];
for ( NSString *filePath in pathsArray ) {
[arguments addObject:filePath]; //Maybe this would even work by specifying relative paths with ./ or however that works, since we set the working directory before executing the command
//[arguments insertObject:#"-j" atIndex:0];
}
//Switch to a relative directory for working.
NSString *currentDirectory = [[NSFileManager defaultManager] currentDirectoryPath];
[[NSFileManager defaultManager] changeCurrentDirectoryPath:[directoryURL path]];
//NSLog(#"dir %#", [[NSFileManager defaultManager] currentDirectoryPath]);
//Create
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath:toolPath];
[task setArguments:arguments];
//Run
[task launch];
[task waitUntilExit];
//Restore normal path
[[NSFileManager defaultManager] changeCurrentDirectoryPath:currentDirectory];
//Update filesystem
[[NSWorkspace sharedWorkspace] noteFileSystemChanged:archivePath];
return ([task terminationStatus] == 0);
}
Again, I make no claims this is bulletproof (and would love improvements) but it does work at correctly zipping any single folder.
Related
Im building an cocoa app that monitors something™ and I am planning to have some hooks for users. So I want to enable the user to put a script (Bash, Ruby, Python you name it) with a specified name (let's say after_event) into the Application Support directory and that script gets executed after a certain event in my code. Ideally I could pass some variables to the script so the script knows what happened.
Any ideas on this?
So problem one is: How do I get the path of the Application Support "the SDK way"? problem two is: How do I execute script with variables like THAT_APPEND="foo"?
Thanks,
Philip
Because sharing is caring here is the method that executes the scripts:
-(void) runScript:(NSString*)scriptName withVariables:(NSDictionary *)variables
{
NSString *appSupportPath = [NSFileManager defaultManager] applicationSupportDirectory];
NSArray *arguments;
NSString* newpath = [NSString stringWithFormat:#"%#/%#",appSupportPath, scriptName];
if ([[NSFileManager defaultManager]fileExistsAtPath:newpath]){
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: newpath];
NSLog(#"Executing hook: %#",newpath);
arguments = [NSArray arrayWithObjects:newpath, nil];
[task setArguments: arguments];
[task setEnvironment:variables];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *string;
string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog (#"script returned:\n%#", string);
}
}
}
UPDATE: I updated the code to be more generic. Now NSTask will tell the kernel to execute the script directly so your user can not online use Bash scripts but also python, perl, php whatever she likes. The only thing she needs to use is a Shebang in that file.
The NSFileManager Category can be found here.
Look for NSTask documentation. There's an environment member you can manipulate. Also adding command line parameters in a form -name = value should be trivial.
I am making a OX Cocoa app and I want to be able to read and write text files using the app on button press. These text files should be saved in /Library/Application Support/AppName but i can't get my app to reading anything from there. It can write to the folder, but not read what it has written there, even though I can see the file sitting there in finder.
Here is the code I am using the successfully write to the folder.
NSString *text = editor.string;
NSString *path = #"/Library/Application Support/";
NSMutableString *mu = [[NSMutableString stringWithString:path] init];
[mu insertString:FileName.stringValue atIndex:mu.length];
[mu insertString:#".txt" atIndex:mu.length];
path = [mu copy];
[text writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:NULL];
Here is the code I am using (and failing) to read from the text files.
NSArray *path = [[NSBundle mainBundle] pathsForResourcesOfType:#"txt" inDirectory:#"/Library/Application Support/"];
NSString *output = #"";
NSMutableString *mu = [[NSMutableString stringWithString:output] init];
for (int i = 0; i < [path count]; i++) {
NSString *text = [NSString stringWithContentsOfFile:path[i] encoding:NSUTF8StringEncoding error:NULL];
[mu insertString:text atIndex:mu.length];
[mu insertString:#"\n" atIndex:mu.length];
}
[textView setString:mu];
Any tips on what I can correct would be super helpful, I'm a bit stuck here.
Edit: Using your input I have updated my code to this:
NSString *fileLocation = #"~/Library/Application Support/";
NSArray *text = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:fileLocation error:nil];
NSString *output = #"";
NSMutableString *mu = [[NSMutableString stringWithString:output] init];
for (int i = 0; i < [text count]; i++) {
[mu insertString:text[i] atIndex:mu.length];
[mu insertString:#"\n" atIndex:mu.length];
}
[textView setString:mu];
However the text from the files is still not appearing.
Most hard-coded paths will fail when you sandbox your app. Even if you get away with this one, or you don't plan to sandbox this app, it's a bad habit that's worth getting out of.
Moreover, are you sure you want /Library and not ~/Library? The former is often not writable by the user. The latter is in the user's Home directory (or your container when sandboxed).
To get the Application Support directory, or the Caches directory, or any other directory that you may want to create things in and later retrieve them from, ask a file manager for it.
/Library/Application Support is not in your bundle. The paths you get using [[NSBundle mainBundle] pathsForResourcesOfType:…] are only useful for accessing files inside your application itself (images, sounds, etc that you included when you built the app).
You want to use [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:error] to get a list of files in a directory outside your application.
Matt Gallagher has a great example of a fault-tolerant method of locating the path to your application support directory at Cocoa With Love. I would recommend using it over hardcoding the /Library/Application Support path.
NSError *error = nil;
NSArray *text = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:fileLocation error:&error];
if (!text) {
NSLog( #"Error reading contents of application support folder at %#.\n%#", applicationSupportFolder, [error userInfo] );
}
You're trying to get the path using NSBundle from the main bundle of the app. But the file is not in the bundle, you should specify the path manually. You could hardcode the path, store previously written paths somewhere, or use NSFileManager to get directory contents and analyze it. For example, -[NSFileManager contentsOfDirectoryAtPath:error:].
So, I need to search for the Unix Executable files in a directory. I Iterate through directory and with the path of the file I am searching. Some of the Methods I tried.
1.With the Help of the file Extension
Unix Executable file does not have the file Extension, but Some documents files are also not having the extensions. So, it failed for me.
2. With the help of NSFileManager
NSDicitionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
It does not have any unique attributes to find the Unix executable file.
3. With the help of MDItemRef
It have the attribute called kMDItemContentType but it is giving the correct result for some of the unix executable files only.
MDItemRef inspectedRef;
CFArrayRef inspectedRefAttributeNames;
CFDictionaryRef inspectedRefAttributeValues;
inspectedRef = MDItemCreate(kCFAllocatorDefault,(CFStringRef)filePath);
if(inspectedRef) {
inspectedRefAttributeNames = MDItemCopyAttributeNames(inspectedRef);
inspectedRefAttributeValues = MDItemCopyAttributes(inspectedRef,inspectedRefAttributeNames);
NSDictionary *attribDict = (__bridge NSDictionary*)inspectedRefAttributeValues;
if([[attribDict objectForKey:#"kMDItemContentType"] isEqualToString:#"public.unix-executable"])
NSLog(#"Unix Executable file");
}
4. With the help of unix command "file"
NSTask *unixTask = [[NSTask alloc] init];
[unixTask setStandardOutput:newPipe];
[unixTask setLaunchPath:#"/usr/bin/file"];
[unixTask setArguments:[NSArray arrayWithObject:filePath]];
[unixTask launch];
[unixTask waitUntilExit];
[unixTask terminationStatus];
while ((inData = [readHandle availableData]) && [inData length]) {
returnValue= [[NSString alloc] initWithData:inData encoding:[NSString defaultCStringEncoding]];
returnValue = [returnValue substringToIndex:[returnValue length]-1];
NSLog(#"%#",returnValue);
}
Here, From the returnValue I can able to find whether it is unix executable or not. But it is very slow Process. So, My question is How to search for the unix executable in an efficient manner ?
Try using either getResourceValue:forKey:error: or resourceValuesForKeys:error: methods of NSURL and requesting NSURLTypeIdentifierKey.
Addendum:
If what #Aravindhanarvi says is correct, on 10.6 there are bugs and the above solution is unreliable. To make things worse #petur solution is also not possible for lack of NSURLIsExecutableKey.
An alternative would be to fall back to the NSFileManager and use methods like isExecutableFileAtPath: and attributesOfItemAtPath:error: (specifically the NSFilePosixPermissions and NSFileType attributes) to implement the same logic suggested by #petur.
Came up with this, just point the url to the directory you to use as the base.
This is ARC code.
The array, files, contains an url pointer to each executable file found.
#autoreleasepool {
NSFileManager *defaultFileManager = [NSFileManager defaultManager];
NSURL *url = [NSURL URLWithString:#"/private/tmp/"]; // Search path
NSDirectoryEnumerator *dirEnumerator = [defaultFileManager enumeratorAtURL:url includingPropertiesForKeys:[NSArray arrayWithObjects:NSURLNameKey, nil] options:0 errorHandler:nil];
NSMutableArray *files = [NSMutableArray array];
// extract non-executable files
for (NSURL *file in dirEnumerator) {
NSNumber *isExecutable;
NSNumber *isDirectory; // Directories have the executable flag set, but we are not interested in them
NSError *error, *error2;
[file getResourceValue:&isExecutable forKey:NSURLIsExecutableKey error:&error];
[file getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error2];
// Deal with errors
if (error)
NSLog(#"%#", [error localizedDescription]);
else if (error2)
NSLog(#"%#", [error2 localizedDescription]);
else if ([isExecutable boolValue] && ![isDirectory boolValue]) {
[files addObject:file];
}
// print out all executable files to the console
for (id i in files)
NSLog(#"%#", [i description]);
}
The following code returns a NSCocoaErrorDomain with error code 513 (NSFileWriteNoPermissionError) when running from xcode.
NSError *error;
[[NSFileManager defaultManager]
createDirectoryAtPath:#"/Library/Application Support/myapp"
withIntermediateDirectories:YES
attributes:nil
error:&error];
This is on a Mac OS X 10.6.7, the specified directory does not exist, and my user has admin privileges.
The purpose is to save application support files that are shared among users. Shouldn't there be write permissions to create this directory?
No, that's the system's Library folder. You need the user's Library, at "~/Library/". You could try:
[NSHomeDirectory() stringByAppendingPathComponent:#"Library/Application Support/myapp"]
or:
NSArray * paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString * appSupportPath = [[paths objectAtIndex:0] stringByAppendingPathComponent:#"myapp"];
Just for one more option, you can also get a URL from the file manager:
NSFileManager * fm = [[NSFileManager alloc] init];
NSArray * urls = [fm URLsForDirectory:NApplicationSupportDirectory inDomains:NSUserDomainMask];
NSURL * appSupportURL = [urls objectAtIndex:0];
my method works for zipping files from a temporary directory previously created and populated:
NSURL *destURL = self.archiveDestURL;
NSTask *task = [[NSTask alloc] init];
[task setCurrentDirectoryPath:[srcURL path]];
[task setLaunchPath:#"/usr/bin/zip"];
NSArray *argsArray = [NSArray arrayWithObjects:#"-r", #"-q", [destURL path], #".", #"-i", #"*", nil];
[task setArguments:argsArray];
[task launch];
[task waitUntilExit];
but what i'd like to have when unzipped, is a folder with the files.
sure i can make a folder in the tempDir and write my files there, but what is the zip argument for having a folder be the top level in the created archive?
i didn't see this in man zip .
This will help you.
NSTask *unzip = [[NSTask alloc] init];
[unzip setLaunchPath:#"/usr/bin/unzip"];
[unzip setArguments:[NSArray arrayWithObjects:#"-u", #"-d",
destination, zipFile, nil]];
NSPipe *aPipe = [[NSPipe alloc] init];
[unzip setStandardOutput:aPipe];
[unzip launch];
[unzip waitUntilExit];
instead of using NSTask, you could incorporate compress functionality into your code. there are several options.
ZipBrowser from apple.com
adding a category to NSData, as in here
a similar question. How can I create a zip file by using Objective C?