// // DDLogFileInfo.m // NVLogManagerDemo // // Created by cxb on 2023/5/10. // Copyright © 2023 com.zhouxi. All rights reserved. // #import "DDLogFileInfo.h" static NSString * const kDDXAttrArchivedName = @"guru.log.archived"; @interface DDLogFileInfo () { __strong NSString *_filePath; __strong NSString *_fileName; __strong NSDictionary *_fileAttributes; __strong NSDate *_creationDate; __strong NSDate *_modificationDate; unsigned long long _fileSize; } #if TARGET_IPHONE_SIMULATOR // Old implementation of extended attributes on the simulator. - (BOOL)_hasExtensionAttributeWithName:(NSString *)attrName; - (void)_removeExtensionAttributeWithName:(NSString *)attrName; #endif @end @implementation DDLogFileInfo @synthesize filePath; @dynamic fileName; @dynamic fileAttributes; @dynamic creationDate; @dynamic modificationDate; @dynamic fileSize; @dynamic age; @dynamic isArchived; #pragma mark Lifecycle + (instancetype)logFileWithPath:(NSString *)aFilePath { if (!aFilePath) return nil; return [[self alloc] initWithFilePath:aFilePath]; } - (instancetype)initWithFilePath:(NSString *)aFilePath { NSParameterAssert(aFilePath); if ((self = [super init])) { filePath = [aFilePath copy]; } return self; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Standard Info //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSDictionary *)fileAttributes { if (_fileAttributes == nil && filePath != nil) { __autoreleasing NSError *error = nil; _fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; if (!_fileAttributes) { NSLogError(@"DDLogFileInfo: Failed to read file attributes: %@", error); } } return _fileAttributes ?: @{}; } - (NSString *)fileName { if (_fileName == nil) { _fileName = [filePath lastPathComponent]; } return _fileName; } - (NSDate *)modificationDate { if (_modificationDate == nil) { _modificationDate = self.fileAttributes[NSFileModificationDate]; } return _modificationDate; } - (NSDate *)creationDate { if (_creationDate == nil) { _creationDate = self.fileAttributes[NSFileCreationDate]; } return _creationDate; } - (unsigned long long)fileSize { if (_fileSize == 0) { _fileSize = [self.fileAttributes[NSFileSize] unsignedLongLongValue]; } return _fileSize; } - (NSTimeInterval)age { return -[[self creationDate] timeIntervalSinceNow]; } - (BOOL)isSymlink { return self.fileAttributes[NSFileType] == NSFileTypeSymbolicLink; } - (NSString *)description { return [@{ @"filePath": self.filePath ? : @"", @"fileName": self.fileName ? : @"", @"fileAttributes": self.fileAttributes ? : @"", @"creationDate": self.creationDate ? : @"", @"modificationDate": self.modificationDate ? : @"", @"fileSize": @(self.fileSize), @"age": @(self.age), @"isArchived": @(self.isArchived) } description]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Archiving //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)isArchived { return [self hasExtendedAttributeWithName:kDDXAttrArchivedName]; } - (void)setIsArchived:(BOOL)flag { if (flag) { [self addExtendedAttributeWithName:kDDXAttrArchivedName]; } else { [self removeExtendedAttributeWithName:kDDXAttrArchivedName]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Changes //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)reset { _fileName = nil; _fileAttributes = nil; _creationDate = nil; _modificationDate = nil; } - (void)renameFile:(NSString *)newFileName { // This method is only used on the iPhone simulator, where normal extended attributes are broken. // See full explanation in the header file. if (![newFileName isEqualToString:[self fileName]]) { NSFileManager* fileManager = [NSFileManager defaultManager]; NSString *fileDir = [filePath stringByDeletingLastPathComponent]; NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName]; // We only want to assert when we're not using the simulator, as we're "archiving" a log file with this method in the sim // (in which case the file might not exist anymore and neither does it parent folder). #if defined(DEBUG) && (!defined(TARGET_IPHONE_SIMULATOR) || !TARGET_IPHONE_SIMULATOR) BOOL directory = NO; [fileManager fileExistsAtPath:fileDir isDirectory:&directory]; NSAssert(directory, @"Containing directory must exist."); #endif __autoreleasing NSError *error = nil; BOOL success = [fileManager removeItemAtPath:newFilePath error:&error]; if (!success && error.code != NSFileNoSuchFileError) { NSLogError(@"DDLogFileInfo: Error deleting archive (%@): %@", self.fileName, error); } success = [fileManager moveItemAtPath:filePath toPath:newFilePath error:&error]; // When a log file is deleted, moved or renamed on the simulator, we attempt to rename it as a // result of "archiving" it, but since the file doesn't exist anymore, needless error logs are printed // We therefore ignore this error, and assert that the directory we are copying into exists (which // is the only other case where this error code can come up). #if TARGET_IPHONE_SIMULATOR if (!success && error.code != NSFileNoSuchFileError) #else if (!success) #endif { NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error); } filePath = newFilePath; [self reset]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Attribute Management //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if TARGET_IPHONE_SIMULATOR // Old implementation of extended attributes on the simulator. // Extended attributes were not working properly on the simulator // due to misuse of setxattr() function. // Now that this is fixed in the new implementation, we want to keep // backward compatibility with previous simulator installations. static NSString * const kDDExtensionSeparator = @"."; static NSString *_xattrToExtensionName(NSString *attrName) { static NSDictionary* _xattrToExtensionNameMap; static dispatch_once_t _token; dispatch_once(&_token, ^{ _xattrToExtensionNameMap = @{ kDDXAttrArchivedName: @"archived" }; }); return [_xattrToExtensionNameMap objectForKey:attrName]; } - (BOOL)_hasExtensionAttributeWithName:(NSString *)attrName { // This method is only used on the iPhone simulator for backward compatibility reason. // Split the file name into components. File name may have various format, but generally // structure is same: // // . and .archived. // or // and .archived // // So we want to search for the attrName in the components (ignoring the first array index). NSArray *components = [[self fileName] componentsSeparatedByString:kDDExtensionSeparator]; // Watch out for file names without an extension for (NSUInteger i = 1; i < components.count; i++) { NSString *attr = components[i]; if ([attrName isEqualToString:attr]) { return YES; } } return NO; } - (void)_removeExtensionAttributeWithName:(NSString *)attrName { // This method is only used on the iPhone simulator for backward compatibility reason. if ([attrName length] == 0) { return; } // Example: // attrName = "archived" // // "mylog.archived.txt" -> "mylog.txt" // "mylog.archived" -> "mylog" NSArray *components = [[self fileName] componentsSeparatedByString:kDDExtensionSeparator]; NSUInteger count = [components count]; NSUInteger estimatedNewLength = [[self fileName] length]; NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; if (count > 0) { [newFileName appendString:components[0]]; } BOOL found = NO; NSUInteger i; for (i = 1; i < count; i++) { NSString *attr = components[i]; if ([attrName isEqualToString:attr]) { found = YES; } else { [newFileName appendString:kDDExtensionSeparator]; [newFileName appendString:attr]; } } if (found) { [self renameFile:newFileName]; } } #endif /* if TARGET_IPHONE_SIMULATOR */ - (BOOL)hasExtendedAttributeWithName:(NSString *)attrName { const char *path = [filePath fileSystemRepresentation]; const char *name = [attrName UTF8String]; BOOL hasExtendedAttribute = NO; char buffer[1]; ssize_t result = getxattr(path, name, buffer, 1, 0, 0); // Fast path if (result > 0 && buffer[0] == '\1') { hasExtendedAttribute = YES; } // Maintain backward compatibility, but fix it for future checks else if (result >= 0) { hasExtendedAttribute = YES; [self addExtendedAttributeWithName:attrName]; } #if TARGET_IPHONE_SIMULATOR else if ([self _hasExtensionAttributeWithName:_xattrToExtensionName(attrName)]) { hasExtendedAttribute = YES; [self addExtendedAttributeWithName:attrName]; } #endif return hasExtendedAttribute; } - (void)addExtendedAttributeWithName:(NSString *)attrName { const char *path = [filePath fileSystemRepresentation]; const char *name = [attrName UTF8String]; int result = setxattr(path, name, "\1", 1, 0, 0); if (result < 0) { if (errno != ENOENT) { NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %s", attrName, filePath, strerror(errno)); } else { NSLogDebug(@"DDLogFileInfo: File does not exist in setxattr(%@, %@): error = %s", attrName, filePath, strerror(errno)); } } #if TARGET_IPHONE_SIMULATOR else { [self _removeExtensionAttributeWithName:_xattrToExtensionName(attrName)]; } #endif } - (void)removeExtendedAttributeWithName:(NSString *)attrName { const char *path = [filePath fileSystemRepresentation]; const char *name = [attrName UTF8String]; int result = removexattr(path, name, 0); if (result < 0 && errno != ENOATTR) { NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %s", attrName, self.fileName, strerror(errno)); } #if TARGET_IPHONE_SIMULATOR [self _removeExtensionAttributeWithName:_xattrToExtensionName(attrName)]; #endif } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Comparisons //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)isEqual:(id)object { if ([object isKindOfClass:[self class]]) { DDLogFileInfo *another = (DDLogFileInfo *)object; return [filePath isEqualToString:[another filePath]]; } return NO; } - (NSUInteger)hash { return [filePath hash]; } - (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another { __auto_type us = [self creationDate]; __auto_type them = [another creationDate]; return [them compare:us]; } - (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another { __auto_type us = [self modificationDate]; __auto_type them = [another modificationDate]; return [them compare:us]; } @end