// // SPExportFileUtilities.m // sequel-pro // // Created by Stuart Connolly (stuconnolly.com) on July 30, 2010. // Copyright (c) 2010 Stuart Connolly. All rights reserved. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // More info at #import "SPExportFileUtilities.h" #import "SPExportInitializer.h" #import "SPExporter.h" #import "SPExportFile.h" #import "SPDatabaseDocument.h" #import "SPCustomQuery.h" #import "SPTableContent.h" #import "SPExportController+SharedPrivateAPI.h" #import typedef enum { SPExportErrorCancelExport = 0, SPExportErrorReplaceFiles = 1, SPExportErrorSkipErrorFiles = 2 } SPExportErrorChoice; @interface SPExportController (SPExportFileUtilitiesPrivateAPI) - (void)_reopenExportSheet; @end @implementation SPExportController (SPExportFileUtilitiesPrivateAPI) /** * 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 { // We don't know where "files" came from, but we know 2 things: // - NSAlert will NOT retain it as contextInfo // - This method continues execution after [alert beginSheet:...], thus even if files was retained before, it could be released before the alert ends [files retain]; // Get the number of files that already exist as well as couldn't be created because of other reasons NSUInteger filesAlreadyExisting = 0; NSUInteger parentFoldersMissing = 0; NSUInteger parentFoldersNotWritable = 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++; NSMutableArray *exportersToRemove = [[NSMutableArray alloc] init]; for (SPExporter *exporter in exporters) { if ([[exporter exportOutputFile] isEqualTo:file]) { [exportersToRemove addObject:exporter]; } } [exporters removeObjectsInArray:exportersToRemove]; [exportersToRemove release]; // Check the parent folder to see if it still is present BOOL parentIsFolder = NO; if (![[NSFileManager defaultManager] fileExistsAtPath:[[[file exportFilePath] stringByDeletingLastPathComponent] stringByExpandingTildeInPath] isDirectory:&parentIsFolder] || !parentIsFolder) { parentFoldersMissing++; } else if (![[NSFileManager defaultManager] isWritableFileAtPath:[[[file exportFilePath] stringByDeletingLastPathComponent] stringByExpandingTildeInPath]]) { parentFoldersNotWritable++; } } } 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]]]; if (parentFoldersMissing) { [alert setInformativeText:NSLocalizedString(@"The target export folder no longer exists. Please select a new export location and try again.", @"Export folder missing explanatory text")]; } else if (parentFoldersNotWritable) { [alert setInformativeText:NSLocalizedString(@"The target export folder is not writable. Please select a new export location and try again.", @"Export folder not writable explanatory text")]; } else { [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")]]; if (parentFoldersMissing == [exportFiles count]) { [alert setInformativeText:NSLocalizedString(@"The target export folder no longer exists. Please select a new export location and try again.", @"Export folder missing explanatory text")]; } else if (parentFoldersMissing) { [alert setInformativeText:NSLocalizedString(@"Some of the target export folders no longer exist. Please select a new export location and try again.", @"Some export folders missing explanatory text")]; } else if (parentFoldersNotWritable) { [alert setInformativeText:NSLocalizedString(@"Some of the target export folders are not writable. Please select a new export location and try again.", @"Some export folders not writable explanatory text")]; } else { [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]]; if (parentFoldersMissing) { [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"%lu of the export files could not be created because their target export folder no longer exists; please select a new export location and try again.", @"Export folder missing for some files explanatory text"), parentFoldersMissing]]; } else if (parentFoldersNotWritable) { [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"%lu of the export files could not be created because their target export folder is not writable; please select a new export location and try again.", @"Export folder not writable for some files explanatory text"), parentFoldersNotWritable]]; } else { [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]; } } [self _hideExportProgress]; [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:files]; [alert autorelease]; } /** * 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:(SPFileCompressionFormat)[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]; } } /** * 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