root / asi-http-request-with-pithos / Classes / ASIDownloadCache.m @ be116d22
History | View | Annotate | Download (16.8 kB)
1 |
// |
---|---|
2 |
// ASIDownloadCache.m |
3 |
// Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest |
4 |
// |
5 |
// Created by Ben Copsey on 01/05/2010. |
6 |
// Copyright 2010 All-Seeing Interactive. All rights reserved. |
7 |
// |
8 |
|
9 |
#import "ASIDownloadCache.h" |
10 |
#import "ASIHTTPRequest.h" |
11 |
#import <CommonCrypto/CommonHMAC.h> |
12 |
|
13 |
static ASIDownloadCache *sharedCache = nil; |
14 |
|
15 |
static NSString *sessionCacheFolder = @"SessionStore"; |
16 |
static NSString *permanentCacheFolder = @"PermanentStore"; |
17 |
|
18 |
@interface ASIDownloadCache () |
19 |
+ (NSString *)keyForURL:(NSURL *)url; |
20 |
- (NSString *)pathToFile:(NSString *)file; |
21 |
@end |
22 |
|
23 |
@implementation ASIDownloadCache |
24 |
|
25 |
- (id)init |
26 |
{ |
27 |
self = [super init]; |
28 |
[self setShouldRespectCacheControlHeaders:YES]; |
29 |
[self setDefaultCachePolicy:ASIUseDefaultCachePolicy]; |
30 |
[self setAccessLock:[[[NSRecursiveLock alloc] init] autorelease]]; |
31 |
return self; |
32 |
} |
33 |
|
34 |
+ (id)sharedCache |
35 |
{ |
36 |
if (!sharedCache) { |
37 |
@synchronized(self) { |
38 |
if (!sharedCache) { |
39 |
sharedCache = [[self alloc] init]; |
40 |
[sharedCache setStoragePath:[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"ASIHTTPRequestCache"]]; |
41 |
} |
42 |
} |
43 |
} |
44 |
return sharedCache; |
45 |
} |
46 |
|
47 |
- (void)dealloc |
48 |
{ |
49 |
[storagePath release]; |
50 |
[accessLock release]; |
51 |
[super dealloc]; |
52 |
} |
53 |
|
54 |
- (NSString *)storagePath |
55 |
{ |
56 |
[[self accessLock] lock]; |
57 |
NSString *p = [[storagePath retain] autorelease]; |
58 |
[[self accessLock] unlock]; |
59 |
return p; |
60 |
} |
61 |
|
62 |
|
63 |
- (void)setStoragePath:(NSString *)path |
64 |
{ |
65 |
[[self accessLock] lock]; |
66 |
[self clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy]; |
67 |
[storagePath release]; |
68 |
storagePath = [path retain]; |
69 |
|
70 |
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; |
71 |
|
72 |
BOOL isDirectory = NO; |
73 |
NSArray *directories = [NSArray arrayWithObjects:path,[path stringByAppendingPathComponent:sessionCacheFolder],[path stringByAppendingPathComponent:permanentCacheFolder],nil]; |
74 |
for (NSString *directory in directories) { |
75 |
BOOL exists = [fileManager fileExistsAtPath:directory isDirectory:&isDirectory]; |
76 |
if (exists && !isDirectory) { |
77 |
[[self accessLock] unlock]; |
78 |
[NSException raise:@"FileExistsAtCachePath" format:@"Cannot create a directory for the cache at '%@', because a file already exists",directory]; |
79 |
} else if (!exists) { |
80 |
[fileManager createDirectoryAtPath:directory withIntermediateDirectories:NO attributes:nil error:nil]; |
81 |
if (![fileManager fileExistsAtPath:directory]) { |
82 |
[[self accessLock] unlock]; |
83 |
[NSException raise:@"FailedToCreateCacheDirectory" format:@"Failed to create a directory for the cache at '%@'",directory]; |
84 |
} |
85 |
} |
86 |
} |
87 |
[self clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy]; |
88 |
[[self accessLock] unlock]; |
89 |
} |
90 |
|
91 |
- (void)updateExpiryForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge |
92 |
{ |
93 |
NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request]; |
94 |
NSMutableDictionary *cachedHeaders = [NSMutableDictionary dictionaryWithContentsOfFile:headerPath]; |
95 |
if (!cachedHeaders) { |
96 |
return; |
97 |
} |
98 |
NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge]; |
99 |
if (!expires) { |
100 |
return; |
101 |
} |
102 |
[cachedHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"]; |
103 |
[cachedHeaders writeToFile:headerPath atomically:NO]; |
104 |
} |
105 |
|
106 |
- (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge |
107 |
{ |
108 |
NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]]; |
109 |
|
110 |
// If we weren't given a custom max-age, lets look for one in the response headers |
111 |
if (!maxAge) { |
112 |
NSString *cacheControl = [[responseHeaders objectForKey:@"Cache-Control"] lowercaseString]; |
113 |
if (cacheControl) { |
114 |
NSScanner *scanner = [NSScanner scannerWithString:cacheControl]; |
115 |
[scanner scanUpToString:@"max-age" intoString:NULL]; |
116 |
if ([scanner scanString:@"max-age" intoString:NULL]) { |
117 |
[scanner scanString:@"=" intoString:NULL]; |
118 |
[scanner scanDouble:&maxAge]; |
119 |
} |
120 |
} |
121 |
} |
122 |
|
123 |
// RFC 2612 says max-age must override any Expires header |
124 |
if (maxAge) { |
125 |
return [[NSDate date] addTimeInterval:maxAge]; |
126 |
} else { |
127 |
NSString *expires = [responseHeaders objectForKey:@"Expires"]; |
128 |
if (expires) { |
129 |
return [ASIHTTPRequest dateFromRFC1123String:expires]; |
130 |
} |
131 |
} |
132 |
return nil; |
133 |
} |
134 |
|
135 |
- (void)storeResponseForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge |
136 |
{ |
137 |
[[self accessLock] lock]; |
138 |
|
139 |
if ([request error] || ![request responseHeaders] || ([request cachePolicy] & ASIDoNotWriteToCacheCachePolicy)) { |
140 |
[[self accessLock] unlock]; |
141 |
return; |
142 |
} |
143 |
|
144 |
// We only cache 200/OK or redirect reponses (redirect responses are cached so the cache works better with no internet connection) |
145 |
int responseCode = [request responseStatusCode]; |
146 |
if (responseCode != 200 && responseCode != 301 && responseCode != 302 && responseCode != 303 && responseCode != 307) { |
147 |
[[self accessLock] unlock]; |
148 |
return; |
149 |
} |
150 |
|
151 |
if ([self shouldRespectCacheControlHeaders] && ![[self class] serverAllowsResponseCachingForRequest:request]) { |
152 |
[[self accessLock] unlock]; |
153 |
return; |
154 |
} |
155 |
|
156 |
NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request]; |
157 |
NSString *dataPath = [self pathToStoreCachedResponseDataForRequest:request]; |
158 |
|
159 |
NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]]; |
160 |
if ([request isResponseCompressed]) { |
161 |
[responseHeaders removeObjectForKey:@"Content-Encoding"]; |
162 |
} |
163 |
|
164 |
// Create a special 'X-ASIHTTPRequest-Expires' header |
165 |
// This is what we use for deciding if cached data is current, rather than parsing the expires / max-age headers individually each time |
166 |
// We store this as a timestamp to make reading it easier as NSDateFormatter is quite expensive |
167 |
|
168 |
NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge]; |
169 |
if (expires) { |
170 |
[responseHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"]; |
171 |
} |
172 |
|
173 |
// Store the response code in a custom header so we can reuse it later |
174 |
|
175 |
// We'll change 304/Not Modified to 200/OK because this is likely to be us updating the cached headers with a conditional GET |
176 |
int statusCode = [request responseStatusCode]; |
177 |
if (statusCode == 304) { |
178 |
statusCode = 200; |
179 |
} |
180 |
[responseHeaders setObject:[NSNumber numberWithInt:statusCode] forKey:@"X-ASIHTTPRequest-Response-Status-Code"]; |
181 |
|
182 |
[responseHeaders writeToFile:headerPath atomically:NO]; |
183 |
|
184 |
if ([request responseData]) { |
185 |
[[request responseData] writeToFile:dataPath atomically:NO]; |
186 |
} else if ([request downloadDestinationPath] && ![[request downloadDestinationPath] isEqualToString:dataPath]) { |
187 |
NSError *error = nil; |
188 |
[[[[NSFileManager alloc] init] autorelease] copyItemAtPath:[request downloadDestinationPath] toPath:dataPath error:&error]; |
189 |
} |
190 |
[[self accessLock] unlock]; |
191 |
} |
192 |
|
193 |
- (NSDictionary *)cachedResponseHeadersForURL:(NSURL *)url |
194 |
{ |
195 |
NSString *path = [self pathToCachedResponseHeadersForURL:url]; |
196 |
if (path) { |
197 |
return [NSDictionary dictionaryWithContentsOfFile:path]; |
198 |
} |
199 |
return nil; |
200 |
} |
201 |
|
202 |
- (NSData *)cachedResponseDataForURL:(NSURL *)url |
203 |
{ |
204 |
NSString *path = [self pathToCachedResponseDataForURL:url]; |
205 |
if (path) { |
206 |
return [NSData dataWithContentsOfFile:path]; |
207 |
} |
208 |
return nil; |
209 |
} |
210 |
|
211 |
- (NSString *)pathToCachedResponseDataForURL:(NSURL *)url |
212 |
{ |
213 |
// 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 |
214 |
NSString *extension = [[url path] pathExtension]; |
215 |
if (![extension length]) { |
216 |
extension = @"html"; |
217 |
} |
218 |
return [self pathToFile:[[[self class] keyForURL:url] stringByAppendingPathExtension:extension]]; |
219 |
} |
220 |
|
221 |
- (NSString *)pathToCachedResponseHeadersForURL:(NSURL *)url |
222 |
{ |
223 |
return [self pathToFile:[[[self class] keyForURL:url] stringByAppendingPathExtension:@"cachedheaders"]]; |
224 |
} |
225 |
|
226 |
- (NSString *)pathToFile:(NSString *)file |
227 |
{ |
228 |
[[self accessLock] lock]; |
229 |
if (![self storagePath]) { |
230 |
[[self accessLock] unlock]; |
231 |
return nil; |
232 |
} |
233 |
|
234 |
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; |
235 |
|
236 |
// Look in the session store |
237 |
NSString *dataPath = [[[self storagePath] stringByAppendingPathComponent:sessionCacheFolder] stringByAppendingPathComponent:file]; |
238 |
if ([fileManager fileExistsAtPath:dataPath]) { |
239 |
[[self accessLock] unlock]; |
240 |
return dataPath; |
241 |
} |
242 |
// Look in the permanent store |
243 |
dataPath = [[[self storagePath] stringByAppendingPathComponent:permanentCacheFolder] stringByAppendingPathComponent:file]; |
244 |
if ([fileManager fileExistsAtPath:dataPath]) { |
245 |
[[self accessLock] unlock]; |
246 |
return dataPath; |
247 |
} |
248 |
[[self accessLock] unlock]; |
249 |
return nil; |
250 |
} |
251 |
|
252 |
|
253 |
- (NSString *)pathToStoreCachedResponseDataForRequest:(ASIHTTPRequest *)request |
254 |
{ |
255 |
[[self accessLock] lock]; |
256 |
if (![self storagePath]) { |
257 |
[[self accessLock] unlock]; |
258 |
return nil; |
259 |
} |
260 |
|
261 |
NSString *path = [[self storagePath] stringByAppendingPathComponent:([request cacheStoragePolicy] == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)]; |
262 |
|
263 |
// 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 |
264 |
NSString *extension = [[[request url] path] pathExtension]; |
265 |
if (![extension length]) { |
266 |
extension = @"html"; |
267 |
} |
268 |
path = [path stringByAppendingPathComponent:[[[self class] keyForURL:[request url]] stringByAppendingPathExtension:extension]]; |
269 |
[[self accessLock] unlock]; |
270 |
return path; |
271 |
} |
272 |
|
273 |
- (NSString *)pathToStoreCachedResponseHeadersForRequest:(ASIHTTPRequest *)request |
274 |
{ |
275 |
[[self accessLock] lock]; |
276 |
if (![self storagePath]) { |
277 |
[[self accessLock] unlock]; |
278 |
return nil; |
279 |
} |
280 |
NSString *path = [[self storagePath] stringByAppendingPathComponent:([request cacheStoragePolicy] == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)]; |
281 |
path = [path stringByAppendingPathComponent:[[[self class] keyForURL:[request url]] stringByAppendingPathExtension:@"cachedheaders"]]; |
282 |
[[self accessLock] unlock]; |
283 |
return path; |
284 |
} |
285 |
|
286 |
- (void)removeCachedDataForURL:(NSURL *)url |
287 |
{ |
288 |
[[self accessLock] lock]; |
289 |
if (![self storagePath]) { |
290 |
[[self accessLock] unlock]; |
291 |
return; |
292 |
} |
293 |
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; |
294 |
|
295 |
NSString *path = [self pathToCachedResponseHeadersForURL:url]; |
296 |
if (path) { |
297 |
[fileManager removeItemAtPath:path error:NULL]; |
298 |
} |
299 |
|
300 |
path = [self pathToCachedResponseDataForURL:url]; |
301 |
if (path) { |
302 |
[fileManager removeItemAtPath:path error:NULL]; |
303 |
} |
304 |
[[self accessLock] unlock]; |
305 |
} |
306 |
|
307 |
- (void)removeCachedDataForRequest:(ASIHTTPRequest *)request |
308 |
{ |
309 |
[self removeCachedDataForURL:[request url]]; |
310 |
} |
311 |
|
312 |
- (BOOL)isCachedDataCurrentForRequest:(ASIHTTPRequest *)request |
313 |
{ |
314 |
[[self accessLock] lock]; |
315 |
if (![self storagePath]) { |
316 |
[[self accessLock] unlock]; |
317 |
return NO; |
318 |
} |
319 |
NSDictionary *cachedHeaders = [self cachedResponseHeadersForURL:[request url]]; |
320 |
if (!cachedHeaders) { |
321 |
[[self accessLock] unlock]; |
322 |
return NO; |
323 |
} |
324 |
NSString *dataPath = [self pathToCachedResponseDataForURL:[request url]]; |
325 |
if (!dataPath) { |
326 |
[[self accessLock] unlock]; |
327 |
return NO; |
328 |
} |
329 |
|
330 |
// New content is not different |
331 |
if ([request responseStatusCode] == 304) { |
332 |
[[self accessLock] unlock]; |
333 |
return YES; |
334 |
} |
335 |
|
336 |
// If we already have response headers for this request, check to see if the new content is different |
337 |
// We check [request complete] so that we don't end up comparing response headers from a redirection with these |
338 |
if ([request responseHeaders] && [request complete]) { |
339 |
|
340 |
// If the Etag or Last-Modified date are different from the one we have, we'll have to fetch this resource again |
341 |
NSArray *headersToCompare = [NSArray arrayWithObjects:@"Etag",@"Last-Modified",nil]; |
342 |
for (NSString *header in headersToCompare) { |
343 |
if (![[[request responseHeaders] objectForKey:header] isEqualToString:[cachedHeaders objectForKey:header]]) { |
344 |
[[self accessLock] unlock]; |
345 |
return NO; |
346 |
} |
347 |
} |
348 |
} |
349 |
|
350 |
if ([self shouldRespectCacheControlHeaders]) { |
351 |
|
352 |
// Look for X-ASIHTTPRequest-Expires header to see if the content is out of date |
353 |
NSNumber *expires = [cachedHeaders objectForKey:@"X-ASIHTTPRequest-Expires"]; |
354 |
if (expires) { |
355 |
if ([[NSDate dateWithTimeIntervalSince1970:[expires doubleValue]] timeIntervalSinceNow] >= 0) { |
356 |
[[self accessLock] unlock]; |
357 |
return YES; |
358 |
} |
359 |
} |
360 |
|
361 |
// No explicit expiration time sent by the server |
362 |
[[self accessLock] unlock]; |
363 |
return NO; |
364 |
} |
365 |
|
366 |
|
367 |
[[self accessLock] unlock]; |
368 |
return YES; |
369 |
} |
370 |
|
371 |
- (ASICachePolicy)defaultCachePolicy |
372 |
{ |
373 |
[[self accessLock] lock]; |
374 |
ASICachePolicy cp = defaultCachePolicy; |
375 |
[[self accessLock] unlock]; |
376 |
return cp; |
377 |
} |
378 |
|
379 |
|
380 |
- (void)setDefaultCachePolicy:(ASICachePolicy)cachePolicy |
381 |
{ |
382 |
[[self accessLock] lock]; |
383 |
if (!cachePolicy) { |
384 |
defaultCachePolicy = ASIAskServerIfModifiedWhenStaleCachePolicy; |
385 |
} else { |
386 |
defaultCachePolicy = cachePolicy; |
387 |
} |
388 |
[[self accessLock] unlock]; |
389 |
} |
390 |
|
391 |
- (void)clearCachedResponsesForStoragePolicy:(ASICacheStoragePolicy)storagePolicy |
392 |
{ |
393 |
[[self accessLock] lock]; |
394 |
if (![self storagePath]) { |
395 |
[[self accessLock] unlock]; |
396 |
return; |
397 |
} |
398 |
NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)]; |
399 |
|
400 |
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; |
401 |
|
402 |
BOOL isDirectory = NO; |
403 |
BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; |
404 |
if (!exists || !isDirectory) { |
405 |
[[self accessLock] unlock]; |
406 |
return; |
407 |
} |
408 |
NSError *error = nil; |
409 |
NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:path error:&error]; |
410 |
if (error) { |
411 |
[[self accessLock] unlock]; |
412 |
[NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",path]; |
413 |
} |
414 |
for (NSString *file in cacheFiles) { |
415 |
[fileManager removeItemAtPath:[path stringByAppendingPathComponent:file] error:&error]; |
416 |
if (error) { |
417 |
[[self accessLock] unlock]; |
418 |
[NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",path]; |
419 |
} |
420 |
} |
421 |
[[self accessLock] unlock]; |
422 |
} |
423 |
|
424 |
+ (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request |
425 |
{ |
426 |
NSString *cacheControl = [[[request responseHeaders] objectForKey:@"Cache-Control"] lowercaseString]; |
427 |
if (cacheControl) { |
428 |
if ([cacheControl isEqualToString:@"no-cache"] || [cacheControl isEqualToString:@"no-store"]) { |
429 |
return NO; |
430 |
} |
431 |
} |
432 |
NSString *pragma = [[[request responseHeaders] objectForKey:@"Pragma"] lowercaseString]; |
433 |
if (pragma) { |
434 |
if ([pragma isEqualToString:@"no-cache"]) { |
435 |
return NO; |
436 |
} |
437 |
} |
438 |
return YES; |
439 |
} |
440 |
|
441 |
+ (NSString *)keyForURL:(NSURL *)url |
442 |
{ |
443 |
NSString *urlString = [url absoluteString]; |
444 |
if ([urlString length] == 0) { |
445 |
return nil; |
446 |
} |
447 |
|
448 |
// Strip trailing slashes so http://allseeing-i.com/ASIHTTPRequest/ is cached the same as http://allseeing-i.com/ASIHTTPRequest |
449 |
if ([[urlString substringFromIndex:[urlString length]-1] isEqualToString:@"/"]) { |
450 |
urlString = [urlString substringToIndex:[urlString length]-1]; |
451 |
} |
452 |
|
453 |
// Borrowed from: http://stackoverflow.com/questions/652300/using-md5-hash-on-a-string-in-cocoa |
454 |
const char *cStr = [urlString UTF8String]; |
455 |
unsigned char result[16]; |
456 |
CC_MD5(cStr, (CC_LONG)strlen(cStr), result); |
457 |
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]]; |
458 |
} |
459 |
|
460 |
- (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request |
461 |
{ |
462 |
// Ensure the request is allowed to read from the cache |
463 |
if ([request cachePolicy] & ASIDoNotReadFromCacheCachePolicy) { |
464 |
return NO; |
465 |
|
466 |
// If we don't want to load the request whatever happens, always pretend we have cached data even if we don't |
467 |
} else if ([request cachePolicy] & ASIDontLoadCachePolicy) { |
468 |
return YES; |
469 |
} |
470 |
|
471 |
NSDictionary *headers = [self cachedResponseHeadersForURL:[request url]]; |
472 |
if (!headers) { |
473 |
return NO; |
474 |
} |
475 |
NSString *dataPath = [self pathToCachedResponseDataForURL:[request url]]; |
476 |
if (!dataPath) { |
477 |
return NO; |
478 |
} |
479 |
|
480 |
// If we get here, we have cached data |
481 |
|
482 |
// If we have cached data, we can use it |
483 |
if ([request cachePolicy] & ASIOnlyLoadIfNotCachedCachePolicy) { |
484 |
return YES; |
485 |
|
486 |
// If we want to fallback to the cache after an error |
487 |
} else if ([request complete] && [request cachePolicy] & ASIFallbackToCacheIfLoadFailsCachePolicy) { |
488 |
return YES; |
489 |
|
490 |
// If we have cached data that is current, we can use it |
491 |
} else if ([request cachePolicy] & ASIAskServerIfModifiedWhenStaleCachePolicy) { |
492 |
if ([self isCachedDataCurrentForRequest:request]) { |
493 |
return YES; |
494 |
} |
495 |
|
496 |
// If we've got headers from a conditional GET and the cached data is still current, we can use it |
497 |
} else if ([request cachePolicy] & ASIAskServerIfModifiedCachePolicy) { |
498 |
if (![request responseHeaders]) { |
499 |
return NO; |
500 |
} else if ([self isCachedDataCurrentForRequest:request]) { |
501 |
return YES; |
502 |
} |
503 |
} |
504 |
return NO; |
505 |
} |
506 |
|
507 |
@synthesize storagePath; |
508 |
@synthesize defaultCachePolicy; |
509 |
@synthesize accessLock; |
510 |
@synthesize shouldRespectCacheControlHeaders; |
511 |
@end |