// // $Id$ // // SPExportFileUtilities.m // sequel-pro // // Created by Stuart Connolly (stuconnolly.com) on July 30, 2010 // Copyright (c) 2010 Stuart Connolly. All rights reserved. // // This program 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. // // This program 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 this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // More info at #import "SPExportFileUtilities.h" #import "SPExporter.h" #import "SPAlertSheets.h" #import "SPExportFile.h" #import "SPDatabaseDocument.h" #import "SPCustomQuery.h" #import "SPMySQL.h" typedef enum { SPExportErrorCancelExport = 0, SPExportErrorReplaceFiles = 1, SPExportErrorSkipErrorFiles = 2 } SPExportErrorChoice; @interface SPExportController (SPExportFileUtilitiesPrivateAPI) - (void)_reopenExportSheet; @end @implementation SPExportController (SPExportFileUtilities) /** * Writes the CSV file header to the supplied export file. * * @param file The export file to write the header to. */ - (void)writeCSVHeaderToExportFile:(SPExportFile *)file { NSMutableString *lineEnding = [NSMutableString stringWithString:[exportCSVLinesTerminatedField stringValue]]; // Escape tabs, line endings and carriage returns [lineEnding replaceOccurrencesOfString:@"\\t" withString:@"\t" options:NSLiteralSearch range:NSMakeRange(0, [lineEnding length])]; [lineEnding replaceOccurrencesOfString:@"\\n" withString:@"\n" options:NSLiteralSearch range:NSMakeRange(0, [lineEnding length])]; [lineEnding replaceOccurrencesOfString:@"\\r" withString:@"\r" options:NSLiteralSearch range:NSMakeRange(0, [lineEnding length])]; // Write the file header and the first table name [file writeData:[[NSMutableString stringWithFormat:@"%@: %@ %@: %@ %@: %@%@%@%@ %@%@%@", NSLocalizedString(@"Host", @"export header host label"), [tableDocumentInstance host], NSLocalizedString(@"Database", @"export header database label"), [tableDocumentInstance database], NSLocalizedString(@"Generation Time", @"export header generation time label"), [NSDate date], lineEnding, lineEnding, NSLocalizedString(@"Table", @"csv export table heading"), [[tables objectAtIndex:0] objectAtIndex:0], lineEnding, lineEnding] dataUsingEncoding:[connection stringEncoding]]]; } /** * Writes the XML file header to the supplied export file. * * @param file The export file to write the header to. */ - (void)writeXMLHeaderToExportFile:(SPExportFile *)file { NSMutableString *header = [NSMutableString string]; [header setString:@"\n\n"]; [header appendString:@"\n\n"]; if ([exportXMLFormatPopUpButton indexOfSelectedItem] == SPXMLExportMySQLFormat) { NSString *tag = @""; if (exportSource == SPTableExport) { tag = [NSString stringWithFormat:@"\n\n\n", [tableDocumentInstance database]]; } else { tag = [NSString stringWithFormat:@"\n\n", (exportSource == SPFilteredExport) ? [tableContentInstance usedQuery] : [customQueryInstance usedQuery]]; } [header appendString:tag]; } else { [header appendFormat:@"<%@>\n\n", [[tableDocumentInstance database] HTMLEscapeString]]; } [file writeData:[header dataUsingEncoding:NSUTF8StringEncoding]]; } /** * Indicates that one or more errors occurred while attempting to create the export file handles. Asks the * user how to proceed. * * @param files An array of export files (SPExportFile) that failed to be created. */ - (void)errorCreatingExportFileHandles:(NSArray *)files { // Get the number of files that already exists as well as couldn't be created because of other reasons NSUInteger filesAlreadyExisting = 0; NSUInteger filesFailed = 0; for (SPExportFile *file in files) { if ([file exportFileHandleStatus] == SPExportFileHandleExists) { filesAlreadyExisting++; // For file handles that we failed to create for some unknown reason, ignore them and remove any // exporters that are associated with them. } else if ([file exportFileHandleStatus] == SPExportFileHandleFailed) { filesFailed++; for (SPExporter *exporter in exporters) { if ([[exporter exportOutputFile] isEqualTo:file]) { [exporters removeObject:exporter]; } } } } NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSCriticalAlertStyle]; // If files failed because they already existed, show a OS-like dialog. if (filesAlreadyExisting) { // Set up a string for use if files had to be skipped. NSString *additionalErrors = filesFailed ? NSLocalizedString(@"\n\n(In addition, one or more errors occurred while attempting to create the export files: %lu could not be created. These files will be ignored.)", @"Additional export file errors") : @""; if (filesAlreadyExisting == 1) { [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"“%@” already exists. Do you want to replace it?", @"Export file already exists message"), [[[files objectAtIndex:0] exportFilePath] lastPathComponent]]]; [alert setInformativeText:[NSString stringWithFormat:@"%@%@", NSLocalizedString(@"A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.", @"Export file already exists explanatory text"), additionalErrors]]; } else if (filesAlreadyExisting == [exportFiles count]) { [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"All the export files already exist. Do you want to replace them?", @"All export files already exist message")]]; [alert setInformativeText:[NSString stringWithFormat:@"%@%@", NSLocalizedString(@"Files with the same names already exist in the target folder. Replacing them will overwrite their current contents.", @"All export files already exist explanatory text"), additionalErrors]]; } else { [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"%lu files already exist. Do you want to replace them?", @"Export file already exists message"), filesAlreadyExisting]]; [alert setInformativeText:[NSString stringWithFormat:@"%@%@", [NSString stringWithFormat:NSLocalizedString(@"%lu files with the same names already exist in the target folder. Replacing them will overwrite their current contents.", @"Some export files already exist explanatory text"), filesAlreadyExisting], additionalErrors]]; } [alert addButtonWithTitle:NSLocalizedString(@"Replace", @"Replace button")]; [[[alert buttons] objectAtIndex:0] setTag:SPExportErrorReplaceFiles]; [[[alert buttons] objectAtIndex:0] setKeyEquivalent:@"r"]; [[[alert buttons] objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")]; [[[alert buttons] objectAtIndex:1] setTag:SPExportErrorCancelExport]; [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"\r"]; if ((filesAlreadyExisting + filesFailed) != [exportFiles count]) { [alert addButtonWithTitle:NSLocalizedString(@"Skip existing", @"skip existing button")]; [[[alert buttons] objectAtIndex:2] setTag:SPExportErrorSkipErrorFiles]; [[[alert buttons] objectAtIndex:2] setKeyEquivalent:@"s"]; [[[alert buttons] objectAtIndex:2] setKeyEquivalentModifierMask:NSCommandKeyMask]; } // If one or multiple files failed, but only due to unhandled errors, show a short dialog } else { if (filesFailed == 1) { [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"“%@” could not be created", @"Export file creation error title"), [[[files objectAtIndex:0] exportFilePath] lastPathComponent]]]; [alert setInformativeText:NSLocalizedString(@"An unhandled error occurred when attempting to create the export file. Please check the details and try again.", @"Export file creation error explanatory text")]; } else if (filesFailed == [exportFiles count]) { [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"No files could be created", @"All export files creation error title")]]; [alert setInformativeText:NSLocalizedString(@"An unhandled error occurred when attempting to create each of the export files. Please check the details and try again.", @"All export files creation error explanatory text")]; } else { [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"%lu files could not be created", @"Export files creation error title"), filesFailed]]; [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"An unhandled error occurred when attempting to create %lu of the export files. Please check the details and try again.", @"Export files creation error explanatory text"), filesFailed]]; } [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")]; [[[alert buttons] objectAtIndex:0] setTag:SPExportErrorCancelExport]; if (filesFailed != [exportFiles count]) { [alert addButtonWithTitle:NSLocalizedString(@"Skip problems", @"skip problems button")]; [[[alert buttons] objectAtIndex:1] setTag:SPExportErrorSkipErrorFiles]; [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"s"]; [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:NSCommandKeyMask]; } } // Close the progress sheet [NSApp endSheet:exportProgressWindow returnCode:0]; [exportProgressWindow orderOut:self]; [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:files]; } /** * NSAlert didEnd method. */ - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { NSArray *files = (NSArray *)contextInfo; // Ignore the files that exist and remove the associated exporters if (returnCode == SPExportErrorSkipErrorFiles) { for (SPExportFile *file in files) { // Use a numerically controlled loop to avoid mutating the collection while enumerating NSUInteger i; for (i = 0; i < [exporters count]; i++) { SPExporter *exporter = [exporters objectAtIndex:i]; if ([[exporter exportOutputFile] isEqualTo:file]) { [exporters removeObjectAtIndex:i]; i--; } } } [files release]; // If we're now left with no exporters, cancel the export operation if ([exporters count] == 0) { [exportFiles removeAllObjects]; // Trigger restoration of the export interface [self performSelector:@selector(_reopenExportSheet) withObject:nil afterDelay:0.1]; } else { // Start the export after a short delay to give this sheet a chance to close [self performSelector:@selector(startExport) withObject:nil afterDelay:0.1]; } } // Overwrite the files and continue else if (returnCode == SPExportErrorReplaceFiles) { for (SPExportFile *file in files) { if ([file exportFileHandleStatus] == SPExportFileHandleExists) { if ([file createExportFileHandle:YES] == SPExportFileHandleCreated) { [file setCompressionFormat:[exportOutputCompressionFormatPopupButton indexOfSelectedItem]]; if ([file exportFileNeedsCSVHeader]) { [self writeCSVHeaderToExportFile:file]; } else if ([file exportFileNeedsXMLHeader]) { [self writeXMLHeaderToExportFile:file]; } } } } [files release]; // Start the export after a short delay to give this sheet a chance to close [self performSelector:@selector(startExport) withObject:nil afterDelay:0.1]; } // Cancel the entire export operation else if (returnCode == SPExportErrorCancelExport) { // Loop the cached export files and remove those we've already created for (SPExportFile *file in exportFiles) { [file delete]; } [files release]; // Finally get rid of all the exporters and files [exportFiles removeAllObjects]; [exporters removeAllObjects]; // Trigger restoration of the export interface [self performSelector:@selector(_reopenExportSheet) withObject:nil afterDelay:0.1]; } } @end @implementation SPExportController (SPExportFileUtilitiesPrivateAPI) /** * Re-open the export sheet without resetting the interface - for use on error. */ - (void)_reopenExportSheet { [NSApp beginSheet:[self window] modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; } @end