| #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 |