// // BloggenGenerator.m // Bloggen // // Created by Davis Remmel on 12/2/13. // Copyright (c) 2013 Davis Remmel. All rights reserved. // #import "BloggenGenerator.h" #import "/Users/davisr/Developer/GHMarkdownParser/GHMarkdownParser/GHMarkdownParser/GHMarkdownParser.h" #import "BloggenSandboxUtilities.h" static const int FEED_ITEM_LIMIT = 10; // Max number of items appearing in Atom feed static NSString * const BUILD_DIRECTORY_NAME = @"HTML from Bloggen"; NSURL *TemporaryDirectoryURL; NSURL *LocalBlogDirectoryURL; NSURL *StagedBuildDirectoryURL; NSURL *TemplateDirectoryURL; NSURL *ArchiveBuildDirectoryURL; NSString *SlugFormat; NSString *BlogTitleString; NSString *AuthorNameString; NSString *CopyrightYearsString; NSString *ServerString; NSString *ServerDomain; NSMutableArray *PostMutableArray; // All Post Content NSCondition *UploadHelperCondition; // These are set by asynchronous threads BOOL UploaderIsFinished; int UploaderErrorCount; @implementation BloggenGenerator - (int)generateThisBlog:(NSURL *)blogSSURL withTemplate:(NSURL *)templateURL withNumberOfFrontpagePosts:(NSInteger)numberOfFrontpagePosts withNumberOfFrontpageFullPostBodies:(NSInteger)numberOfFrontpageFullPostBodies withSlugFormat:(NSString *)slugFormat uploadWith:(NSString *)uploadProtocol withUsername:(NSString *)username withPassword:(NSString *)password withSSHBookmark:(NSData *)sshBookmark withKey:(NSURL *)keySSURL toServer:(NSString *)server hasServerChanged:(BOOL)serverHasChanged { /*////////////////////////////////////////// This function returns an error code, depending on it's completion status. These codes are listed here. Code | Description ------------------ -1 | Generic error. 0 | The generator and uploading were both successful. 1 | The generator was not successful (upload not attempted). 2 | The generator was successful, but upload was not. 3 | The clean up procedure was unsuccessful; did not attempt upload. //////////////////////////////////////////*/ //NSLog(@"Generating the blog..."); // NURL Counterparts TemporaryDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]; LocalBlogDirectoryURL = blogSSURL; StagedBuildDirectoryURL = [TemporaryDirectoryURL URLByAppendingPathComponent:BUILD_DIRECTORY_NAME isDirectory:YES]; TemplateDirectoryURL = templateURL; NSString *sandboxedDocumentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSURL *sandboxedDocumentDirectoryURL = [NSURL fileURLWithPath:sandboxedDocumentDirectory isDirectory:YES]; ArchiveBuildDirectoryURL = [sandboxedDocumentDirectoryURL URLByAppendingPathComponent:BUILD_DIRECTORY_NAME isDirectory:YES]; ServerString = server; SlugFormat = slugFormat; ServerDomain = [[[[ServerString componentsSeparatedByString:@"/"] objectAtIndex:0] componentsSeparatedByString:@":"] firstObject]; // Before generating anything, BE ABSOLUTELY SURE a temporary build dir doesn't exist, otherwise the diffs will be wrong NSFileManager *fileManager = [[NSFileManager alloc] init]; [fileManager removeItemAtURL:StagedBuildDirectoryURL error:nil]; // Remove the build directory if it exists int errorCount = 0; errorCount += [self createPostArray]; errorCount += [self copyTemplateAssets]; errorCount += [self getBlogMetadata]; errorCount += [self createPostPage]; errorCount += [self createIndexPage:nil withNumberOfFrontpagePosts:numberOfFrontpagePosts withNumberOfFrontpageArticles:numberOfFrontpageFullPostBodies]; errorCount += [self createArchivePage]; errorCount += [self createFeed]; if (errorCount) { NSLog(@"The blog did not generate successfully."); return 1; } else { NSLog(@"The blog was generated successfully."); } // Because the XPC runs asynchronously, we need to wait for it to finish. // When it's done, uploaderIsDone:uploadExitStatus: is called and sets // the UploaderIsFinished variable to YES. UploaderIsFinished = NO; [self upload:StagedBuildDirectoryURL withProtocol:uploadProtocol withUsername:username withPassword:password withSSHBookmark:sshBookmark keyFile:keySSURL toServer:server hasServerChanged:serverHasChanged]; [UploadHelperCondition lock]; // During this lock and while-loop, Xcode's debugger will show 100% // CPU usage and a 'Very High' energy impact. This is probably an // Xcode bug, because Activity Monitor doesn't show high CPU utilization // at all. while (!UploaderIsFinished) { [UploadHelperCondition wait]; } [UploadHelperCondition unlock]; switch (UploaderErrorCount) { case 0: // Now that our upload process is complete, we can archive // the staged build directory with the cleanUp: method. if (![self cleanUp]) { NSLog(@"Clean up unsuccessful. Aborting upload."); return 3; // Clean up unsuccessful } NSLog(@"Upload complete."); return 0; // No error default: NSLog(@"Uploader had an error. Error count: %d", UploaderErrorCount); return 2; // Upload unsuccessful } } - (int)createPostArray { PostMutableArray = [[NSMutableArray alloc] init]; // Insert posts into array //NSLog(@"Collecting posts."); NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *postsURL = LocalBlogDirectoryURL; // DEAD NSArray *pathContents = [fileManager contentsOfDirectoryAtPath:[postsURL path] error:nil]; NSArray *pathContents = [fileManager contentsOfDirectoryAtURL:postsURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(pathExtension == 'md') OR (pathExtension == 'rtfd')"]; for (NSURL *fileURL in [pathContents filteredArrayUsingPredicate:predicate]) { // Enumerate each post file in the blog directory // DEAD NSString *filenameStringWithoutExtension = [filenameString stringByDeletingPathExtension]; // DEAD NSString *filenameExtension = [filenameString pathExtension]; NSString *filenameStringWithoutExtension = [[fileURL URLByDeletingPathExtension] lastPathComponent]; NSString *filenameExtension = [fileURL pathExtension]; // Get variables from post file // (postSlug, postTitle, postDate, postContent, etc.) NSString *postSlug = filenameStringWithoutExtension; // Replace special characters that shouldn't be in a URL slug postSlug = [postSlug stringByReplacingOccurrencesOfString:@" " withString:@"-"]; postSlug = [postSlug stringByReplacingOccurrencesOfString:@"_" withString:@"-"]; postSlug = [postSlug stringByReplacingOccurrencesOfString:@"&" withString:@"and"]; NSCharacterSet *blacklistedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-&"]; blacklistedCharacters = [blacklistedCharacters invertedSet]; postSlug = [[postSlug componentsSeparatedByCharactersInSet:blacklistedCharacters] componentsJoinedByString:@""]; // Finally, make it lowercase for style postSlug = [postSlug lowercaseString]; // HANDLE FILE TYPES NSString *postContentHTML = @""; // Markdown NSString *postContentMD = @""; if ([filenameExtension isEqualToString:@"md"]) { NSError *readError = nil; NSString *postFileContentString = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:&readError]; if (readError) { NSLog(@"Error reading post file: %@", [readError localizedDescription]); return 1; } NSMutableArray *postFileContentMutableArray = [NSMutableArray arrayWithArray:[postFileContentString componentsSeparatedByString:@"\n"]]; postContentMD = [[postFileContentMutableArray valueForKey:@"description"] componentsJoinedByString:@"\n"]; postContentHTML = [GHMarkdownParser flavoredHTMLStringFromMarkdownString:postContentMD]; } // RTFD if ([filenameExtension isEqualToString:@"rtfd"]) { NSLog(@"rtfd detected"); // Convert the foreign file NSString *filePath = [fileURL path]; NSPipe *stdoutPipe = [NSPipe pipe]; NSTask *task = [[NSTask alloc] init]; [task setLaunchPath:@"/usr/bin/textutil"]; [task setArguments:@[ @"-convert", @"html", filePath, @"-baseurl", @"./", @"-strip", @"-excludedelements", @"(html, xml, head, body, span, div, br)", @"-stdout" ]]; [task setStandardOutput:stdoutPipe]; [task launch]; [task waitUntilExit]; if (0 != [task terminationStatus]) { NSLog(@"Error converting RTFD to HTML. Failed file: %@", filePath); return 2; } NSFileHandle *readPipe = [stdoutPipe fileHandleForReading]; NSData *dataRead = [readPipe readDataToEndOfFile]; NSString *stringRead = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding]; // Now that we have our HTML string, we should do some fixing, // since the string isn't really 'clean'. We should remove things // like empty paragraphs, fix fonts, links, etc. stringRead = [stringRead stringByReplacingOccurrencesOfString:@"

" withString:@""]; stringRead = [stringRead stringByReplacingOccurrencesOfString:@"face=\"Helvetica\" " withString:@""]; } // File attributes // Post Title NSString *postTitle = filenameStringWithoutExtension; // Changed to filename for ease-of-use NSError *readError = nil; NSDictionary *fileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:&readError]; if (readError) { NSLog(@"Error reading post file attributes: %@", [readError localizedDescription]); return 3; } NSDate *postDateObject = [fileAttribs fileCreationDate]; // Get additional variables to add (aides template generation later) // Pretty date NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; [dateFormatter setLocale:enUSPOSIXLocale]; [dateFormatter setDateFormat:@"EEE, MMM d, yyyy 'at' h:mma"]; NSString *postDatePretty = [dateFormatter stringFromDate:postDateObject]; // Additional date variables [dateFormatter setDateFormat:@"yyyy"]; NSString *postDateYear = [dateFormatter stringFromDate:postDateObject]; [dateFormatter setDateFormat:@"MM"]; NSString *postDateMonth = [dateFormatter stringFromDate:postDateObject]; [dateFormatter setDateFormat:@"dd"]; NSString *postDateDay = [dateFormatter stringFromDate:postDateObject]; // Get the full slug, using the provided slug format [dateFormatter setDateFormat:SlugFormat]; NSString *postFullSlug = [dateFormatter stringFromDate:postDateObject]; postFullSlug = [postFullSlug stringByAppendingPathComponent:postSlug]; // Insert post data into the unsorted array NSArray *postArray = @[postSlug, postTitle, postDateObject, postContentMD, postDatePretty, postDateYear, postDateMonth, postDateDay, postContentHTML, postFullSlug ]; [PostMutableArray addObject:postArray]; } // End of post loop // Sort the posts by date (low index is a newer post) //NSLog(@"Sorting posts."); for (int x = 0; x < [PostMutableArray count]; x++) { for (int y = 0; y < [PostMutableArray count]-1; y++) { NSDate *dateAtY = [[PostMutableArray objectAtIndex:y] objectAtIndex:2]; NSDate *dateAtYPlus1 = [[PostMutableArray objectAtIndex:y+1] objectAtIndex:2]; if ([dateAtY compare:dateAtYPlus1] == NSOrderedDescending) { NSArray *temp = [PostMutableArray objectAtIndex:y+1]; [PostMutableArray replaceObjectAtIndex:y+1 withObject:[PostMutableArray objectAtIndex:y]]; [PostMutableArray replaceObjectAtIndex:y withObject:temp]; } } } // Reverse the sort (so low index is an older post) PostMutableArray = [[[PostMutableArray reverseObjectEnumerator] allObjects] mutableCopy]; return 0; } - (int)copyTemplateAssets { //NSLog(@"Copying template and assets.\n"); // Create the build directory NSURL *buildAssetsDirectoryURL = [StagedBuildDirectoryURL URLByAppendingPathComponent:@"assets" isDirectory:YES]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *createBuildPathError = nil; if (![fileManager createDirectoryAtURL:StagedBuildDirectoryURL withIntermediateDirectories:YES attributes:nil error:&createBuildPathError]) { NSLog(@"Error creating build path: %@", [createBuildPathError localizedDescription]); return 1; } // Copy template assets NSURL *templateAssetsDirectoryURL = [TemplateDirectoryURL URLByAppendingPathComponent:@"assets" isDirectory:YES]; if([fileManager fileExistsAtPath:[templateAssetsDirectoryURL path]]) { [fileManager removeItemAtURL:buildAssetsDirectoryURL error:nil]; // Remove build assets (if they exist) } NSError *copyTemplateAssetsError = nil; if(![fileManager copyItemAtURL:templateAssetsDirectoryURL toURL:buildAssetsDirectoryURL error:©TemplateAssetsError]) // Copy template assets to build assets { NSLog(@"Error copying template assets: %@", [copyTemplateAssetsError localizedDescription]); return 2; } return 0; } - (int)getBlogMetadata { //NSLog(@"Reading blog information."); // Blog title BlogTitleString = [LocalBlogDirectoryURL lastPathComponent]; // Author name AuthorNameString = NSFullUserName(); // Copyright years NSString *oldestPostYear = [[PostMutableArray objectAtIndex:[PostMutableArray count]-1] objectAtIndex:5]; NSString *newestPostYear = [[PostMutableArray objectAtIndex:0] objectAtIndex:5]; if ([newestPostYear isEqualToString:oldestPostYear]) { CopyrightYearsString = newestPostYear; } else { CopyrightYearsString = [NSString stringWithFormat:@"%@–%@", oldestPostYear, newestPostYear]; } return 0; } - (int)createPostPage { //NSLog(@"Generating post pages.\n"); for (NSArray *postArray in PostMutableArray) { // Get the pretty date NSString *postDatePretty = [postArray objectAtIndex:4]; // Get the full slug NSString *postFullSlug = [postArray objectAtIndex:9]; // Get the title link NSString *postTitle = [postArray objectAtIndex:1]; NSString *postTitleLink = [NSString stringWithFormat:@"%@", postTitle]; // Get the post body in HTML NSString *postBodyHTML = [postArray objectAtIndex:8]; // Create post's file path NSString *postDateYear = [postArray objectAtIndex:5]; NSString *postDateMonth = [postArray objectAtIndex:6]; NSString *postDateDay = [postArray objectAtIndex:7]; NSURL *localPostDirectory = [StagedBuildDirectoryURL URLByAppendingPathComponent:postFullSlug isDirectory:YES]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *copyError = nil; if (![fileManager createDirectoryAtURL:localPostDirectory withIntermediateDirectories:YES attributes:nil error:©Error]) // Create post directory { NSLog(@"Error creating post path: %@", [copyError localizedDescription]); return 1; } NSURL *templatePostFileURL = [TemplateDirectoryURL URLByAppendingPathComponent:@"post.html" isDirectory:NO]; NSURL *localPostIndexFile = [localPostDirectory URLByAppendingPathComponent:@"index.html" isDirectory:NO]; // Read template file into string NSString *postTemplateString = [self templateWithReplacedGenericPlaceholders:templatePostFileURL]; // Insert post information into template string NSString *postHTML = [postTemplateString stringByReplacingOccurrencesOfString:@"[[[!POST_TITLE!]]]" withString:postTitle]; postHTML = [postHTML stringByReplacingOccurrencesOfString:@"[[[!POST_TITLE_LINK!]]]" withString:postTitleLink]; postHTML = [postHTML stringByReplacingOccurrencesOfString:@"[[[!DATE!]]]" withString:[NSString stringWithFormat:@"%@-%@-%@", postDateYear, postDateMonth, postDateDay]]; postHTML = [postHTML stringByReplacingOccurrencesOfString:@"[[[!PRETTY_DATE!]]]" withString:postDatePretty]; postHTML = [postHTML stringByReplacingOccurrencesOfString:@"[[[!POST_BODY!]]]" withString:postBodyHTML]; // Write the full-formed HTML to the post's index file NSError *writeError = nil; if (![postHTML writeToURL:localPostIndexFile atomically:YES encoding:NSUTF8StringEncoding error:&writeError]) { NSLog(@"Error writing post index file: %@", [writeError localizedDescription]); return 2; } // Detect files in the post body and copy them into the post's directory NSString *postBodyMD = [postArray objectAtIndex:3]; NSArray *filenamesInPost = [self arrayOfLocallyLinkedFilesInPostBodyMD:postBodyMD]; for (NSString *filename in filenamesInPost) { // Copy the matched file to the post's directory NSURL *fileOriginURL = [LocalBlogDirectoryURL URLByAppendingPathComponent:filename isDirectory:NO]; NSURL *fileDestinationURL = [localPostDirectory URLByAppendingPathComponent:filename isDirectory:NO]; NSError *copyError = nil; if (![fileManager copyItemAtURL:fileOriginURL toURL:fileDestinationURL error:©Error]) { NSLog(@"Error copying linked file: %@", [copyError localizedDescription]); } } } return 0; } - (int)createIndexPage:id withNumberOfFrontpagePosts:(NSInteger)numberOfFrontpagePosts withNumberOfFrontpageArticles:(NSInteger)numberOfFrontpageArticles { //NSLog(@"Generating index page.\n"); // Get template html NSURL *templateIndexFileURL = [TemplateDirectoryURL URLByAppendingPathComponent:@"index.html" isDirectory:NO]; NSString *indexHTML = [self templateWithReplacedGenericPlaceholders:templateIndexFileURL]; // Generate article HTML NSString *articleHTML = @""; numberOfFrontpagePosts = (numberOfFrontpagePosts > [PostMutableArray count]) ? [PostMutableArray count] : numberOfFrontpagePosts; // Don't let the number of front page posts exceed the number of actual posts for (NSInteger i = 0; i < numberOfFrontpagePosts; i++) { NSArray *postArray = [PostMutableArray objectAtIndex:i]; articleHTML = [articleHTML stringByAppendingString:@"
"]; NSString *title = [postArray objectAtIndex:1]; NSString *postFullSlug = [postArray objectAtIndex:9]; NSString *fullSlugPath = [NSString stringWithFormat:@"./%@", postFullSlug]; NSString *postTitleHTML = [NSString stringWithFormat:@"

%@

", fullSlugPath, title]; articleHTML = [articleHTML stringByAppendingString:postTitleHTML]; NSString *prettyDate = [postArray objectAtIndex:4]; NSString *postDateHTML = [NSString stringWithFormat:@"%@", prettyDate]; articleHTML = [articleHTML stringByAppendingString:postDateHTML]; if (i < numberOfFrontpageArticles) { // We can't take the stored article HTML because it may contain file links meant for a post directory // So we prefix those links with their post directories, then regenerate the HTML NSString *fixedPostBodyMD = [postArray objectAtIndex:3]; NSArray *filenamesInPost = [self arrayOfLocallyLinkedFilesInPostBodyMD:fixedPostBodyMD]; for (NSString *filename in filenamesInPost) { NSString *replaceThisInstance = [NSString stringWithFormat:@"](%@", filename]; NSString *fixedLink = [NSString stringWithFormat:@"](%@/%@", fullSlugPath, filename]; fixedPostBodyMD = [fixedPostBodyMD stringByReplacingOccurrencesOfString:replaceThisInstance withString:fixedLink]; } NSString *fixedPostBodyHTML = [GHMarkdownParser HTMLStringFromMarkdownString:fixedPostBodyMD]; articleHTML = [articleHTML stringByAppendingString:fixedPostBodyHTML]; } articleHTML = [articleHTML stringByAppendingString:@"
"]; } indexHTML = [indexHTML stringByReplacingOccurrencesOfString:@"[[[!FRONTPAGE_ARTICLES!]]]" withString:articleHTML]; // Write the index page to disk NSURL *indexFileURL = [StagedBuildDirectoryURL URLByAppendingPathComponent:@"index.html" isDirectory:NO]; NSError *writeError = nil; if (![indexHTML writeToURL:indexFileURL atomically:YES encoding:NSUTF8StringEncoding error:&writeError]) { NSLog(@"Error creating main index file: %@", [writeError localizedDescription]); return 1; } return 0; } - (int)createArchivePage { //NSLog(@"Generating archive page.\n"); // Make the archive directory NSURL *archiveDirectoryURL = [StagedBuildDirectoryURL URLByAppendingPathComponent:@"archive" isDirectory:YES]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *createArchiveDirectoryError = nil; if (![fileManager createDirectoryAtURL:archiveDirectoryURL withIntermediateDirectories:YES attributes:nil error:&createArchiveDirectoryError]) { NSLog(@"Error creating archive directory: %@", [createArchiveDirectoryError localizedDescription]); return 1; } // Read the archive template NSURL *archiveTemplateFileURL = [TemplateDirectoryURL URLByAppendingPathComponent:@"archive.html" isDirectory:NO]; NSString *archiveHTML = [self templateWithReplacedGenericPlaceholders:archiveTemplateFileURL]; NSMutableArray *archiveHTMLMutableArray = [[archiveHTML componentsSeparatedByString:@"[[[!ARTICLES!]]]"] mutableCopy]; // Temporarily reverse post order from newest-to-oldest, so they are insererted properly in the section below. PostMutableArray = [[[PostMutableArray reverseObjectEnumerator] allObjects] mutableCopy]; // Enumerate through the posts for (NSArray *postArray in PostMutableArray) { NSString *postTitle = [postArray objectAtIndex:1]; NSString *postPrettyDate = [postArray objectAtIndex:4]; NSString *postFullSlug = [postArray objectAtIndex:9]; NSString *postLink = [NSString stringWithFormat:@"%@", postFullSlug, postTitle]; NSString *articleHTML = [NSString stringWithFormat:@"

%@

%@
", postLink, postPrettyDate]; [archiveHTMLMutableArray insertObject:articleHTML atIndex:1]; // Orders the posts within the mutable array in-line with HTML } // Reverse the temporary re-ordering PostMutableArray = [[[PostMutableArray reverseObjectEnumerator] allObjects] mutableCopy]; // Flatten the mutable array into a string archiveHTML = [archiveHTMLMutableArray componentsJoinedByString:@"\n"]; // Write the final archive HTML to disk NSURL *archiveIndexFileURL = [archiveDirectoryURL URLByAppendingPathComponent:@"index.html" isDirectory:NO]; NSError *writeError = nil; if (![archiveHTML writeToURL:archiveIndexFileURL atomically:YES encoding:NSUTF8StringEncoding error:&writeError]) { NSLog(@"Error creating archive index file: %@", [writeError localizedDescription]); return 2; } return 0; } - (int)createFeed { //NSLog(@"Generating feed.\n"); // Read the feed template NSURL *feedTemplateFileURL = [TemplateDirectoryURL URLByAppendingPathComponent:@"feed.atom" isDirectory:NO]; NSString *feedContent = [self templateWithReplacedGenericPlaceholders:feedTemplateFileURL]; NSMutableArray *feedContentMutableArray = [[feedContent componentsSeparatedByString:@"[[[!ENTRIES!]]]"] mutableCopy]; int feedItemLimit = FEED_ITEM_LIMIT; int numberOfPosts = (int)[PostMutableArray count]; if (feedItemLimit > numberOfPosts) { feedItemLimit = numberOfPosts; // Don't let the feedItemLimit exceed the number of posts in the array } for (int i = feedItemLimit; i > 0; i--) // Count backwards for newest posts first { NSArray *postArray = [PostMutableArray objectAtIndex:(i-1)]; // newest post NSString *postTitle = [postArray objectAtIndex:1]; NSString *postContentHTML = [postArray objectAtIndex:8]; NSString *postFullSlug = [postArray objectAtIndex:9]; // Also need iso8601 date NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; [dateFormatter setLocale:enUSPOSIXLocale]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; // What's with the Z and Atom? Still don't know... NSString *postDateISO = [dateFormatter stringFromDate:[postArray objectAtIndex:2]]; NSString *domain = ServerDomain; NSString *entryHTML = [NSString stringWithFormat:@"%@http://%@/%@%@%@%@", postTitle, domain, postFullSlug, domain, postFullSlug, postDateISO, AuthorNameString, postContentHTML]; [feedContentMutableArray insertObject:entryHTML atIndex:1]; // Orders the posts within the mutable array in-line } // Flatten the feed content array into a string feedContent = [feedContentMutableArray componentsJoinedByString:@"\n"]; // Replace the date NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; [dateFormatter setLocale:enUSPOSIXLocale]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; // What's with the Z and Atom? Still don't know... NSDate *dateNow = [NSDate date]; NSString *dateNowString = [dateFormatter stringFromDate:dateNow]; feedContent = [feedContent stringByReplacingOccurrencesOfString:@"[[[!UPDATE_TIME!]]]" withString:dateNowString]; // Write the final atom feed to disk NSURL *feedFileURL = [StagedBuildDirectoryURL URLByAppendingPathComponent:@"feed.atom" isDirectory:NO]; NSError *writeError = nil; if (![feedContent writeToURL:feedFileURL atomically:YES encoding:NSUTF8StringEncoding error:&writeError]) { NSLog(@"Error creating feed file: %@", [writeError localizedDescription]); return 1; } return 0; } - (BOOL)cleanUp { NSFileManager *fileManager = [[NSFileManager alloc] init]; NSError *error = nil; if (![fileManager removeItemAtURL:ArchiveBuildDirectoryURL error:&error]) { NSLog(@"Error removing archived build: %@ If this is the first time running Bloggen, there is not yet an archived build to remove.", [error localizedDescription]); // Not a problem if it doesn't exist, so don't return anything } if (![fileManager copyItemAtURL:StagedBuildDirectoryURL toURL:ArchiveBuildDirectoryURL error:&error]) { NSLog(@"Error archiving staged build: %@", [error localizedDescription]); return NO; } if (![fileManager removeItemAtURL:StagedBuildDirectoryURL error:&error]) { NSLog(@"Error removing staged build: %@", [error localizedDescription]); return NO; } return YES; } - (void)upload:(NSURL *)sourceDirectoryURL withProtocol:(NSString *)protocol withUsername:(NSString *)username withPassword:(NSString *)password withSSHBookmark:(NSData *)sshBookmark keyFile:(NSURL *)keyFileSSURL toServer:(NSString *)server hasServerChanged:(BOOL)serverHasChanged { BOOL okToUpload = YES; // Running-check. Only change from YES->NO, so it sours. NSString *errorMessage = @""; // Just to be safe, we're going to error check all the input variables. // This isn't the time to be messing around, because there's no way a // normal user will know how to handle a failed upload. That's probably // enough to make them uninstall Bloggen. // Protocol check if (!([protocol isEqualToString:@"FTP"] || [protocol isEqualToString:@"S3"] || [protocol isEqualToString:@"Rsync"])) { okToUpload = NO; errorMessage = [NSString stringWithFormat:@"%@The selected upload protocol \"%@\" isn't supported. ", errorMessage, protocol]; } // Username and server check if ([username length] && [server length]) { // Super! } else { okToUpload = NO; errorMessage = [errorMessage stringByAppendingString:@"A username and server are required in Preferences. "]; } // Kick out if not ok if (!okToUpload) { NSLog(@"Unable to upload. %@", errorMessage); [self uploaderIsDone:1 XPCConnection:nil]; return; // Terminate this thread } // Find the files that changed from the previous build NSMutableArray *fileAttributesMutableArray = [[NSMutableArray alloc] init]; // Stores relative paths of files // Get file paths // We aren't using enumerateAtURL: because that returns absolute paths, // and we need relative paths because the two directories have mirrored // file trees, and derive this from the relative path. NSFileManager *fileManager = [[NSFileManager alloc] init]; NSDirectoryEnumerator *buildPathEnumerator = [fileManager enumeratorAtPath:[sourceDirectoryURL path]]; // Now we make an array of relative file paths, and a flag of whether // they have changed from the previous build. This provides a 'diff' // so the upload helper will only upload changed files. for (NSString *relativePath in buildPathEnumerator) { NSURL *fileInBuildDirectoryURL = [sourceDirectoryURL URLByAppendingPathComponent:relativePath]; // DO NOT define if this is directory: we don't know! BOOL isDir; [fileManager fileExistsAtPath:[fileInBuildDirectoryURL path] isDirectory:&isDir]; if (!isDir) // Act only on files, not directories { BOOL filesAreIdentical = NO; if (!serverHasChanged) { // Check if the file has changed from the previous build NSURL *newFileURL = fileInBuildDirectoryURL; NSURL *oldFileURL = [ArchiveBuildDirectoryURL URLByAppendingPathComponent:relativePath isDirectory:NO]; filesAreIdentical = [fileManager contentsEqualAtPath:[newFileURL path] andPath:[oldFileURL path]]; } // Add the file attributes to the fileListMutableArray NSNumber *fileHasChangedAsObject = [NSNumber numberWithBool:filesAreIdentical]; // Can only add objects to arrays NSArray *fileAttributesArray = @[fileHasChangedAsObject, relativePath]; [fileAttributesMutableArray addObject:fileAttributesArray]; } } // Now that we have the file paths, enumerate through them NSMutableArray *nonIdenticalFilePathMutableArray = [[NSMutableArray alloc] init]; for (NSArray *fileAttributesArray in fileAttributesMutableArray) { BOOL filesAreIdentical = [[fileAttributesArray objectAtIndex:0] boolValue]; NSString *relativeFilePath = [fileAttributesArray objectAtIndex:1]; NSURL *fileInBuildDirectoryURL = [sourceDirectoryURL URLByAppendingPathComponent:relativeFilePath isDirectory:NO]; if (!filesAreIdentical) { // Add them to the non-identical file array NSArray *filePathArray = @[ relativeFilePath, [fileInBuildDirectoryURL path] ]; // XPC can't accept NSURLs [nonIdenticalFilePathMutableArray addObject:filePathArray]; } } // Only basic object types are allowed over the XPC, so we need a flat array NSArray *nonIdenticalFilePathArray = [nonIdenticalFilePathMutableArray copy]; // Ok, now we can upload the files which are different from the previous build if ([protocol isEqualToString:@"FTP"]) { // The BloggenUploadHelper process runs in its own sandbox, so // it won't be able to acess the security-scoped URLs of this // application. Therefore, it needs to be passed vanilla bookmarks // and resolve its own security-scoped URLs. NSData *directoryBookmark = [BloggenSandboxUtilities bookmarkForURL:sourceDirectoryURL]; // Initiate a connection to the XPC helper NSXPCInterface *xpcInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BloggenUploadHelperXPCProtocol)]; __block NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"me.davisr.BloggenUploadHelper"]; xpcConnection.remoteObjectInterface = xpcInterface; [xpcConnection resume]; // Initiate FTP upload [[xpcConnection remoteObjectProxy] ftpChangedFilesInArray:nonIdenticalFilePathArray withDirectoryBookmark:directoryBookmark withUsername:username withPassword:password toServer:server error:^(int returnedErrorCount) { [self uploaderIsDone:returnedErrorCount XPCConnection:xpcConnection]; } ]; } if ([protocol isEqualToString:@"S3"]) { // The BloggenUploadHelper process runs in its own sandbox, so // it won't be able to acess the security-scoped URLs of this // application. Therefore, it needs to be passed vanilla bookmarks // and resolve its own security-scoped URLs. NSData *directoryBookmark = [BloggenSandboxUtilities bookmarkForURL:sourceDirectoryURL]; // Initiate a connection to the XPC helper NSXPCInterface *xpcInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BloggenUploadHelperXPCProtocol)]; __block NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"me.davisr.BloggenUploadHelper"]; xpcConnection.remoteObjectInterface = xpcInterface; [xpcConnection resume]; // Initiate S3 upload [[xpcConnection remoteObjectProxy] s3ChangedFilesInArray:nonIdenticalFilePathArray withDirectoryBookmark:directoryBookmark withAccessKeyID:username withSecretAccessKey:password toBucket:server error:^(int returnedErrorCount) { [self uploaderIsDone:returnedErrorCount XPCConnection:xpcConnection]; } ]; } if([protocol isEqualToString:@"Rsync"]) { // The BloggenUploadHelper process runs in its own sandbox, so // it won't be able to acess the security-scoped URLs of this // application. Therefore, it needs to be passed vanilla bookmarks // and resolve its own security-scoped URLs. if ([keyFileSSURL startAccessingSecurityScopedResource]) { NSData *directoryBookmark = [BloggenSandboxUtilities bookmarkForURL:sourceDirectoryURL]; NSString *keyFilePath = [keyFileSSURL path]; NSURL *keyFileURL = [NSURL fileURLWithPath:keyFilePath isDirectory:NO]; NSData *keyFileBookmark = [BloggenSandboxUtilities bookmarkForURL:keyFileURL]; [keyFileSSURL stopAccessingSecurityScopedResource]; // Initiate a connection to the XPC helper NSXPCInterface *xpcInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BloggenUploadHelperXPCProtocol)]; __block NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"me.davisr.BloggenUploadHelper"]; xpcConnection.remoteObjectInterface = xpcInterface; [xpcConnection resume]; // Initiate rsync of build path [[xpcConnection remoteObjectProxy] rsyncDirectoryBookmark:directoryBookmark withUsername:username withSSHBookmark:sshBookmark withKeyFileBookmark:keyFileBookmark toServer:server exitStatus:^(int exitStatus) { [self uploaderIsDone:exitStatus XPCConnection:xpcConnection]; } ]; } else { NSLog(@"Couldn't start rsync process because the key file (which is necessary) is unaccessable."); [self uploaderIsDone:1 XPCConnection:nil]; } } } - (void)uploaderIsDone:(int)uploaderExitStatus XPCConnection:(NSXPCConnection *)xpcConnection { UploaderErrorCount = uploaderExitStatus; [xpcConnection invalidate]; UploaderIsFinished = YES; [UploadHelperCondition signal]; } - (NSString *)templateWithReplacedGenericPlaceholders:(NSURL *)templateFileURL { // Read the template file into a string NSString *templateContentsString = [NSString stringWithContentsOfURL:templateFileURL encoding:NSUTF8StringEncoding error:nil]; // Replace generic template strings templateContentsString = [templateContentsString stringByReplacingOccurrencesOfString:@"[[[!BLOG_TITLE!]]]" withString:BlogTitleString]; templateContentsString = [templateContentsString stringByReplacingOccurrencesOfString:@"[[[!COPYRIGHT_YEARS!]]]" withString:CopyrightYearsString]; templateContentsString = [templateContentsString stringByReplacingOccurrencesOfString:@"[[[!AUTHOR!]]]" withString:AuthorNameString]; templateContentsString = [templateContentsString stringByReplacingOccurrencesOfString:@"[[[!BLOGGEN_VERSION!]]]" withString:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]; templateContentsString = [templateContentsString stringByReplacingOccurrencesOfString:@"[[[!DOMAIN!]]]" withString:ServerString]; return templateContentsString; } - (NSArray *)arrayOfLocallyLinkedFilesInPostBodyMD:(NSString *)postBodyMD { NSMutableArray *arrayOfLocallyLinkedFiles = [[NSMutableArray alloc] init]; // We need to match all the locally-linked files, but not remote links. // All local files have the pattern: // // [](ourfile.md .. // // So, we want to match the thing behind that first parenthesis, but // before a following space (or another parenthesis). // HOWEVER, it's also important to remember that we should not match // anything between ` marks, because that's Markdown for code brackets. // ANYTHING inside of code brackets should never be touched, ever! NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\[.*\\]\\((.+?)[ \\)]" options:NSRegularExpressionCaseInsensitive error:nil]; [regex enumerateMatchesInString:postBodyMD options:0 range:NSMakeRange(0, [postBodyMD length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) { // Handle match results NSString *matchedFilename = [postBodyMD substringWithRange:[match rangeAtIndex:1]]; if (!([matchedFilename rangeOfString:@"://"].location == NSNotFound)) { return; // Exit the loop now, the match is not a local file } // Add the filename to the array [arrayOfLocallyLinkedFiles addObject:matchedFilename]; }]; return [arrayOfLocallyLinkedFiles copy]; } @end