// // P4FileManager.m // P4Menu // // Created by Michael Bishop on 7/29/11. // Copyright 2011 Numerical Garden LLC. All rights reserved. // #import "P4LocalFileManager.h" #import "P4Connection.h" #import "P4Response.h" #import "P4SpecManager.h" #import "P4Client.h" #import "P4FilePath.h" #import "P4ErrorCodes.h" #import "NSError+NGAAdditions.h" // TODO Create a the observer token and use it as the sole item in observing paths static const NSTimeInterval kRefreshRate = 5.0 * 60.0; // 5 minutes // P4FileManagers are retained by the application as a singleton static NSMutableDictionary * FileManagerInstances = nil; @interface P4FileObserverToken () @property (readwrite, nonatomic, retain) NSString * path; @property (readwrite, nonatomic, retain) FileUpdateBlock block; @property (readwrite, nonatomic, assign) P4LocalFileManager * fileManager; @end @interface P4FileManagerRecord : NSObject { @private NSMutableSet * _observationBlocks; NSDictionary * _fileInfo; NSTimeInterval _lastRefreshTime; BOOL _isRefreshing; } @property (readonly, nonatomic) NSSet * observers; @property (readwrite, nonatomic, copy) NSDictionary * fileInfo; @property (readwrite, nonatomic) NSTimeInterval lastRefreshTime; @property (readwrite, nonatomic) BOOL refreshing; -(void)addObservervationBlock:(FileUpdateBlock)observer; -(void)removeObservervationBlock:(FileUpdateBlock)observer; -(BOOL)flushData; -(BOOL)needsRefresh; @end @interface P4LocalFileManager () -(P4FileObserverToken*)addObservationBlock:(FileUpdateBlock)updateBlock forFilePath:(NSString*)filepath; -(void)removeObservationForToken:(P4FileObserverToken*)token; -(id)initWithP4Port:(NSString*)p username:(NSString*)u clientName:(NSString*)clientName; -(P4FileManagerRecord*)P4_recordForLocalPath:(NSString*)localPath; @property (readwrite, nonatomic, retain) NSError * lastError; @property (readwrite, nonatomic, copy) NSString * clientName; -(NSString*)P4_tempFilePathForPort:(NSString*)p4port filePath:(P4FilePath*)filePath; @end @implementation P4LocalFileManager @synthesize lastError = _lastError ,clientName = _clientName ; +(P4LocalFileManager*)managerWithPortString:(NSString*)p4port username:(NSString*)username clientName:(NSString*)clientName { if ( !clientName ) return nil; if ( !FileManagerInstances ) FileManagerInstances = [NSMutableDictionary new]; NSString * key = [NSString stringWithFormat:@"%@/%@/%@", p4port, username, clientName]; P4LocalFileManager * specManager = [FileManagerInstances objectForKey:key]; if ( !specManager ) { specManager = [[[P4LocalFileManager alloc] initWithP4Port:p4port username:username clientName:clientName] autorelease]; [FileManagerInstances setObject:specManager forKey:key]; } return specManager; } -(id)initWithP4Port:(NSString*)p username:(NSString*)u clientName:(NSString*)clientName { if ( !(self = [super init]) ) return nil; connection = [[P4Connection connectionWithPortString:p user:u] retain]; self.clientName = clientName; _fileInfoCache = [[NSMutableDictionary alloc] init]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp]; return self; } +(P4LocalFileManager*)managerWithClient:(P4Client*)client { if ( !client ) return nil; return [self managerWithPortString:client.connection.portString username:client.connection.username clientName:client.identifier]; } -(id)init { return [self initWithP4Port:nil username:nil clientName:nil]; } -(void)applicationWillTerminate:(NSNotification*)notification { [self purgeData:nil]; } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [connection release]; [_fileInfoCache release]; [_cachedTemporaryDirectory release]; self.clientName = nil; [super dealloc]; } -(P4Client*)client { if (!self.clientName) return nil; P4SpecManager * specManager = [P4SpecManager managerWithPortString:connection.portString username:connection.username]; return (P4Client*)[specManager specOfType:@"client" identifier:self.clientName createIfNotFound:YES]; } -(void)purgeDataForLocalPath:(NSString*)localPath { P4FileManagerRecord * fileRecord = [_fileInfoCache objectForKey:localPath]; if ( fileRecord ) [fileRecord flushData]; } -(BOOL)purgeData:(NSError**)error { if ( error ) *error = nil; for (NSString * recordKey in _fileInfoCache) [[_fileInfoCache objectForKey:recordKey] flushData]; return YES; } -(P4FileManagerRecord*)P4_recordForLocalPath:(NSString*)localPath { if ( !localPath ) return nil; P4FileManagerRecord * fileRecord = [_fileInfoCache objectForKey:localPath]; if ( !fileRecord ) { fileRecord = [[P4FileManagerRecord alloc] init]; [_fileInfoCache setObject:fileRecord forKey:localPath]; [fileRecord release]; } return fileRecord; } -(NSDictionary*)informationForLocalFilePath:(NSString*)localfilepath { P4FileManagerRecord * fileRecord = [self P4_recordForLocalPath:localfilepath]; if ( [fileRecord needsRefresh] ) { // LOG_DEBUG(@"Refreshing fstat info for %@", localfilepath); [self fetchInformationForLocalFilePath:localfilepath]; } // else // { // LOG_DEBUG(@"NO fstat NEEDED for %@", localfilepath); // } return fileRecord.fileInfo; } -(void)setFileInformation:(NSDictionary*)info forLocalPath:(NSString*)localPath { P4FileManagerRecord * record = [self P4_recordForLocalPath:localPath]; record.fileInfo = info; for (FileUpdateBlock block in record.observers) block( self, localPath, info ); record.lastRefreshTime = [[NSDate date] timeIntervalSinceReferenceDate]; } -(void)fetchInformationForLocalFilePath:(NSString*)localfilepath { if ( !localfilepath ) return; P4FileManagerRecord * record = [self P4_recordForLocalPath:localfilepath]; if ( record.refreshing ) return; record.refreshing = true; NSDictionary * context = [NSDictionary dictionaryWithObjectsAndKeys:localfilepath, kP4ConnectionCWDContextKey, self.clientName, kP4ConnectionClientContextKey, nil]; [connection runArguments:[NSArray arrayWithObjects:@"fstat", localfilepath, nil] withContext:context updateBlock:nil completionBlock:^(P4Response * response) { self.lastError = response.error; if (response.error == nil) { [self setFileInformation:response.result forLocalPath:localfilepath]; } else if ( response.error.code == kP4FileNotRecognized ) { // File not in Perforce [self setFileInformation:nil forLocalPath:localfilepath]; } else { LOG_DEBUG(@"Error received: %@", response.error); } record.refreshing = false; }]; } -(void)contentForFilePath:(P4FilePath*)filepath block:(void(^)(NSString*, NSError*))block { NSString * tempFile = [self P4_tempFilePathForPort:self.client.manager.portString filePath:filepath]; if ( !tempFile ) block( nil, [NSError errorWithDomain:P4MEErrorDomain code:kP4MEErrorCodeBadTempDir userInfo:nil] ); if ( [[NSFileManager defaultManager] fileExistsAtPath:tempFile] ) { block( tempFile, nil ); return; } NSArray * arguments = [NSArray arrayWithObjects:@"print", @"-o", tempFile, filepath.formattedString, nil]; [self.client runArguments:arguments withContext:nil completionBlock:^(P4Response *response) { if ( ![response isSuccess] ) { block( tempFile, response.error ); return; } // Turn off all write permissions NSDictionary * attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:tempFile error:nil]; NSUInteger posixPermissions = [attributes filePosixPermissions]; NSUInteger writePermissions = (S_IWUSR | S_IWGRP | S_IWOTH); posixPermissions &= ~writePermissions; // Set the new permissions to turn off the writes [[NSFileManager defaultManager] setAttributes:[NSDictionary dictionaryWithObject:[NSNumber numberWithUnsignedInteger:posixPermissions] forKey:NSFilePosixPermissions] ofItemAtPath:tempFile error:nil ]; block( tempFile, nil ); }]; } -(P4FileObserverToken*)addObservationBlock:(FileUpdateBlock)updateBlock forFilePath:(NSString*)filepath { if ( !filepath || !updateBlock ) return nil; P4FileManagerRecord * fileRecord = [self P4_recordForLocalPath:filepath]; FileUpdateBlock updateBlockCopy = [updateBlock copy]; [fileRecord addObservervationBlock:updateBlockCopy]; [updateBlockCopy release]; P4FileObserverToken * token = [[P4FileObserverToken new] autorelease]; token.path = filepath; token.block = updateBlockCopy; token.fileManager = self; return token; } -(void)removeObservationForToken:(P4FileObserverToken*)token { P4FileManagerRecord * fileRecord = [self P4_recordForLocalPath:token.path]; [fileRecord removeObservervationBlock:(FileUpdateBlock)token.block]; } -(NSString*)cachedTemporaryDirectory { if ( _cachedTemporaryDirectory ) return _cachedTemporaryDirectory; // From Cocoa With Love // http://cocoawithlove.com/2009/07/temporary-files-and-folders-in-cocoa.html NSString *tempDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"XXXX"]; const char *tempDirectoryTemplateCString = [tempDirectoryTemplate fileSystemRepresentation]; char *tempDirectoryNameCString = strdup(tempDirectoryTemplateCString); char *result = mkdtemp(tempDirectoryNameCString); if (!result) { free(tempDirectoryNameCString); tempDirectoryNameCString = NULL; NSError * underlyingError = [NSError errorWithErrno]; // handle directory creation failure NSString * localizedMessage = NSLocalizedString( @"Could not create a temporary directory at %@", @"Could not create temp directory message" ); localizedMessage = [NSString stringWithFormat:localizedMessage, tempDirectoryTemplate]; NSError * error = [NSError errorWithDomain:P4MEErrorDomain code:kP4MEErrorCodeBadTempDir userInfo:[NSDictionary dictionaryWithObjectsAndKeys:localizedMessage, NSLocalizedDescriptionKey , NSUnderlyingErrorKey, underlyingError , nil]]; [NSApp presentError:error]; return nil; } _cachedTemporaryDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:result length:strlen(result)] retain]; free(tempDirectoryNameCString); tempDirectoryNameCString = NULL; return _cachedTemporaryDirectory; } -(NSString*)P4_tempFilePathForPort:(NSString*)p4port filePath:(P4FilePath*)filePath; { // The temporary file has to preserve the path extension of the original // or it cannot be opened in the editor. // The following code.. // turns "path.jpg#1" // into "path#1.jpg" NSString * basePath = filePath.formattedPath; NSString * directory = [basePath stringByDeletingLastPathComponent]; NSString * fileName = [basePath lastPathComponent]; NSRange extensionRange = [fileName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]; if ( extensionRange.location == NSNotFound ) { // Make the range point to an empty string past the end so our // extension string will be empty extensionRange.location = [fileName length]; } extensionRange.length = [fileName length] - extensionRange.location; NSString * extensionString = [fileName substringWithRange:extensionRange]; extensionRange.length = extensionRange.location; extensionRange.location = 0; fileName = [fileName substringWithRange:extensionRange]; fileName = [fileName stringByAppendingString:[NSString stringWithFormat:@"#%@", filePath.revision]]; fileName = [fileName stringByAppendingString:extensionString]; NSString * uniquePortString = [[self cachedTemporaryDirectory] stringByAppendingPathComponent:p4port]; NSString * uniqueFullString = [uniquePortString stringByAppendingPathComponent:directory]; uniqueFullString = [uniqueFullString stringByAppendingPathComponent:fileName]; return uniqueFullString; } @end @implementation P4FileManagerRecord @synthesize lastRefreshTime = _lastRefreshTime ,fileInfo = _fileInfo ,refreshing = _isRefreshing; -(id)init { if ( !(self = [super init]) ) return nil; _observationBlocks = [[NSMutableSet alloc] init]; return self; } -(void)dealloc { [_observationBlocks release]; [super dealloc]; } -(void)addObservervationBlock:(FileUpdateBlock)observer { [_observationBlocks addObject:observer]; } -(void)removeObservervationBlock:(FileUpdateBlock)observer { [_observationBlocks removeObject:observer]; } -(BOOL)flushData { _lastRefreshTime = 0; if ( [_observationBlocks count] ) return NO; [_fileInfo release]; _fileInfo = nil; return YES; } -(BOOL)needsRefresh { return ( [[NSDate date] timeIntervalSinceReferenceDate] - _lastRefreshTime > kRefreshRate ); } -(NSSet*)observers { return [NSSet setWithSet:_observationBlocks]; } @end @implementation P4FileObserverToken @synthesize path, block, fileManager; +(id)tokenRegisteringLocalFilePath:(NSString*)path withFileManager:(P4LocalFileManager*)fileManager observationBlock:(FileUpdateBlock)block { return [fileManager addObservationBlock:block forFilePath:path]; } -(void)unregister { [self.fileManager removeObservationForToken:self]; } -(void)dealloc { [self unregister]; self.fileManager = nil; self.path = nil; self.block = nil; [super dealloc]; } @end