", 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