//
// 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 "SPAlertSheets.h"
#import "SPExportFile.h"
#import "SPDatabaseDocument.h"
#import "SPCustomQuery.h"
#import
typedef enum
{
SPExportErrorCancelExport = 0,
SPExportErrorReplaceFiles = 1,
SPExportErrorSkipErrorFiles = 2
}
SPExportErrorChoice;
@interface SPExportController (SPExportFileUtilitiesPrivateAPI)
- (void)_reopenExportSheet;
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
@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 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];
}
}
// 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:(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