123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- //
- // ASIDownloadCache.m
- // Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest
- //
- // Created by Ben Copsey on 01/05/2010.
- // Copyright 2010 All-Seeing Interactive. All rights reserved.
- //
- #import "ASIDownloadCache.h"
- #import "ASIHTTPRequest.h"
- #import <CommonCrypto/CommonHMAC.h>
- static ASIDownloadCache *sharedCache = nil;
- static NSString *sessionCacheFolder = @"SessionStore";
- static NSString *permanentCacheFolder = @"PermanentStore";
- static NSArray *fileExtensionsToHandleAsHTML = nil;
- @interface ASIDownloadCache ()
- + (NSString *)keyForURL:(NSURL *)url;
- - (NSString *)pathToFile:(NSString *)file;
- @end
- @implementation ASIDownloadCache
- + (void)initialize
- {
- if (self == [ASIDownloadCache class]) {
- // Obviously this is not an exhaustive list, but hopefully these are the most commonly used and this will 'just work' for the widest range of people
- // I imagine many web developers probably use url rewriting anyway
- fileExtensionsToHandleAsHTML = [[NSArray alloc] initWithObjects:@"asp",@"aspx",@"jsp",@"php",@"rb",@"py",@"pl",@"cgi", nil];
- }
- }
- - (id)init
- {
- self = [super init];
- [self setShouldRespectCacheControlHeaders:YES];
- [self setDefaultCachePolicy:ASIUseDefaultCachePolicy];
- [self setAccessLock:[[[NSRecursiveLock alloc] init] autorelease]];
- return self;
- }
- + (id)sharedCache
- {
- if (!sharedCache) {
- @synchronized(self) {
- if (!sharedCache) {
- sharedCache = [[self alloc] init];
- [sharedCache setStoragePath:[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"ASIHTTPRequestCache"]];
- }
- }
- }
- return sharedCache;
- }
- - (void)dealloc
- {
- [storagePath release];
- [accessLock release];
- [super dealloc];
- }
- - (NSString *)storagePath
- {
- [[self accessLock] lock];
- NSString *p = [[storagePath retain] autorelease];
- [[self accessLock] unlock];
- return p;
- }
- - (void)setStoragePath:(NSString *)path
- {
- [[self accessLock] lock];
- [self clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy];
- [storagePath release];
- storagePath = [path retain];
- NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
- BOOL isDirectory = NO;
- NSArray *directories = [NSArray arrayWithObjects:path,[path stringByAppendingPathComponent:sessionCacheFolder],[path stringByAppendingPathComponent:permanentCacheFolder],nil];
- for (NSString *directory in directories) {
- BOOL exists = [fileManager fileExistsAtPath:directory isDirectory:&isDirectory];
- if (exists && !isDirectory) {
- [[self accessLock] unlock];
- [NSException raise:@"FileExistsAtCachePath" format:@"Cannot create a directory for the cache at '%@', because a file already exists",directory];
- } else if (!exists) {
- [fileManager createDirectoryAtPath:directory withIntermediateDirectories:NO attributes:nil error:nil];
- if (![fileManager fileExistsAtPath:directory]) {
- [[self accessLock] unlock];
- [NSException raise:@"FailedToCreateCacheDirectory" format:@"Failed to create a directory for the cache at '%@'",directory];
- }
- }
- }
- [self clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy];
- [[self accessLock] unlock];
- }
- - (void)updateExpiryForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge
- {
- NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request];
- NSMutableDictionary *cachedHeaders = [NSMutableDictionary dictionaryWithContentsOfFile:headerPath];
- if (!cachedHeaders) {
- return;
- }
- NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge];
- if (!expires) {
- return;
- }
- [cachedHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"];
- [cachedHeaders writeToFile:headerPath atomically:NO];
- }
- - (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge
- {
- return [ASIHTTPRequest expiryDateForRequest:request maxAge:maxAge];
- }
- - (void)storeResponseForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge
- {
- [[self accessLock] lock];
- if ([request error] || ![request responseHeaders] || ([request cachePolicy] & ASIDoNotWriteToCacheCachePolicy)) {
- [[self accessLock] unlock];
- return;
- }
- // We only cache 200/OK or redirect reponses (redirect responses are cached so the cache works better with no internet connection)
- int responseCode = [request responseStatusCode];
- if (responseCode != 200 && responseCode != 301 && responseCode != 302 && responseCode != 303 && responseCode != 307) {
- [[self accessLock] unlock];
- return;
- }
- if ([self shouldRespectCacheControlHeaders] && ![[self class] serverAllowsResponseCachingForRequest:request]) {
- [[self accessLock] unlock];
- return;
- }
- NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request];
- NSString *dataPath = [self pathToStoreCachedResponseDataForRequest:request];
- NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]];
- if ([request isResponseCompressed]) {
- [responseHeaders removeObjectForKey:@"Content-Encoding"];
- }
- // Create a special 'X-ASIHTTPRequest-Expires' header
- // This is what we use for deciding if cached data is current, rather than parsing the expires / max-age headers individually each time
- // We store this as a timestamp to make reading it easier as NSDateFormatter is quite expensive
- NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge];
- if (expires) {
- [responseHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"];
- }
- // Store the response code in a custom header so we can reuse it later
- // We'll change 304/Not Modified to 200/OK because this is likely to be us updating the cached headers with a conditional GET
- int statusCode = [request responseStatusCode];
- if (statusCode == 304) {
- statusCode = 200;
- }
- [responseHeaders setObject:[NSNumber numberWithInt:statusCode] forKey:@"X-ASIHTTPRequest-Response-Status-Code"];
- [responseHeaders writeToFile:headerPath atomically:NO];
- if ([request responseData]) {
- [[request responseData] writeToFile:dataPath atomically:NO];
- } else if ([request downloadDestinationPath] && ![[request downloadDestinationPath] isEqualToString:dataPath]) {
- NSError *error = nil;
- NSFileManager* manager = [[NSFileManager alloc] init];
- if ([manager fileExistsAtPath:dataPath]) {
- [manager removeItemAtPath:dataPath error:&error];
- }
- [manager copyItemAtPath:[request downloadDestinationPath] toPath:dataPath error:&error];
- [manager release];
- }
- [[self accessLock] unlock];
- }
- - (NSDictionary *)cachedResponseHeadersForURL:(NSURL *)url
- {
- NSString *path = [self pathToCachedResponseHeadersForURL:url];
- if (path) {
- return [NSDictionary dictionaryWithContentsOfFile:path];
- }
- return nil;
- }
- - (NSData *)cachedResponseDataForURL:(NSURL *)url
- {
- NSString *path = [self pathToCachedResponseDataForURL:url];
- if (path) {
- return [NSData dataWithContentsOfFile:path];
- }
- return nil;
- }
- - (NSString *)pathToCachedResponseDataForURL:(NSURL *)url
- {
- // Grab the file extension, if there is one. We do this so we can save the cached response with the same file extension - this is important if you want to display locally cached data in a web view
- NSString *extension = [[url path] pathExtension];
- // If the url doesn't have an extension, we'll add one so a webview can read it when locally cached
- // If the url has the extension of a common web scripting language, we'll change the extension on the cached path to html for the same reason
- if (![extension length] || [[[self class] fileExtensionsToHandleAsHTML] containsObject:[extension lowercaseString]]) {
- extension = @"html";
- }
- return [self pathToFile:[[[self class] keyForURL:url] stringByAppendingPathExtension:extension]];
- }
- + (NSArray *)fileExtensionsToHandleAsHTML
- {
- return fileExtensionsToHandleAsHTML;
- }
- - (NSString *)pathToCachedResponseHeadersForURL:(NSURL *)url
- {
- return [self pathToFile:[[[self class] keyForURL:url] stringByAppendingPathExtension:@"cachedheaders"]];
- }
- - (NSString *)pathToFile:(NSString *)file
- {
- [[self accessLock] lock];
- if (![self storagePath]) {
- [[self accessLock] unlock];
- return nil;
- }
- NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
- // Look in the session store
- NSString *dataPath = [[[self storagePath] stringByAppendingPathComponent:sessionCacheFolder] stringByAppendingPathComponent:file];
- if ([fileManager fileExistsAtPath:dataPath]) {
- [[self accessLock] unlock];
- return dataPath;
- }
- // Look in the permanent store
- dataPath = [[[self storagePath] stringByAppendingPathComponent:permanentCacheFolder] stringByAppendingPathComponent:file];
- if ([fileManager fileExistsAtPath:dataPath]) {
- [[self accessLock] unlock];
- return dataPath;
- }
- [[self accessLock] unlock];
- return nil;
- }
- - (NSString *)pathToStoreCachedResponseDataForRequest:(ASIHTTPRequest *)request
- {
- [[self accessLock] lock];
- if (![self storagePath]) {
- [[self accessLock] unlock];
- return nil;
- }
- NSString *path = [[self storagePath] stringByAppendingPathComponent:([request cacheStoragePolicy] == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
- // Grab the file extension, if there is one. We do this so we can save the cached response with the same file extension - this is important if you want to display locally cached data in a web view
- NSString *extension = [[[request url] path] pathExtension];
- // If the url doesn't have an extension, we'll add one so a webview can read it when locally cached
- // If the url has the extension of a common web scripting language, we'll change the extension on the cached path to html for the same reason
- if (![extension length] || [[[self class] fileExtensionsToHandleAsHTML] containsObject:[extension lowercaseString]]) {
- extension = @"html";
- }
- path = [path stringByAppendingPathComponent:[[[self class] keyForURL:[request url]] stringByAppendingPathExtension:extension]];
- [[self accessLock] unlock];
- return path;
- }
- - (NSString *)pathToStoreCachedResponseHeadersForRequest:(ASIHTTPRequest *)request
- {
- [[self accessLock] lock];
- if (![self storagePath]) {
- [[self accessLock] unlock];
- return nil;
- }
- NSString *path = [[self storagePath] stringByAppendingPathComponent:([request cacheStoragePolicy] == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
- path = [path stringByAppendingPathComponent:[[[self class] keyForURL:[request url]] stringByAppendingPathExtension:@"cachedheaders"]];
- [[self accessLock] unlock];
- return path;
- }
- - (void)removeCachedDataForURL:(NSURL *)url
- {
- [[self accessLock] lock];
- if (![self storagePath]) {
- [[self accessLock] unlock];
- return;
- }
- NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
- NSString *path = [self pathToCachedResponseHeadersForURL:url];
- if (path) {
- [fileManager removeItemAtPath:path error:NULL];
- }
- path = [self pathToCachedResponseDataForURL:url];
- if (path) {
- [fileManager removeItemAtPath:path error:NULL];
- }
- [[self accessLock] unlock];
- }
- - (void)removeCachedDataForRequest:(ASIHTTPRequest *)request
- {
- [self removeCachedDataForURL:[request url]];
- }
- - (BOOL)isCachedDataCurrentForRequest:(ASIHTTPRequest *)request
- {
- [[self accessLock] lock];
- if (![self storagePath]) {
- [[self accessLock] unlock];
- return NO;
- }
- NSDictionary *cachedHeaders = [self cachedResponseHeadersForURL:[request url]];
- if (!cachedHeaders) {
- [[self accessLock] unlock];
- return NO;
- }
- NSString *dataPath = [self pathToCachedResponseDataForURL:[request url]];
- if (!dataPath) {
- [[self accessLock] unlock];
- return NO;
- }
- // New content is not different
- if ([request responseStatusCode] == 304) {
- [[self accessLock] unlock];
- return YES;
- }
- // If we already have response headers for this request, check to see if the new content is different
- // We check [request complete] so that we don't end up comparing response headers from a redirection with these
- if ([request responseHeaders] && [request complete]) {
- // If the Etag or Last-Modified date are different from the one we have, we'll have to fetch this resource again
- NSArray *headersToCompare = [NSArray arrayWithObjects:@"Etag",@"Last-Modified",nil];
- for (NSString *header in headersToCompare) {
- if (![[[request responseHeaders] objectForKey:header] isEqualToString:[cachedHeaders objectForKey:header]]) {
- [[self accessLock] unlock];
- return NO;
- }
- }
- }
- if ([self shouldRespectCacheControlHeaders]) {
- // Look for X-ASIHTTPRequest-Expires header to see if the content is out of date
- NSNumber *expires = [cachedHeaders objectForKey:@"X-ASIHTTPRequest-Expires"];
- if (expires) {
- if ([[NSDate dateWithTimeIntervalSince1970:[expires doubleValue]] timeIntervalSinceNow] >= 0) {
- [[self accessLock] unlock];
- return YES;
- }
- }
- // No explicit expiration time sent by the server
- [[self accessLock] unlock];
- return NO;
- }
-
- [[self accessLock] unlock];
- return YES;
- }
- - (ASICachePolicy)defaultCachePolicy
- {
- [[self accessLock] lock];
- ASICachePolicy cp = defaultCachePolicy;
- [[self accessLock] unlock];
- return cp;
- }
- - (void)setDefaultCachePolicy:(ASICachePolicy)cachePolicy
- {
- [[self accessLock] lock];
- if (!cachePolicy) {
- defaultCachePolicy = ASIAskServerIfModifiedWhenStaleCachePolicy;
- } else {
- defaultCachePolicy = cachePolicy;
- }
- [[self accessLock] unlock];
- }
- - (void)clearCachedResponsesForStoragePolicy:(ASICacheStoragePolicy)storagePolicy
- {
- [[self accessLock] lock];
- if (![self storagePath]) {
- [[self accessLock] unlock];
- return;
- }
- NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
- NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
- BOOL isDirectory = NO;
- BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
- if (!exists || !isDirectory) {
- [[self accessLock] unlock];
- return;
- }
- NSError *error = nil;
- NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:path error:&error];
- if (error) {
- [[self accessLock] unlock];
- [NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",path];
- }
- for (NSString *file in cacheFiles) {
- [fileManager removeItemAtPath:[path stringByAppendingPathComponent:file] error:&error];
- if (error) {
- [[self accessLock] unlock];
- [NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",path];
- }
- }
- [[self accessLock] unlock];
- }
- + (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request
- {
- NSString *cacheControl = [[[request responseHeaders] objectForKey:@"Cache-Control"] lowercaseString];
- if (cacheControl) {
- if ([cacheControl isEqualToString:@"no-cache"] || [cacheControl isEqualToString:@"no-store"]) {
- return NO;
- }
- }
- NSString *pragma = [[[request responseHeaders] objectForKey:@"Pragma"] lowercaseString];
- if (pragma) {
- if ([pragma isEqualToString:@"no-cache"]) {
- return NO;
- }
- }
- return YES;
- }
- + (NSString *)keyForURL:(NSURL *)url
- {
- NSString *urlString = [url absoluteString];
- if ([urlString length] == 0) {
- return nil;
- }
- // Strip trailing slashes so http://allseeing-i.com/ASIHTTPRequest/ is cached the same as http://allseeing-i.com/ASIHTTPRequest
- if ([[urlString substringFromIndex:[urlString length]-1] isEqualToString:@"/"]) {
- urlString = [urlString substringToIndex:[urlString length]-1];
- }
- // Borrowed from: http://stackoverflow.com/questions/652300/using-md5-hash-on-a-string-in-cocoa
- const char *cStr = [urlString UTF8String];
- unsigned char result[16];
- CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
- return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]];
- }
- - (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request
- {
- // Ensure the request is allowed to read from the cache
- if ([request cachePolicy] & ASIDoNotReadFromCacheCachePolicy) {
- return NO;
- // If we don't want to load the request whatever happens, always pretend we have cached data even if we don't
- } else if ([request cachePolicy] & ASIDontLoadCachePolicy) {
- return YES;
- }
- NSDictionary *headers = [self cachedResponseHeadersForURL:[request url]];
- if (!headers) {
- return NO;
- }
- NSString *dataPath = [self pathToCachedResponseDataForURL:[request url]];
- if (!dataPath) {
- return NO;
- }
- // If we get here, we have cached data
- // If we have cached data, we can use it
- if ([request cachePolicy] & ASIOnlyLoadIfNotCachedCachePolicy) {
- return YES;
- // If we want to fallback to the cache after an error
- } else if ([request complete] && [request cachePolicy] & ASIFallbackToCacheIfLoadFailsCachePolicy) {
- return YES;
- // If we have cached data that is current, we can use it
- } else if ([request cachePolicy] & ASIAskServerIfModifiedWhenStaleCachePolicy) {
- if ([self isCachedDataCurrentForRequest:request]) {
- return YES;
- }
- // If we've got headers from a conditional GET and the cached data is still current, we can use it
- } else if ([request cachePolicy] & ASIAskServerIfModifiedCachePolicy) {
- if (![request responseHeaders]) {
- return NO;
- } else if ([self isCachedDataCurrentForRequest:request]) {
- return YES;
- }
- }
- return NO;
- }
- @synthesize storagePath;
- @synthesize defaultCachePolicy;
- @synthesize accessLock;
- @synthesize shouldRespectCacheControlHeaders;
- @end
|