123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- //
- // CompressingLogFileManager.m
- // LogFileCompressor
- //
- // CocoaLumberjack Demos
- //
- #import "CompressingLogFileManager.h"
- #import <zlib.h>
- // We probably shouldn't be using DDLog() statements within the DDLog implementation.
- // But we still want to leave our log statements for any future debugging,
- // and to allow other developers to trace the implementation (which is a great learning tool).
- //
- // So we use primitive logging macros around NSLog.
- // We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog.
- #define LOG_LEVEL 4
- #define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog(frmt, ##__VA_ARGS__); } while(0)
- #define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog(frmt, ##__VA_ARGS__); } while(0)
- #define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog(frmt, ##__VA_ARGS__); } while(0)
- #define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog(frmt, ##__VA_ARGS__); } while(0)
- @interface CompressingLogFileManager (/* Must be nameless for properties */)
- @property (readwrite) BOOL isCompressing;
- @end
- @interface DDLogFileInfo (Compressor)
- @property (nonatomic, readonly) BOOL isCompressed;
- - (NSString *)tempFilePathByAppendingPathExtension:(NSString *)newExt;
- - (NSString *)fileNameByAppendingPathExtension:(NSString *)newExt;
- @end
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- @implementation CompressingLogFileManager
- @synthesize isCompressing;
- - (id)init
- {
- return [self initWithLogsDirectory:nil];
- }
- - (id)initWithLogsDirectory:(NSString *)aLogsDirectory
- {
- if ((self = [super initWithLogsDirectory:aLogsDirectory]))
- {
- upToDate = NO;
-
- // Check for any files that need to be compressed.
- // But don't start right away.
- // Wait for the app startup process to finish.
-
- [self performSelector:@selector(compressNextLogFile) withObject:nil afterDelay:5.0];
- }
- return self;
- }
- - (void)dealloc
- {
- [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(compressNextLogFile) object:nil];
- }
- - (void)compressLogFile:(DDLogFileInfo *)logFile
- {
- self.isCompressing = YES;
- CompressingLogFileManager* __weak weakSelf = self;
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
- [weakSelf backgroundThread_CompressLogFile:logFile];
- });
- }
- - (void)compressNextLogFile
- {
- if (self.isCompressing)
- {
- // We're already compressing a file.
- // Wait until it's done to move onto the next file.
- return;
- }
-
- NSLogVerbose(@"CompressingLogFileManager: compressNextLogFile");
-
- upToDate = NO;
-
- NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
-
- NSUInteger count = [sortedLogFileInfos count];
- if (count == 0)
- {
- // Nothing to compress
- upToDate = YES;
- return;
- }
-
- NSUInteger i = count;
- while (i > 0)
- {
- DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:(i - 1)];
-
- if (logFileInfo.isArchived && !logFileInfo.isCompressed)
- {
- [self compressLogFile:logFileInfo];
-
- break;
- }
-
- i--;
- }
-
- upToDate = YES;
- }
- - (void)compressionDidSucceed:(DDLogFileInfo *)logFile
- {
- NSLogVerbose(@"CompressingLogFileManager: compressionDidSucceed: %@", logFile.fileName);
-
- self.isCompressing = NO;
-
- [self compressNextLogFile];
- }
- - (void)compressionDidFail:(DDLogFileInfo *)logFile
- {
- NSLogWarn(@"CompressingLogFileManager: compressionDidFail: %@", logFile.fileName);
-
- self.isCompressing = NO;
-
- // We should try the compression again, but after a short delay.
- //
- // If the compression failed there is probably some filesystem issue,
- // so flooding it with compression attempts is only going to make things worse.
-
- NSTimeInterval delay = (60 * 15); // 15 minutes
-
- [self performSelector:@selector(compressNextLogFile) withObject:nil afterDelay:delay];
- }
- - (void)didArchiveLogFile:(NSString *)logFilePath
- {
- NSLogVerbose(@"CompressingLogFileManager: didArchiveLogFile: %@", [logFilePath lastPathComponent]);
-
- // If all other log files have been compressed,
- // then we can get started right away.
- // Otherwise we should just wait for the current compression process to finish.
-
- if (upToDate)
- {
- [self compressLogFile:[DDLogFileInfo logFileWithPath:logFilePath]];
- }
- }
- - (void)didRollAndArchiveLogFile:(NSString *)logFilePath
- {
- NSLogVerbose(@"CompressingLogFileManager: didRollAndArchiveLogFile: %@", [logFilePath lastPathComponent]);
-
- // If all other log files have been compressed,
- // then we can get started right away.
- // Otherwise we should just wait for the current compression process to finish.
-
- if (upToDate)
- {
- [self compressLogFile:[DDLogFileInfo logFileWithPath:logFilePath]];
- }
- }
- - (void)backgroundThread_CompressLogFile:(DDLogFileInfo *)logFile
- {
- @autoreleasepool {
-
- NSLogInfo(@"CompressingLogFileManager: Compressing log file: %@", logFile.fileName);
-
- // Steps:
- // 1. Create a new file with the same fileName, but added "gzip" extension
- // 2. Open the new file for writing (output file)
- // 3. Open the given file for reading (input file)
- // 4. Setup zlib for gzip compression
- // 5. Read a chunk of the given file
- // 6. Compress the chunk
- // 7. Write the compressed chunk to the output file
- // 8. Repeat steps 5 - 7 until the input file is exhausted
- // 9. Close input and output file
- // 10. Teardown zlib
-
-
- // STEP 1
-
- NSString *inputFilePath = logFile.filePath;
-
- NSString *tempOutputFilePath = [logFile tempFilePathByAppendingPathExtension:@"gz"];
-
- #if TARGET_OS_IPHONE
- // We use the same protection as the original file. This means that it has the same security characteristics.
- // Also, if the app can run in the background, this means that it gets
- // NSFileProtectionCompleteUntilFirstUserAuthentication so that we can do this compression even with the
- // device locked. c.f. DDFileLogger.doesAppRunInBackground.
- NSString* protection = logFile.fileAttributes[NSFileProtectionKey];
- NSDictionary* attributes = protection == nil ? nil : @{NSFileProtectionKey: protection};
- [[NSFileManager defaultManager] createFileAtPath:tempOutputFilePath contents:nil attributes:attributes];
- #endif
-
- // STEP 2 & 3
-
- NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:inputFilePath];
- NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:tempOutputFilePath append:NO];
-
- [inputStream open];
- [outputStream open];
-
- // STEP 4
-
- z_stream strm;
-
- // Zero out the structure before (to be safe) before we start using it
- bzero(&strm, sizeof(strm));
-
- strm.zalloc = Z_NULL;
- strm.zfree = Z_NULL;
- strm.opaque = Z_NULL;
- strm.total_out = 0;
-
- // Compresssion Levels:
- // Z_NO_COMPRESSION
- // Z_BEST_SPEED
- // Z_BEST_COMPRESSION
- // Z_DEFAULT_COMPRESSION
-
- deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY);
-
- // Prepare our variables for steps 5-7
- //
- // inputDataLength : Total length of buffer that we will read file data into
- // outputDataLength : Total length of buffer that zlib will output compressed bytes into
- //
- // Note: The output buffer can be smaller than the input buffer because the
- // compressed/output data is smaller than the file/input data (obviously).
- //
- // inputDataSize : The number of bytes in the input buffer that have valid data to be compressed.
- //
- // Imagine compressing a tiny file that is actually smaller than our inputDataLength.
- // In this case only a portion of the input buffer would have valid file data.
- // The inputDataSize helps represent the portion of the buffer that is valid.
- //
- // Imagine compressing a huge file, but consider what happens when we get to the very end of the file.
- // The last read will likely only fill a portion of the input buffer.
- // The inputDataSize helps represent the portion of the buffer that is valid.
-
- NSUInteger inputDataLength = (1024 * 2); // 2 KB
- NSUInteger outputDataLength = (1024 * 1); // 1 KB
-
- NSMutableData *inputData = [NSMutableData dataWithLength:inputDataLength];
- NSMutableData *outputData = [NSMutableData dataWithLength:outputDataLength];
-
- NSUInteger inputDataSize = 0;
-
- BOOL done = YES;
- NSError* error = nil;
- do
- {
- @autoreleasepool {
-
- // STEP 5
- // Read data from the input stream into our input buffer.
- //
- // inputBuffer : pointer to where we want the input stream to copy bytes into
- // inputBufferLength : max number of bytes the input stream should read
- //
- // Recall that inputDataSize is the number of valid bytes that already exist in the
- // input buffer that still need to be compressed.
- // This value is usually zero, but may be larger if a previous iteration of the loop
- // was unable to compress all the bytes in the input buffer.
- //
- // For example, imagine that we ready 2K worth of data from the file in the last loop iteration,
- // but when we asked zlib to compress it all, zlib was only able to compress 1.5K of it.
- // We would still have 0.5K leftover that still needs to be compressed.
- // We want to make sure not to skip this important data.
- //
- // The [inputData mutableBytes] gives us a pointer to the beginning of the underlying buffer.
- // When we add inputDataSize we get to the proper offset within the buffer
- // at which our input stream can start copying bytes into without overwriting anything it shouldn't.
-
- const void *inputBuffer = [inputData mutableBytes] + inputDataSize;
- NSUInteger inputBufferLength = inputDataLength - inputDataSize;
-
- NSInteger readLength = [inputStream read:(uint8_t *)inputBuffer maxLength:inputBufferLength];
- if (readLength < 0) {
- error = [inputStream streamError];
- break;
- }
-
- NSLogVerbose(@"CompressingLogFileManager: Read %li bytes from file", (long)readLength);
-
- inputDataSize += readLength;
-
- // STEP 6
- // Ask zlib to compress our input buffer.
- // Tell it to put the compressed bytes into our output buffer.
-
- strm.next_in = (Bytef *)[inputData mutableBytes]; // Read from input buffer
- strm.avail_in = (uInt)inputDataSize; // as much as was read from file (plus leftovers).
-
- strm.next_out = (Bytef *)[outputData mutableBytes]; // Write data to output buffer
- strm.avail_out = (uInt)outputDataLength; // as much space as is available in the buffer.
-
- // When we tell zlib to compress our data,
- // it won't directly tell us how much data was processed.
- // Instead it keeps a running total of the number of bytes it has processed.
- // In other words, every iteration from the loop it increments its total values.
- // So to figure out how much data was processed in this iteration,
- // we fetch the totals before we ask it to compress data,
- // and then afterwards we subtract from the new totals.
-
- NSInteger prevTotalIn = strm.total_in;
- NSInteger prevTotalOut = strm.total_out;
-
- int flush = [inputStream hasBytesAvailable] ? Z_SYNC_FLUSH : Z_FINISH;
- deflate(&strm, flush);
-
- NSInteger inputProcessed = strm.total_in - prevTotalIn;
- NSInteger outputProcessed = strm.total_out - prevTotalOut;
-
- NSLogVerbose(@"CompressingLogFileManager: Total bytes uncompressed: %lu", (unsigned long)strm.total_in);
- NSLogVerbose(@"CompressingLogFileManager: Total bytes compressed: %lu", (unsigned long)strm.total_out);
- NSLogVerbose(@"CompressingLogFileManager: Compression ratio: %.1f%%",
- (1.0F - (float)(strm.total_out) / (float)(strm.total_in)) * 100);
-
- // STEP 7
- // Now write all compressed bytes to our output stream.
- //
- // It is theoretically possible that the write operation doesn't write everything we ask it to.
- // Although this is highly unlikely, we take precautions.
- // Also, we watch out for any errors (maybe the disk is full).
-
- NSUInteger totalWriteLength = 0;
- NSInteger writeLength = 0;
-
- do
- {
- const void *outputBuffer = [outputData mutableBytes] + totalWriteLength;
- NSUInteger outputBufferLength = outputProcessed - totalWriteLength;
-
- writeLength = [outputStream write:(const uint8_t *)outputBuffer maxLength:outputBufferLength];
-
- if (writeLength < 0)
- {
- error = [outputStream streamError];
- }
- else
- {
- totalWriteLength += writeLength;
- }
-
- } while((totalWriteLength < outputProcessed) && !error);
-
- // STEP 7.5
- //
- // We now have data in our input buffer that has already been compressed.
- // We want to remove all the processed data from the input buffer,
- // and we want to move any unprocessed data to the beginning of the buffer.
- //
- // If the amount processed is less than the valid buffer size, we have leftovers.
-
- NSUInteger inputRemaining = inputDataSize - inputProcessed;
- if (inputRemaining > 0)
- {
- void *inputDst = [inputData mutableBytes];
- void *inputSrc = [inputData mutableBytes] + inputProcessed;
-
- memmove(inputDst, inputSrc, inputRemaining);
- }
-
- inputDataSize = inputRemaining;
-
- // Are we done yet?
-
- done = ((flush == Z_FINISH) && (inputDataSize == 0));
-
- // STEP 8
- // Loop repeats until end of data (or unlikely error)
-
- } // end @autoreleasepool
-
- } while (!done && error == nil);
-
- // STEP 9
-
- [inputStream close];
- [outputStream close];
-
- // STEP 10
-
- deflateEnd(&strm);
-
- // We're done!
- // Report success or failure back to the logging thread/queue.
-
- if (error)
- {
- // Remove output file.
- // Our compression attempt failed.
- NSLogError(@"Compression of %@ failed: %@", inputFilePath, error);
- error = nil;
- BOOL ok = [[NSFileManager defaultManager] removeItemAtPath:tempOutputFilePath error:&error];
- if (!ok)
- NSLogError(@"Failed to clean up %@ after failed compression: %@", tempOutputFilePath, error);
-
- // Report failure to class via logging thread/queue
-
- dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
-
- [self compressionDidFail:logFile];
- }});
- }
- else
- {
- // Remove original input file.
- // It will be replaced with the new compressed version.
- error = nil;
- BOOL ok = [[NSFileManager defaultManager] removeItemAtPath:inputFilePath error:&error];
- if (!ok)
- NSLogWarn(@"Warning: failed to remove original file %@ after compression: %@", inputFilePath, error);
-
- // Mark the compressed file as archived,
- // and then move it into its final destination.
- //
- // temp-log-ABC123.txt.gz -> log-ABC123.txt.gz
- //
- // The reason we were using the "temp-" prefix was so the file would not be
- // considered a log file while it was only partially complete.
- // Only files that begin with "log-" are considered log files.
-
- DDLogFileInfo *compressedLogFile = [DDLogFileInfo logFileWithPath:tempOutputFilePath];
- compressedLogFile.isArchived = YES;
-
- NSString *outputFileName = [logFile fileNameByAppendingPathExtension:@"gz"];
- [compressedLogFile renameFile:outputFileName];
-
- // Report success to class via logging thread/queue
-
- dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
-
- [self compressionDidSucceed:compressedLogFile];
- }});
- }
-
- } // end @autoreleasepool
- }
-
- @end
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- @implementation DDLogFileInfo (Compressor)
- @dynamic isCompressed;
- - (BOOL)isCompressed
- {
- return [[[self fileName] pathExtension] isEqualToString:@"gz"];
- }
- - (NSString *)tempFilePathByAppendingPathExtension:(NSString *)newExt
- {
- // Example:
- //
- // Current File Name: "/full/path/to/log-ABC123.txt"
- //
- // newExt: "gzip"
- // result: "/full/path/to/temp-log-ABC123.txt.gzip"
-
- NSString *tempFileName = [NSString stringWithFormat:@"temp-%@", [self fileName]];
-
- NSString *newFileName = [tempFileName stringByAppendingPathExtension:newExt];
-
- NSString *fileDir = [[self filePath] stringByDeletingLastPathComponent];
-
- NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName];
-
- return newFilePath;
- }
- - (NSString *)fileNameByAppendingPathExtension:(NSString *)newExt
- {
- // Example:
- //
- // Current File Name: "log-ABC123.txt"
- //
- // newExt: "gzip"
- // result: "log-ABC123.txt.gzip"
-
- NSString *fileNameExtension = [[self fileName] pathExtension];
-
- if ([fileNameExtension isEqualToString:newExt])
- {
- return [self fileName];
- }
-
- return [[self fileName] stringByAppendingPathExtension:newExt];
- }
- @end
|