// // FreeTar // FreeTarController.m // // Copyright (c) 2002 Murray M Johnson // // http://homepage.mac.com/mjsoftware/ // mjsoftware@mac.com // // // This file is part of FreeTar version 1.1. // // FreeTar is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // FreeTar is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with FreeTar; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // #import "FreeTarController.h" #import "FreeTarPreferences.h" #import "NSString_PathUtils.h" #import "PreferencesController.h" const NSTimeInterval TIMER_INTERVAL = 0.1; @implementation FreeTarController // -init function - (id) init { // Check for safety. Probably not strictly necessary, but this is // good style if( (self = [super init]) ) { // initialize FreeTarController members timer = nil; archivePath = nil; quitWhenFinished = NO; appHasLaunched = NO; fileList = [[NSMutableArray alloc] initWithCapacity:10]; preferences = [[FreeTarPreferences alloc] init]; preferencesController = nil; } return self; } // -dealloc function - (void) dealloc { // clean-up FreeTarController members timer = nil; [fileList release]; fileList = nil; [preferences release]; preferences = nil; [preferencesController release]; preferencesController = nil; [super dealloc]; } // This NSApplication delegate function is called when the application is done launching - (void) applicationDidFinishLaunching:(NSNotification *)aNotification { #ifndef __APPLE__ [NSBundle loadNibNamed:@"MainMenu" owner:self]; #endif // set app has launched flag, used in application:openFile: to set the // quitWhenFinished flag. appHasLaunched = YES; } // This NSApplication delegate function is called when the application wants to open // a file. In our case it will be called when a user drops a file onto the application // icon. If multiple files are dropped onto the application, this function will be // called once for each file. This function will be called AFTER applicationWillFinishLaunching: // but BEFORE applicationDidFinishLaunching. The docs say tp return YES if file is opened // successfully, otherwise return NO. We always return NO since we don't actually open // these files. - (BOOL) application:(NSApplication *)theApplication openFile:(NSString *)filename { // set quitWhenFinished flag if(!appHasLaunched) quitWhenFinished = YES; // add file/directory to file list [fileList addObject:filename]; // Fire timer at some later time. A timeInterval of 0.0 tells the run loop // to process this right away, but first it has to get through all the pending // open file messages. if(!timer) { timer = [NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(filesWereDragged:) userInfo:nil repeats:NO]; } // return NO since we didn't open the file return NO; } // filesWereDragged is called by the timer that is set-up in the application:openFile: // method. This system allows all the files dragged onto the icon to be collected // in the fileList member variable and sent to make - (void) filesWereDragged:(NSTimer*)aTimer { int file; // clean-up timer [aTimer invalidate]; timer = nil; // call method that does archive creation if([preferences multipleArchives]) { for(file = 0; file < [fileList count]; ++file) { [self createNewArchiveWithFiles:[NSArray arrayWithObject:[fileList objectAtIndex:file]]]; } } else { [self createNewArchiveWithFiles:fileList]; } // remove all the files from the file list [fileList removeAllObjects]; } //Lazy Services - (void)archiveService:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error { NSArray *types,*files; int i; id pool; // Not sure NSString *path; NSMutableArray *filesToOpen; NSFileManager *filemanager = [NSFileManager defaultManager]; BOOL isDir; types = [pboard types]; if (! [types containsObject: NSFilenamesPboardType] ) return; // Show an Alert Panel ?? files = [pboard propertyListForType: NSFilenamesPboardType]; filesToOpen = [NSMutableArray arrayWithCapacity:1]; for (i=0;i<[files count];i++) { path=[files objectAtIndex:i]; if ( [filemanager fileExistsAtPath:path isDirectory: &isDir] ) [filesToOpen addObject:path]; } pool = [[NSAutoreleasePool alloc] init]; [self createNewArchiveWithFiles:filesToOpen]; [pool release]; } // "File --> New Tar Archive..." menu command. Connected to menu item in // "MainMenu.nib" - (IBAction)newTarArchive:(id)sender { int result; NSOpenPanel* open_panel = [NSOpenPanel openPanel]; NSArray* file_array; int file; // get file name(s) from user and use these files to create the archive // set open_panel attributes [open_panel setAllowsMultipleSelection:YES]; [open_panel setCanChooseDirectories:YES]; [open_panel setTitle: NSLocalizedString(@"Choose Files or Folders to Archive", @"Open panel title.")]; // run open panel result = [open_panel runModalForDirectory:nil file:nil types:nil]; if (result == NSOKButton) { // call method that does archive creation if([preferences multipleArchives]) { file_array = [[open_panel filenames] retain]; for(file = 0; file < [file_array count]; ++file) { [self createNewArchiveWithFiles:[NSArray arrayWithObject:[file_array objectAtIndex:file]]]; } [file_array release]; } else { [self createNewArchiveWithFiles:[open_panel filenames]]; } } } // This method is designed to take the list of files to archive, prompt the user // for the output archive name/directory, and use this info to call // startArchive:withFiles: - (void) createNewArchiveWithFiles:(NSArray*)theFiles { NSString* archive_base_name; NSString* archive_name; NSString* archive_path; NSSavePanel* save_panel; int rtn_value; BOOL do_archive; int try_count; // retain "theFiles" array [theFiles retain]; // error check if([theFiles count] == 0) { [self doError:@"No files in file list."]; return; } // start progress indicator [self spinProgressBar]; // set-up status text [self setLargeStatusTextValue:NSLocalizedString(@"Preparing:", @"Status text 1")]; [self setSmallStatusTextValue:NSLocalizedString(@"Examining Files...", @"Status text 1a")]; // show window [mainWindow center]; [mainWindow makeKeyAndOrderFront:nil]; // count files (including files in directories) filesToArchive = [self countFilesInList:theFiles]; filesArchived = 0; // determine default name (user can change) if([theFiles count] > 1) { archive_base_name = @"Archive"; } else { archive_base_name = [[[theFiles objectAtIndex:0] lastPathComponent] stringByDeletingPathExtension]; } archive_name = [archive_base_name stringByAppendingPathExtension: [preferences archiveExtension]]; // determine default directory (user can change) archive_path = [[theFiles objectAtIndex:0] stringByDeletingLastPathComponent]; // show save panel and get user to decide where to put the archive [self setSmallStatusTextValue:NSLocalizedString(@"Determining archive location...", @"Status text 1b")]; if([preferences autoLocation]) { do_archive = YES; // check for naming conflicts try_count = 1; while([[NSFileManager defaultManager] fileExistsAtPath:[archive_path stringByAppendingPathComponent:archive_name]]) { try_count++; archive_name = [[NSString stringWithFormat:@"%@%d", archive_base_name, try_count] stringByAppendingPathExtension:[preferences archiveExtension]]; } archive_name = [archive_path stringByAppendingPathComponent:archive_name]; } else { save_panel = [NSSavePanel savePanel]; [save_panel setTitle:NSLocalizedString(@"Choose Archive Destination", @"Save panel title")]; #ifdef __APPLE__ [save_panel beginSheetForDirectory:archive_path file:archive_name modalForWindow:mainWindow modalDelegate:nil didEndSelector:nil contextInfo:nil]; // run modal loop rtn_value = [NSApp runModalForWindow:[mainWindow attachedSheet]]; do_archive = NO; if(rtn_value == NSFileHandlingPanelOKButton) { do_archive = YES; } #else //GNUstep does not implement the sheet (hopefully :) rtn_value= [save_panel runModalForDirectory:archive_path file:archive_name]; do_archive = NO; //TODO GNUstep manage that in the OpenStep way ?? //Ask to the mailing list !!!! if(rtn_value) do_archive = YES; #endif // get info out of save panel archive_name = [save_panel filename]; #ifdef __APPLE__ // end sheet [NSApp endSheet:[mainWindow attachedSheet]]; #endif } // launch the tar process? if(do_archive) { [self startArchive:archive_name withFiles:theFiles]; } else { // kill window [mainWindow orderOut:nil]; // quit if needed if(quitWhenFinished) [NSApp terminate:self]; } // release "theFiles" array [theFiles release]; } // This file creates and launches an NSTask for the tar process. It also sets up // repeating NSTimer which will call monitorTarOperation: periodically. - (void) startArchive:(NSString*)fullPath withFiles:(NSArray*)theFiles { NSString* working_dir; NSMutableArray* args; int f; NSLog(@"start"); // Update status display [self setLargeStatusTextValue:NSLocalizedString(@"Preparing:", @"Status text 1")]; [self setSmallStatusTextValue:NSLocalizedString(@"Launching tar process...", @"Status text 2a")]; //TODO: Use [preferences autoBaseDir], for now use path of first file working_dir = [[theFiles objectAtIndex:0] stringByDeletingLastPathComponent]; // copy full path of archive we're going to create (in case we need to delete // it when the user presses the cancel button archivePath = [[NSString alloc] initWithString:fullPath]; // set up tar task tarTask = [[NSTask alloc] init]; tarStdOut = [[NSPipe alloc] init]; // path to tar app (change for running tar, vs. gnutar, etc.) [tarTask setLaunchPath:[preferences launchPath]]; // current/working directory [tarTask setCurrentDirectoryPath:working_dir]; // attach stdout & stderr to a pipe // tar writes to stderr, gnutar writes to stdout [tarTask setStandardOutput:tarStdOut]; [tarTask setStandardError:tarStdOut]; // set-up arguments args = [NSMutableArray arrayWithCapacity:10]; [args addObject:@"-c"]; // -c = create tar archive [args addObject:@"-v"]; // -v = verbose mode if( [preferences compressWithGzip] ) [args addObject:@"-z"]; // -z = compress tar archive with gzip if( [preferences compressWithCompress] ) [args addObject:@"-Z"]; // -Z = compress with compress if( [preferences compressWithBzip] ) [args addObject:@"-j"]; // -j = compress with bzip2 [args addObject:[NSString stringWithFormat:@"-f%@", [fullPath pathRelativeTo:working_dir]]]; // -f = use archive file (no space or there will // be a space in the file name // add names of files to put in archive for(f = 0; f < [theFiles count]; ++f) [args addObject:[[theFiles objectAtIndex:f] pathRelativeTo:working_dir]]; // set tarTask's arguments [tarTask setArguments:args]; // prepare UI for archiving process [self setProgressBarValue:0.0]; [self setLargeStatusTextValue: [NSString stringWithFormat:@"%@ \"%@\":", NSLocalizedString(@"Creating", @"Status text 3"), [fullPath lastPathComponent]]]; [self setSmallStatusTextValue:NSLocalizedString(@"Launching tar...", @"Status text 3a")]; archiveFailed = NO; // register to get notifications from asynchronous read [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dataAvailable:) name:NSFileHandleReadCompletionNotification object:nil]; // register to get notification from task termination [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidTerminate:) name:NSTaskDidTerminateNotification object:nil]; // asynch read [[tarStdOut fileHandleForReading] readInBackgroundAndNotify]; // run tar [tarTask launch]; } // Loop through files in the list and count all the files (including directories, // subdirectories, and contents in subdirectories). - (int) countFilesInList:(NSArray*)theFiles { int num_files; int file; BOOL is_dir; NSDirectoryEnumerator* enumerator; NSString* file_name; NSString* data_string; NSFileManager* dflt_manager; // get default file manager dflt_manager = [NSFileManager defaultManager]; // count files num_files = 0; for(file = 0; file < [theFiles count]; ++file) { file_name = [theFiles objectAtIndex:file]; // is this a directory? if( [dflt_manager fileExistsAtPath:file_name isDirectory:&is_dir] && is_dir ) { // one for the directory num_files++; // loop through multiple files enumerator = [dflt_manager enumeratorAtPath:file_name]; while( (data_string = [enumerator nextObject]) ) num_files++; } else { // just this file num_files++; } } return num_files; } // notification when task finishes - (void) taskDidTerminate:(NSNotification*)aNotification { // update UI [self setProgressBarValue:100.0]; [self setLargeStatusTextValue:NSLocalizedString(@"Complete", @"Status text 4")]; [self setSmallStatusTextValue:@""]; // need to display error message? if([tarTask terminationStatus] != 0) { #ifdef __APPLE__ // inform user a problem occurred NSBeginCriticalAlertSheet( NSLocalizedString(@"Free Tar Error", @"Error sheet title"), // title nil, // dflt button -- nil = localized "OK" nil, // alternate button nil, // other button mainWindow, // window nil, // modal delagate nil, // willEndSelector nil, // didEndSelector nil, // context info NSLocalizedString(@"Archiving failed.", @"error sheet message") // message ); // run modal loop [NSApp runModalForWindow:[mainWindow attachedSheet]]; [NSApp endSheet:[mainWindow attachedSheet]]; //GNUstep #else NSRunAlertPanel ( _(@"Free Tar Error"), _(@"Archiving failed"), _(@"OK"), nil, nil); #endif archiveFailed = YES; } // need to delete file? if(archiveFailed) { if(![[NSFileManager defaultManager] removeFileAtPath:archivePath handler:nil]) [self doError:@"removeFileAtPath:handler: failed"]; } // Get rid of window [mainWindow orderOut:nil]; // clean up memory [tarTask release]; tarTask = nil; [tarStdOut release]; tarStdOut = nil; [archivePath release]; archivePath = nil; // remove this object from notification center [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadCompletionNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSTaskDidTerminateNotification object:nil]; // quit if needed if(quitWhenFinished) [NSApp terminate:self]; } // Notification for asynchronous data reading - (void) dataAvailable:(NSNotification*)aNotification { NSData* data; NSString* data_string; int num_lines; // get data data = [[aNotification userInfo] objectForKey:NSFileHandleNotificationDataItem]; if( [data length] > 0 ) { data_string = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; // this always comes out one to many num_lines = [[data_string componentsSeparatedByString:@"\n"] count] - 1; // Set status text [self setSmallStatusTextValue:data_string]; // update file count filesArchived += num_lines; // update progress bar [self setProgressBarValue:100.0*(double)filesArchived/filesToArchive]; // clean-up memory [data_string release]; data_string = nil; // asynch read [[tarStdOut fileHandleForReading] readInBackgroundAndNotify]; } } // "FreeTar --> Preferences..." menu command. Connected to menu item in // "MainMenu.nib" - (IBAction)preferences:(id)sender { if(!preferencesController) { preferencesController = [[PreferencesController alloc] initWithPrefsObject:preferences]; } [preferencesController showWindow:self]; } // "Cancel" button. Connected to button in "MainMenu.nib" - (IBAction) cancelButtonPressed:(id)sender { if( [tarTask isRunning] ) { // terminate the task and let monitorTarOperation: clean up when it realizes // that the tarTask has ended. [tarTask terminate]; archiveFailed = YES; } } // Make progess bar "spin" (indeterminate animation) - (void) spinProgressBar; { [progressBar setIndeterminate:YES]; [progressBar setUsesThreadedAnimation:YES]; [progressBar startAnimation:nil]; } // set progress bar, value = 0.0 - 1.0 - (void) setProgressBarValue:(double)value { if([progressBar isIndeterminate]) { [progressBar stopAnimation:nil]; [progressBar setIndeterminate:NO]; } [progressBar setDoubleValue:value]; [progressBar displayIfNeeded]; } // Set text in largeStatusText NSTextField - (void) setLargeStatusTextValue:(NSString*)message { [largeStatusText setStringValue:message]; [largeStatusText displayIfNeeded]; } // Set text in smallStatusText NSTextField - (void) setSmallStatusTextValue:(NSString*)message { [smallStatusText setStringValue:message]; [smallStatusText displayIfNeeded]; } // Main error handling function. For now, just NSLog() the error. - (void) doError:(NSString*)message { NSLog(@"ERROR: %@", message); } // Window delegate function, windowShouldClose:. If tar task is running, prompt user // to see if we should end the task and close (hide) the window. mainWindow delegate // is set to this class in the "MainMenu.nib" nib file. - (BOOL)windowShouldClose:(id)sender { // Always return NO. Window will hide when the tar task is done. Window NEVER // closes, only hides. return NO; } @end